Unity3d——自定义编辑器Editor教程

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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值