Star,自定义编辑器的介绍
一、介绍
在本教程中,您将创建一个简单的star控件,并为他编写属于自己的自定义编辑器。你会学到:
l 动态生成一个mesh;
l 使用一个嵌套类;
l 创建一个自定义编辑器;
l 使用SerializedObject;
l 支持WYSIWYG(所见即所得)的编辑;
l 对撤销,重做,还原和预设修改做出反映;
l 支持多对象编辑;
l 在场景视图中支持编辑。
假设你已经知道了你周围的Unity的Editor并且知道Utnity C#的脚本的基本用法。如果你已经完成了其他教程的学习,那么你可以往下进行。
二、创建Star
首先我们创建一个新的项目并且添加一个新的C#脚本命名为Star。我们将使用这个脚本创建一个三角形风扇(trangle fan)来产生stat的效果,这个triangle还需要Mesh。
using UnityEngine; public class Star : MonoBehaviour { privateMesh mesh; } |
对于这个网格的使用,它必须被分配给一个MeshFilter组件,进而使用MeshRenderer组件,只有这样才能在Utnity中绘制Mesh。所以这是很必需的,这两个组件连接到的GameObject也会被我们的Star组件连接。
当然,我们还可以手动的添加这些组件,但是自动添加的话会更好一些。我们将添加一个RequireComponent类属性给我们的类。
using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class Star : MonoBehaviour { private Mesh mesh; } |
现在,我们创建一个新的empty GameObject,命名为MyFirstStar,将我们的Star脚本拖拽到它上。这是你会发现这个Object已经获得了另外两个Component。
下一步就是创建一个Mesh,我们会在Unity的Start方法中去创建,以便与已进入播放模式他就会去创建Mesh。我们也会把这个Mesh分配给MeshFilter,并给一个名字。
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 和在播放模式下有网格
当然现在我们在播放模式下不会看到任何东西,因为这个Mesh现在是空的。所以让我来添加一组vertices(顶点),用一个字段来控制有多少个点,这些点的位置应该是相对与Star的中心。
我们的triangle的第一个点应该在Star的中心,其他的点在他周围以顺时针的顺序放置。我们将使用Quaternion来旋转这些点,旋转是逆时针的,因为我们是从下往上看的Z轴,所以逆时针旋转的话,我们就会看到他绕着Z轴顺时针转动。
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; } } |
新的inspector属性
每个三角形的三个顶点会被存储在一个数组中。因为我们用三角扇形的方法,所以每个三角的开始点连接着上一个三角形和下一个三角形,最后一个三角形将会回到第一个三角形。例如,如果我们有四个三角形,顶点的索引将会是{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; } } |
一个简陋的Star
现在我们的star看起来就像一个简陋的多边形。Unity也会警告丢失了纹理坐标,因为默认着色器也需要他们。因为我们不在使用一个纹理,让我们来通过创建只是用顶点颜色的shader来摆脱这种警告。
让我们来创建一个新的shader 资源,并且命名为Star,下面是源码:
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 } } } |
现在我们创建一个心的material,命名为Star,shader就是我们上方创建的,再把这个material拖拽给MyFirstStar
现在的材料
顶点的默认颜色为白色,所以现在我们的多边形变成了白色。可是我们想要一个顶点距离中心不等的多彩星星。所以让我们设置一个点数组来代替单个的顶点的设置。让我们也添加一个频率的option,可以自动重复点序列而不必配置每个单点Star。用这个option代替numberOfPoints。
最后,我们包含了一个检查方法,来确定频率是正确的,至少会有一个点。如果不这样做,可能会出现数组的一些问题。
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类中创建一个新类来保存颜色和每个点的偏移值。然后用数组来代替矢量矩阵。
在Star中定义一个Point类,在Star类外中可以通过Star.Point来调用,来类内直接调用Point就可以。通过给类增加System.Serializable属性,Unity会自动的保存数据。
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; } } |
有颜色的连个顶点
如果这一步完成你还是什么也看不到,那就尝试的去调节Point颜色的alpha值。
最后一部分就是star的中心。现在,我们没有设置他的颜色,所以他一直都是透明的。所以让我来增加一个option,来设置和调整Star,让他看起来更好。
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; } } |
三、创建Inspector
现在这个Star看起来很好,但是设计很糟糕。Inspector看着不人性化。所以让我们来创建属于我们自己的Inspector。
所有的编辑器处理的脚本都应该放在一个名为Editor的文件夹中,否则他不会在Unity中正常的工作。这些文件夹放在哪都不要紧,所以我们会把它放在project的根目录下。在这里创建一个名为StarInspector的C#脚本。
因为我们的类是一个自定义编辑器,他需要继承Editor来代替继承Monobehaviour。我们还需要增加一个类属性来告诉Unity这是一个Star组件的自定义编辑器。
using UnityEditor; using UnityEngine; [CustomEditor(typeof(Star))] public class StarInspector : Editor {} |
目前为止,我们的Star的inspector还没有任何的改变。我们需要自己写代码来替换默认的inspector。通过覆写Editor的OnInspectorGUI方法来实现。
using UnityEditor; using UnityEngine; [CustomEditor(typeof(Star))] public class StarInspector : Editor { public override void OnInspectorGUI () {} } |
一个空的inspector
MyFirstStar的Star脚本下的东西没有了!。我和我的小伙伴都惊呆了,〇.〇
这是因为我们在OnInspectorGUI中什么也没有做,所以他什么都不会显示。我们可以通过调用DrawDefaultInspector方法来获得以前inspector中的内容。但这正是我们想要拜托的,所以我们不会这么做。
我们所要做的第一件事就是,当Inspector显示的时候,指出哪个Star是被选中的。我们可是使用target,它是我们继承的Editor的一个变量。我们可以直接使用target,把他封装在SerializedObject中。虽然这不是必须的,但这却是很方便的,因为它让很多东西变得更容易,像撤消编辑。
使用SerializedObject时,你可以通过访问SerializedProperty来获得它的内容。我们在Unity的事件方法OnEnable中来操作所有的三角星星的变量和初始化操作。当我们选择一个有Star控件的GameObject的时候它将会被出触发。
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 () {} } |
每次Inspector被更新,我们都需要确认SerializedObject是最新的。这就是在OnInspectorGUI中做的第一件事情。之后,我们可以通过EditorGUILayout.PropertyField这样的一个简单回调函数来显示我们的一些属性,Points也会显示自己的数组元素。最后,我们将把所修改的所有属性应用到所选择的component上。
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(); } } |
一个重建的Inspector
现在或多或少和我们一开始的Inspector一样了,但是我们通过手工绘制各个点的方式做的比这个更好。我们会遍历所有的点,把color和Offset放在彼此的旁边。
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(); } } |
一个布局很差的Inspector
我们需要去修复这个布局。让我们摆脱掉Offset和Color标签,并且设置Color颜色条的最大宽度为50px。我们可以通过EditorGUILayout.PropertyField 方法提供的额外信息来实现这一点。因为我们总会使用相同的配置,所以将这些配置存在静态变量中。
一个紧凑的inspector
最后,这看起来还是不错的。现在如果我们能在任何地方插入和删除points就更好了。所以,让我们加两个Button来实现他。
我们会给没一个点都添加两个按钮,一个标签为“+”,一个标签为“-”。我们还会加入提示,告诉用户这个按钮是用来做什么的。作为迷你按钮我们会限制按钮的宽度和风格,因为他们应该比较小。
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呢?如果我们能拖拽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(); } } |
一个很酷的Inspector已经激活瞬移
四、WYSIWYG(所见即所得)
虽然我们的Inspector在这样的point上还是不错的,但当我们编辑Star的时候并不能实际的看到它,所以我们要在时间上去改变它。
我们所需要做的第一件事就是告诉Unity,在编辑模式下我们的Component是可以操作的。我们可以通过添加ExecuteInEditMode属性类来表明这一点。这样现在当一个Star在编辑器中显示的时候我们的Start方法就会被调用。
它可以被创建在编辑模式中,是因为我们在Star方法中创建了Mesh。我们会把他给一个MeshFilter,他将一直保持下去,并且存在在当前的scene中。我们不希望这种情况发生,因为我们最后是动态生成Mesh。我们可以通过设置适当的HideFlags来防止Unity保存Mesh。但是现在我们还需要确保Mesh被清理时,它不再需要在编辑器中。最好在Utnity的OnDisable方法中去操作它,当调用它的时候这个Component将不可用。我们还会清理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刚刚出现在了编辑模式下。如果我们关掉star组建或整个对象,Star的Mesh就会消失。如果我们返回去的话,他不会再次出现。那是因为Start方法只有在第一次被调用时,这个Component才会被激活。解决办法就是将我们的初始化方法转移到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 () { … } } |
现在当Star再次出现的时候,其组件将被再次调用,不幸的是,他不响应修改,幸运的是这很容易解决。
SerializedObject.ApplyModifiedProperties方法返回任何修改是否实际上操作了。如果是这样我们只需要调用Update方法的target,我们需要强制转换Star,因为一个编辑器可以处理所有类型的对象,所以它使用Object类型。
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会立即得到更新。这让编辑变得更容易!可惜的是,他不响应撤销。
很不幸,还没有简单可行的方法来实现撤销事件,但是我们可以做的非常接近。在我们的例子中,我可以通过检测“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(); } } } |
最后,编辑变得很好了。那么还有其他的什么?怎么重置我们的Component呢?在每个Component的右上方的齿轮图标中给这个Component设置一个重置选项。当你重置star的Component的时候我们的Mesh没有更新。
你可以通过增加一个Reset方法来重置component。这是一个Utnity的方法,只能用在Editor中。无论什么时候这个时间触发,我们就需要去更新我们的Star。
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(); } } |
好的,现在也重置工程。那么prefabs怎么样了呢?
现在对我们的Star使用prefabs是没有多大意义的,因为每个Star都有自己的Mesh。如果你想使用大量的类似的Star,在3D编辑下导入mesh来创建Star这将会是个不错的方法。这样所有的Star就会共享一样的Mesh。但假设我们想使用perfabs,只是来实例化类似的Star,可能以后还会单独调整。
幸运的是,你可以简单地拖动一个Star从hierarchy到project中,这时你会得到一个perfab。 那些已经作出反映的Update的方法可以延伸到perfab的初始化中,因为每个perfab的修改都会触发Ondisable和OnEnable方法。返回一个示例给perfab也是他应该做的。
还有一件事没有完成,那就是prefab的MeshFilter显示的类型与mesh的值不匹配,这是因为prefab是一种资产,而生成的Mesh是不是。这似乎没什么事,但我们也要解决他。
要停止prefab产生自己的Mesh,我们就不能调用Update方法。很不幸的是,这也意味着我们不能在显示预览了。我们可以使用 PrefabUtility.GetPrefabType方法去检测我们在inspector中的目标是不是一个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(); } } } } |
现在还没完,我们不是不想在同意时间编辑多个Star,而是现在没有这么做。让我们选择多个Star来尝试下。
那么现在让我们来支持多个对象的编辑。首先,我们要添加一个类属性来表明我们的Editor支持它。然后我们需要初始化包含多个目标的SerialzedObject,而不是某个单一的对象。我们还要确定当我们检测到变化的时候会更新所有的target。
这样可以编辑多个对象,但是如果选择的Star的points数量不同则会出错。这是因为GUI会常识的去寻找那些不存在的点(译者:溢出)。我们可以通过每个point的offset来阻止他,检查point是否存在。如果没有,就停止。所以我们显示的Star的point数量与最少point的Star相同。
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(); } } } } } |
四、在Scene中的编辑
现在我们的确有个很好的Inspector,但是如果能在scene视图中直接编辑point那会更酷的。通过给Inspector增加OnSceneGUI就可以这样做了。在Object被target标记期间,每次选择对象都调用这个方法。这里我们不应该用SerializedObject。实际上这个把editor中剩余部分完成的想法是很好的。
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第一次出现时这样做,而不是反复的重复这样的操作。替换这些point的工作就好像制作Mesh一样,只不过我们在world中做,而不是local中,所以我们需要star的transform。
我们会用有一对参数的Handles.FreeMoveHandle方法来画句柄。首先我们要在world为handle获得一个点。然后还要离开旋转handle的旋转角度。接着就是句柄的大小,我们需要一个较小的值。然后一个vector被用作snapping的大小(hold Control or Command to snap),我们设置为(0.1,0.1,0.1)。最后一个参数是用来定义句柄的shape。
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); } } } |
我们现在有个很好的句柄了,可是还有很多没有做。比如:你可以点击他变成黄色。我们需要做的是把我们放进去的handle和返回的handle相比较。如果不同的话,用户拖拽handle,我们就应该修改star。在分配point的offset和更新Star之前,我们不应该忘记用新的position去覆盖star在local中的位置。
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(); } } } } |
是的就是这么做。稍等,他不支持撤销。这里我们不能依靠 SerializedObject ,但是幸运的是handle可以为我们提供关于撤销的东西。我们只要告诉他,那个对象是要被编辑的,撤销步骤该如何命名。我们可以用 Undo.SetSnapshotTarget方法来做。
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(); } } } } |
And with that,we're done! Have fun designing stars!