原文地址 http://catlikecoding.com/unity/tutorials/star/编程
http://blog.csdn.net/lilanfei/article/details/7680802数组
这个教程将让你学会如何建立一个星型控件以及如何制做这个控件的自定义编辑器。你将学会:app
SerializedObject
。咱们假设你已经学会了Unity C#的基础编程知识,以及Unity 编辑器的基础知识。若是你已经完成了相关的学习,Let's Go!编辑器
咱们创建一个全新的Unity工程,而后创建一个新的C#脚本,将它命名为Star。咱们将用这个脚本,创建一个由三角面拼接成的星,这里须要一个Mesh。ide
3D模型是由多边形拼接而成,一个复杂的多边形,其实是由多个三角面拼接而成。因此一个3D模型的表面是由多个彼此相连的三角面构成。三维空间中,构成这些三角面的点以及三角形的边的集合就是Mesh。学习
using UnityEngine;
public class Star : MonoBehaviour { private Mesh mesh; }
任何对于Mesh的使用,都必须搭配一个MeshFilter组件,而MeshFilter又被用于MeshRenderer组件。只有这样,才能被Unity绘制。因此,这些组件都必须被加载到GameObject对象上,咱们的Star对象也必须这么作。ui
固然,咱们能够手动添加这些组件,但默认的自动添加是一个更好的办法。因此咱们须要添加一个RequireComponent类做为Star对象的一个特性。this
特性像对类附加一个标签,用来告诉编译器这个类须要如何处理。是除了类声明的代码以外,对类作的附加说明。另外,特性不止针对类,对方法和属性一样适用。idea
typeof是一种运算符,可以得到任何类的类型描述数据,数据里最经常使用的就是类的名字。那为何不直接在代码里写类的名字就好呢?由于代码中全部的赋值和运算都须要变量,直接使用类的名字会致使编译错误。spa
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
private Mesh mesh;
}
如今,咱们创建一个新的空GameObject,将它命名为My First Star,而后拖拽咱们的脚本Star到My First Star上。你能够看到,My First Star拥有了两个组件,MeshRenderer和Star。
下一个步骤是创建一个Mesh。咱们须要在Unity的Start事件里来作这些事,Start事件将在程序启动的时候发生。咱们还须要在MeshFilter中给这个新的Mesh起一个名字。
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
private Mesh mesh;
void Start () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; } }
固然,如今咱们在预览模式下还看不到任何东西,由于Mesh仍是空的。因此让咱们开始编辑顶点数组吧,咱们的Star类须要一个用来设置顶点数量的属性,以及这些定点与中心的相对距离。
第一个顶点是Star的中心点,其他的顶点将顺时针排列。咱们将使用四元数来计算这些点的排列。由于咱们假设俯视Z轴,因此,轮转的角度是负数,不然,将使这些点作逆时针排列。咱们不须要设置第一个点,由于vector默认会被设置成0, Mesh中使用本地坐标系。
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
public Vector3 point = Vector3.up;
public int numberOfPoints = 10; private Mesh mesh; private Vector3[] vertices; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; vertices = new Vector3[numberOfPoints + 1]; float angle = -360f / numberOfPoints; for(int v = 1; v < vertices.Length; v++){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point; } mesh.vertices = vertices; } }
三角面会被保存成顶点数组,每一个面三个顶点。由于咱们使用三角形来描述多边形,每一个三角形都起始于相同的中点,而且与其余的三角形相连。最后一个三角形与第一个三角形相连。例如,若是有四个三角形,那么顶点数组以下{0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1}。
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
public Vector3 point = Vector3.up;
public int numberOfPoints = 10;
private Mesh mesh;
private Vector3[] vertices;
private int[] triangles;
void Start () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
vertices = new Vector3[numberOfPoints + 1];
triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int v = 1, t = 1; v < vertices.Length; v++, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point; triangles[t] = v; triangles[t + 1] = v + 1; } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.triangles = triangles; } }
如今,咱们的星星看起来还只是一个简单的多边形。Unity也提示说丢失材质坐标,由于默认的Shader须要这些坐标。咱们不会使用一个纹理来描绘全部的星星,让咱们经过创建咱们本身的Shader来消除这个警告,这个Shader将只使用顶点着色。
咱们创建一个新的Shader将它命名为Star,而后写入如下代码。
Basically, data flows from the Unity engine into the graphics card, where it's processed per vertex. Then interpolated data flows from the vertices down to the individual pixels. In this case, we pass position and color data all the way down. The only additional thing we do is convert vertex positions from world space to screen space.
The statements above the CGPROGRAM switch off default lighting and depth buffer writing. Culling is switched off so we can see the triangles from both sides, not just the front. "Blend SrcAlpha OneMinusSrcAlpha" is default alpha blending, allowing for transparency.
fixed-function shader已经属于过期的技术了。 CGPROGRAM 在将数据转化成屏幕像素方面拥有更强大的功能。
Shader "Star"{ SubShader{ Tags{ "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha Cull Off Lighting Off ZWrite Off Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag struct data { float4 vertex : POSITION; fixed4 color: COLOR; }; data vert (data v) { v.vertex = mul(UNITY_MATRIX_MVP, v.vertex); return v; } fixed4 frag(data f) : COLOR { return f.color; } ENDCG } } }
如今咱们创建一个新的材质球,命名为Star,将Shader设置为咱们刚刚编写的Star,而且将这个材质球赋予My First Star。
顶点着色默认是白色,因此咱们的多边形如今变成了白色。咱们想要一个更漂亮的星星。因此咱们来为每一个点定义一种颜色。
咱们再添加一个frequency属性,这样咱们就能让程序自动重复点的序列,而不用咱们逐个定义所有的点。这个选项取代了numberOfPoints。
咱们在最后须要确认frequency属性是否正确,而且星星至少拥有一个点。若是没有,咱们的代码就可能出错。
When freshly created, our star component won't have an array yet. It's also technically possible for scripts to explicitly set our array to null later on. We need to watch out for that, to prevent errors. Only if the array does exists do we go ahead and check its length as well.
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
public Vector3[] points;
public int frequency = 1; private Mesh mesh; private Vector3[] vertices; private int[] triangles; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Vector3[]{ Vector3.up}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP]; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.triangles = triangles; } }
咱们须要些颜色!若是把所有的顶点都指定相同的颜色就很简单,但这样太无聊了。咱们来试试给每一个顶点分配一个颜色。咱们须要一个数组来保存这些颜色数据,并且必须保持颜色和顶点的数量一致。这有点小麻烦,咱们干脆换成另一种方式,在Star类中创建一个新的类,这个类能够保存一个顶点的颜色和位置。而后咱们能够用这个类的数组来代替vector数组。
这类叫Point,若是在Star类以外使用,就是Star.Point。在Star里面Point就能够了。为了让Unity可以将Point序列化,咱们为Point添加System.Serializable特性。
Because Star.Point is so lightweight and its data is always needed all at once, it would make sense to use a struct type and avoid the overhead that objects add. However, Unity does not support serialization of custom struct types. So you're stuck using classes to bundle data you want to store.
If you're really concerned about the object overhead and possible null errors, you can always store the offset and color data in two separate arrays. However, then you would need to make sure that these arrays always stay synchronized. While that is definitely doable, the class approach is simpler. That's why I use it in this tutorial.
using System;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
[Serializable] public class Point { public Color color; public Vector3 offset; } public Point[] points; public int frequency = 1; private Mesh mesh; private Vector3[] vertices; private Color[] colors; private int[] triangles; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } }
最后是关于中心点的。如今,咱们尚未给它设置颜色,因此它一直保持着透明。让咱们来为它添加一个颜色属性,最终,这个星星看上去变漂亮了。
using System;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
[Serializable]
public class Point {
public Color color;
public Vector3 offset;
}
public Point[] points;
public int frequency = 1;
public Color centerColor;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
void Start () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
if(frequency < 1){
frequency = 1;
}
if(points == null || points.Length == 0){
points = new Point[]{ new Point()};
}
int numberOfPoints = frequency * points.Length;
vertices = new Vector3[numberOfPoints + 1];
colors = new Color[numberOfPoints + 1];
triangles = new int[numberOfPoints * 3];
float angle = -360f / numberOfPoints;
colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } }
如今Star看起来不错,但设计起来有些麻烦。默认的编辑器有点蛋疼,让咱们本身作一个!
全部编辑器类的代码,都须要放在Editor文件夹下,只有这样Unity才能正确的识别这代码。Editor的名字对就行,放在哪倒无所谓。咱们如今把Editor创建在根目录也就是Assets下。而后再建一个StarInspector类的代码文件,放在Editor里面。
须要了解的是,编辑器面板不仅有一个类型。咱们这个例子里面使用的是属性面板——Inspector,其他还有 EditorWindow——编辑对话框,能够实现一个彻底自定义的弹出式对话框,还有ScriptableWizard——向导对话框,以及编辑器菜单。
由于咱们的类是一个编辑器类,它须要继承Editor类而不是MonoBehaviour。咱们还须要添加一个属性来告诉Unity,这个类是为Star类定义编辑器的。
using UnityEditor;
using UnityEngine; [CustomEditor(typeof(Star))] public class StarInspector : Editor {}
到目前为止,咱们没有改变Star的编辑器。咱们须要替换默认的编辑器。咱们能够经过重载Editor 类的OnInspectorGUI事件来实现。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
public override void OnInspectorGUI () {}
}
默认的编辑器捏?没了?
由于咱们没有在OnInspectorGUI事件里写任何代码,因此一切都是空白的。DrawDefaultInspector方法能够用来绘制默认的编辑器界面,但咱们原本就不想要这个,仍是试试别的吧。
咱们首先要确认是哪一个Star对象被选中,应该在编辑器中被显示。咱们可使用target属性来表示这个对象,target属性是Editor的一个属性,咱们继承了Editor,因此也继承了这个属性,能够直接使用它,很是方便。虽然这不是必须的,咱们能够用 SerializedObject来包装target,这么作会很方便,由于会使对不少编辑器的操做支持变得简单,好比undo。
咱们使用了SerializedObject,能够经过SerializedProperty对象来提取它的数据。咱们要在OnEnable事件里初始化全部的star类的变量。这个事件会在一个添加Star组件的GameObject被选中时发生。
SerializedObject is a class that acts as a wrapper or proxy for Unity objects. You can use it to extract data from the object even if you don't have a clue what's inside it. This is how the Unity inspector can show default inspectors for anything you create yourself. As a bonus, you get undo support for free.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private SerializedObject star;
private SerializedProperty points, frequency, centerColor; void OnEnable () { star = new SerializedObject(target); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); } public override void OnInspectorGUI () {} }
每一次编辑器更新的时候,咱们都须要肯定SerializedObject被实时更新了。这就是咱们要在OnInspectorGUI事件里作的第一件事。以后咱们能够简单的调用EditorGUILayout.PropertyField来显示咱们的属性,显示points及其内部的全部元素。以后咱们结束全部属性修改并应用到选定的组件。
EditorGUILayout is a utility class for displaying stuff in the Unity editor. It contains methods for drawing all kinds of things, in this case we're simply using the default method for drawing a SerializedProperty.
There's also an EditorGUI utility class which does that same thing, but requires you to perform your own GUI layout.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
void OnEnable () { … }
public override void OnInspectorGUI () {
star.Update();
EditorGUILayout.PropertyField(points, true); EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
如今的编辑器和默认的差很少,咱们能够作的更好。咱们须要从新整理一下points的显示格式,让每一个点的颜色和位移信息合并为一组显示。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
void OnEnable () { … }
public override void OnInspectorGUI () {
star.Update();
for(int i = 0; i < points.arraySize; i++){
EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset")); EditorGUILayout.PropertyField(point.FindPropertyRelative("color")); EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
咱们须要修正这个排版。让咱们去掉颜色和位置的标签,设置颜色条的最大长度为50像素。咱们经过EditorGUILayout.PropertyField方法的额外参数可以实现。由于咱们对全部的对象都使用相同的配置,因此咱们使用静态变量来保存这些设置。
而后再经过GUILayout.Label方法来给全部的points添加一个统一的标签。
GUIContent is a wrapper object for text, textures, and tooltips that you typically use as labels.
You use the same Unity GUI system for editors that you can use for your games. GUILayout provided basic functionality like labels and buttons, while EditorGUILayout provides extra editor-specific stuff like input fields.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent pointContent = GUIContent.none;
private static GUILayoutOption colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; void OnEnable () { … } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
最终,看上去好多了!如今,若是咱们能方便的添加和删除points就更好了。让咱们试试添加这些按钮吧。
咱们为每一个point添加两个按钮,一个是“+”用来插入point,一个是"-"用来删除point。咱们再添加一些说明使用户可以了解这些按钮的用途。咱们还须要控制按钮宽度,将样式设置成mini buttons,由于这些按钮要小一些。
The method GUILayout.Button both shows a button and returns whether it was clicked. So you typically call it inside an if statement and perform the necessary work in the corresponding code block.
What actually happens is that your own GUI method, in this case OnInspectorGUI, gets called far more often than just once. It gets called when performing layout, when repainting, and whenever a significant GUI event happens, which is quite often. Only when a mouse click event comes along that is consumed by the button, will it return true.
To get an idea, put Debug.Log(Event.current); at the start of your OnInspectorGUI method and fool around a bit.
Usually you need not worry about this, but be aware of it when performing heavy work like generating textures. You don't want to do that dozens of times per second if you don't need to.
If you insert a new array element via a SerializedProperty, the new element will be a duplicate of the element just above it. If there's no other element, it gets default values.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"), pointContent = GUIContent.none; private static GUILayoutOption buttonWidth = GUILayout.MaxWidth(20f), colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; void OnEnable () { … } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(insertContent, EditorStyles.miniButtonLeft, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
看上去不错,可是怎么移动points?若是咱们可以直接拖动这些点来排列它们,那就太棒了。虽然这确定是整齐的,让咱们用一个简单办法来解决它。
咱们能够给每一个point添加一个传送按钮。点一下,你就激活了这个point的显示。点另外一个,就会跳转到另外一个point,同时移动视角到当前point。
这种方式须要咱们来记录哪一个point是当前的焦点。咱们可使用point的索引值来记录焦点,用-1表示焦点为空。咱们将改变按钮的提示信息,信息将根据按钮的状态而定,并添加一个标签来告诉用户该作什么。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T"); private static GUILayoutOption buttonWidth = GUILayout.MaxWidth(20f), colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; private int teleportingElement; void OnEnable () { star = new SerializedObject(target); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){ if(teleportingElement >= 0){ points.MoveArrayElement(teleportingElement, i); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } else{ teleportingElement = i; teleportContent.tooltip = "teleport here"; } } if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } if(teleportingElement >= 0){ GUILayout.Label("teleporting point " + teleportingElement); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
咱们的编辑器对point已经很友好了,但咱们还不能实时看到咱们编辑过程当中的结果。是时候改变这一切了!
第一件事是让Unity了解,咱们的组件须要在编辑模式下被激活。咱们经过ExecuteInEditMode类来声明这一属性。此后,star在编辑中的任何显示,都会调用Start方法。
由于咱们创建了一个Mesh在Start方法中,它将在编辑模式下被建立。正如咱们把Mesh存放在MeshFilter中,它将被保存于场景中。咱们不但愿这样,由于咱们须要动态的建立Mesh。咱们能够设置HideFlags来阻止Unity保存Mesh。因而,咱们还须要确认Mesh被清理时,编辑器已经再也不须要它。OnDisable事件会在每个组件实效时被调用,它能够帮咱们处理这些事情。咱们须要在OnDisable中清理MeshFilter来阻止它发出缺乏Mesh的警告。
using System;
using UnityEngine;
[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
[Serializable]
public class Point { … }
public Point[] points;
public int frequency = 1;
public Color centerColor;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
void Start () {
GetComponent<MeshFilter>().mesh = mesh = new Mesh();
mesh.name = "Star Mesh";
mesh.hideFlags = HideFlags.HideAndDontSave; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } void OnDisable () { if(Application.isEditor){ GetComponent<MeshFilter>().mesh = null; DestroyImmediate(mesh); } } }
咱们的星星已经显示在了编辑模式中!当咱们在一个对象上关闭Star组件,星星的Mesh将被消除。当咱们启用Star组件,它将再也不恢复。由于Start方法仅在组件第一次激活时被调用。解决的办法是将咱们的初始化代码移动到OnEnable事件中去。
作好以后,咱们进一步重构代码,让咱们能随时初始化Mesh。为了在不须要的时候不进行初始化,咱们还须要添加少许的检查。
using System;
using UnityEngine;
[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
[Serializable]
public class Point { … }
public Point[] points;
public int frequency = 1;
public Color centerColor;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
public void UpdateStar () { if(mesh == null){ GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; mesh.hideFlags = HideFlags.HideAndDontSave; } if(frequency < 1){ frequency = 1; } if(points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; if(vertices == null || vertices.Length != numberOfPoints + 1){ vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; } float angle = -360f / numberOfPoints; colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } void OnEnable () { UpdateStar (); } void OnDisable () { … } }
如今,组件再被启动时,星星再也不出现。不幸的是,它再也不相应修改。幸亏,这很容易解决。
SerializedObject.ApplyModifiedProperties方法能够返回任何修改的实际状况。这样,咱们就能很简单的调用target的UpdateStar方法。咱们须要显式转换target的类型,由于编辑器须要为全部类型提供支持,因此target的类型被定义成了Object。
译者注,有一种方法能够简单的解决这个问题,写一个基类以下
public class InspectorBase<T> : Editor where T : UnityEngine.Object { protected T Target { get { return (T)target; } } }
而后所有的编辑器类都继承这个基类以下
[CustomEditor(typeof(Star))] public class StarEditor : InspectorBase< Star > { ...... }
这样在之后的代码里,target会自动成为你想要的类型。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () {
star.Update();
GUILayout.Label("Points");
for(int i = 0; i < points.arraySize; i++){
EditorGUILayout.BeginHorizontal();
SerializedProperty point = points.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);
if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
if(teleportingElement >= 0){
points.MoveArrayElement(teleportingElement, i);
teleportingElement = -1;
teleportContent.tooltip = "start teleporting this point";
}
else{
teleportingElement = i;
teleportContent.tooltip = "teleport here";
}
}
if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
points.InsertArrayElementAtIndex(i);
}
if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
points.DeleteArrayElementAtIndex(i);
}
EditorGUILayout.EndHorizontal();
}
if(teleportingElement >= 0){
GUILayout.Label("teleporting point " + teleportingElement);
}
EditorGUILayout.PropertyField(frequency);
EditorGUILayout.PropertyField(centerColor);
if(star.ApplyModifiedProperties()){ ((Star)target).UpdateStar(); } } }
如今,Mesh没有当即更新。这让编辑轻松许多!惋惜的是,它尚未支持Undo!
不幸的是,在Unity中没有一种简单的方法来支持Undo事件,但咱们能够作到接近支持。在咱们的案例中,咱们能够检查ValidateCommand事件是否发生,来判断Undo操做。当前被选中的对象这个事件的目标,咱们假设它被修改过。
ValidateCommand is a type of GUI event, which indicates that some special action happened, like undo or redo. So why isn't it called something like ExecuteCommand? Actually, that command type exists as well. While they have a slightly different meaning, in practice you use them for the exact same purpose. Unfortunately, depening on exactly where you're checking and how you're constructing your GUI, either one or the other event happens, but not both. Why this is so, I do not know.
So to be perfectly safe, you have to check for both command types. In this case, however, you can suffice with checking ValidateCommand.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () {
star.Update();
GUILayout.Label("Points");
for(int i = 0; i < points.arraySize; i++){
EditorGUILayout.BeginHorizontal();
SerializedProperty point = points.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);
if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
if(teleportingElement >= 0){
points.MoveArrayElement(teleportingElement, i);
teleportingElement = -1;
teleportContent.tooltip = "start teleporting this point";
}
else{
teleportingElement = i;
teleportContent.tooltip = "teleport here";
}
}
if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
points.InsertArrayElementAtIndex(i);
}
if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
points.DeleteArrayElementAtIndex(i);
}
EditorGUILayout.EndHorizontal();
}
if(teleportingElement >= 0){
GUILayout.Label("teleporting point " + teleportingElement);
}
EditorGUILayout.PropertyField(frequency);
EditorGUILayout.PropertyField(centerColor);
if(
star.ApplyModifiedProperties() ||
(Event.current.type == EventType.ValidateCommand && Event.current.commandName == "UndoRedoPerformed") ){ ((Star)target).UpdateStar(); } } }
最后,一个舒服的编辑过程!还有什么须要作吗?在编辑器的右上角有一个齿轮图标可以重置组件。当咱们重置Star组件的时候咱们的Mesh没有及时更新。
你能够定义Reset方法来监听一个组件的重置。这事Unity为Editor及其子类提供的一个方法。当这个事件发生,咱们只要及时更新咱们的星星就能够了。
using System;
using UnityEngine;
[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
[Serializable]
public class Point { … }
public Point[] points;
public int frequency = 1;
public Color centerColor;
private Mesh mesh;
private Vector3[] vertices;
private Color[] colors;
private int[] triangles;
public void UpdateStar () { … }
void OnEnable () { … }
void OnDisable () { … }
void Reset () {
UpdateStar(); } }
OK咱们开始写Reset。咱们要作什么?咱们来试试prefabs?
如今使用prefabs对于咱们star并无太多意义,由于每个star都拥有本身的独立的Mesh。若是你想使用不少个同样的star,那在创建一个3D模型而且导入Mesh是一个好主意。这样全部的star就共享了同一个Mesh。但假设咱们使用prefab,就能够实例化多个一样的star而后咱们还可以调整它们。
你只要简单的拖拽一个star从层级视图到项目视图,就能创建一个prefab。对prefab的更新可以影响所有的prefab实例,由于每一个prefab的修改都会触发OnDisable和OnEnable。将一个实例回复成prefab一样的状态它依然可以工做。
惟一咱们没有彻底作好的事情是prefab的MeshFilter会显示它的Mesh类型不匹配。这事由于prefab是一个实际的资源,而动态生成的Mesh不是。这不影响功能,但仍是让咱们解决它吧。
为了中止prefab生成它的Mesh,咱们不能再调用UpdateStar方法。不幸的是,这表明咱们将不能再看到预览了。咱们能够用PrefabUtility.GetPrefabType方法来检测编辑窗口当前的对象是否是prefab。若是是,咱们简单的不更新它就好了。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () {
star.Update();
GUILayout.Label("Points");
for(int i = 0; i < points.arraySize; i++){
EditorGUILayout.BeginHorizontal();
SerializedProperty point = points.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);
if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
if(teleportingElement >= 0){
points.MoveArrayElement(teleportingElement, i);
teleportingElement = -1;
teleportContent.tooltip = "start teleporting this point";
}
else{
teleportingElement = i;
teleportContent.tooltip = "teleport here";
}
}
if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
points.InsertArrayElementAtIndex(i);
}
if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
points.DeleteArrayElementAtIndex(i);
}
EditorGUILayout.EndHorizontal();
}
if(teleportingElement >= 0){
GUILayout.Label("teleporting point " + teleportingElement);
}
EditorGUILayout.PropertyField(frequency);
EditorGUILayout.PropertyField(centerColor);
if(
star.ApplyModifiedProperties() ||
(Event.current.type == EventType.ValidateCommand &&
Event.current.commandName == "UndoRedoPerformed")
){
if(PrefabUtility.GetPrefabType(target) != PrefabType.Prefab){
((Star)target).UpdateStar();
} } } }
OK,咱们完成了,真的?我没尚未对同时存在多个对象的状况进行支持。试试同时选择多个star。
让咱们尝试多对象编辑功能吧。首先,咱们须要给类添加一个属性让编辑器提供相应的支持。而后咱们须要初始化全部target的SerializedObject,而再也不只是一个。咱们还须要把任何变化同步到所有的target上。
这样就能在编辑器中支持多个对象了,但若是一些star的point个数不同,就会出错。由于在Unity的编辑器尝试读取所有点的资料的时候,有些点会不存在。咱们能够在得到每一个point的数据的时候检查一下这个point是否存在,若是不存在,就中止取值。因此咱们只须要显示一个star所拥有的数量的point就能够了。
using UnityEditor;
using UnityEngine;
[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () {
star = new SerializedObject(targets); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ SerializedProperty point = points.GetArrayElementAtIndex(i), offset = point.FindPropertyRelative("offset"); if(offset == null){ break; } EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(offset, pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){ if(teleportingElement >= 0){ points.MoveArrayElement(teleportingElement, i); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } else{ teleportingElement = i; teleportContent.tooltip = "teleport here"; } } if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } if(teleportingElement >= 0){ GUILayout.Label("teleporting point " + teleportingElement); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); if( star.ApplyModifiedProperties() || (Event.current.type == EventType.ValidateCommand && Event.current.commandName == "UndoRedoPerformed") ){ foreach(Star s in targets){ if(PrefabUtility.GetPrefabType(s) != PrefabType.Prefab){ s.UpdateStar(); } } } } }
如今咱们拥有了一个很不错的编辑器了,但若是咱们能直接在场景里编辑这些point会不会更酷一些?用OnSceneGUI事件,咱们能够作到。这个方法会在一个对象被选中即将赋予target时调用。咱们不能在这个事件中使用SerializedObject。事实上,你能够认为这个方法与咱们编辑器类中的其它部分是彻底分离的。
Probably for backwards compatibility. Multi-object editing was introduced in Unity 3.5. Versions before that only had the target variable.
using UnityEditor;
using UnityEngine;
[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () { … }
void OnSceneGUI () {}
}
让咱们设置一个方形的小手柄在star所有的point上面。咱们只要在这些point的第一个重复周期里显示手柄就能够了,不须要把所有的重复周期都显示出来。放置这些手柄就好象生成Mesh同样,除了咱们使用的是世界坐标系,不是本地坐标系,因此咱们要用到star的transform。
咱们能够经过Handles.FreeMoveHandle方法来绘制咱们的手柄。首先,须要一个世界坐标系的位置,手柄的位置。其次,须要一个绘制手柄的角度,但咱们不须要旋转。而后,还须要手柄的尺寸,咱们用一个很小的尺寸就够了。咱们用一个vector来保存这个尺寸,能够设置成(0.1, 0.1 0.1)。最后一个参数是定义手柄的形状。
You convert a point from local to world space by appling all transformation matrices of its object hierarchy to it. Unity takes care of this when rendering the scene, but sometimes you need to do it yourself. You can use the Transform.TransformPoint method for this.
using UnityEditor;
using UnityEngine;
[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static Vector3 pointSnap = Vector3.one * 0.1f;
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () { … }
void OnSceneGUI () {
Star star = (Star)target; Transform starTransform = star.transform; float angle = -360f / (star.frequency * star.points.Length); for(int i = 0; i < star.points.Length; i++){ Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i); Vector3 oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset); Handles.FreeMoveHandle(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap); } } }
如今还有什么能够作到更好吗?你能够点击一个手柄,让它变成黄色。咱们须要比较一个手柄的初始化位置和返回位置。若是不一样,说明用户拖动了手柄,咱们须要将改变同步到star。star的Mesh使用本地坐标系,在把坐标改变保存以前,不要忘记转换坐标。
You have to perform the exact opposite steps for converting to world space, in reverse order. You can use the Transform.InverseTransformPoint method for this. Note that when going to world space we rotated in local space first, then transformed. So to convert back, we inverse transform first, then inverse rotate in local space.
using UnityEditor;
using UnityEngine;
[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static Vector3 pointSnap = Vector3.one * 0.1f;
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () { … }
void OnSceneGUI () {
Star star = (Star)target;
Transform starTransform = star.transform;
float angle = -360f / (star.frequency * star.points.Length);
for(int i = 0; i < star.points.Length; i++){
Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
Vector3
oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset),
newPoint = Handles.FreeMoveHandle (oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap); if(oldPoint != newPoint){ star.points[i].offset = Quaternion.Inverse(rotation) * starTransform.InverseTransformPoint(newPoint); star.UpdateStar(); } } } }
有用了!不过咱们还没支持Undo!这里咱们不能靠SerializedObject来解决问题,不过幸亏这些手柄能够支持Undo。咱们只须要告诉编辑器哪一个对象被改变了,咱们还应该为此次改变起一个名字。咱们能够用Undo.SetSnapshotTarget来作这些事。
If an undo step would be created for each GUI event, dragging a handle would result in an undo history filled with dozens of tiny modifications. Instead, the handles make a copy – a snapshot – of the object when movement begins and only register a single undo step with the copy when movement ends. SetSnapshotTarget tells the handles which object to use for this.
All Unity editor GUI elements essentialy do the same thing, whether it's for draggin handles, sliding numbers, typing text, or whatever.
using UnityEditor;
using UnityEngine;
[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {
private static Vector3 pointSnap = Vector3.one * 0.1f;
private static GUIContent
insertContent = new GUIContent("+", "duplicate this point"),
deleteContent = new GUIContent("-", "delete this point"),
pointContent = GUIContent.none,
teleportContent = new GUIContent("T");
private static GUILayoutOption
buttonWidth = GUILayout.MaxWidth(20f),
colorWidth = GUILayout.MaxWidth(50f);
private SerializedObject star;
private SerializedProperty
points,
frequency,
centerColor;
private int teleportingElement;
void OnEnable () { … }
public override void OnInspectorGUI () { … }
void OnSceneGUI () {
Star star = (Star)target;
Transform starTransform = star.transform;
Undo.SetSnapshotTarget(star, "Move Star Point");
float angle = -360f / (star.frequency * star.points.Length);
for(int i = 0; i < star.points.Length; i++){
Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
Vector3
oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset),
newPoint = Handles.FreeMoveHandle
(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap);
if(oldPoint != newPoint){
star.points[i].offset = Quaternion.Inverse(rotation) *
starTransform.InverseTransformPoint(newPoint);
star.UpdateStar();
}
}
}
}