Unity教程之-制作闪亮的星星Star(三):给Star创建Unity Editor编辑器

继续上篇文章《Unity教程之-制作闪亮的星星Star(二):创建Shader》,本篇我们来讲解 unity star editor的创建!

建立编辑器 Creating the Inspector

现在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被选中时发生。

What’s a SerializedObject?

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及其内部的所有元素。之后我们结束所有属性修改并应用到选定的组件。

What’s EditorGUILayout?

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添加一个统一的标签。

What’s a GUIContent?

GUIContent is a wrapper object for text, textures, and tooltips that you typically use as labels.

Why use GUILayout instead of EditorGUILayout?

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,因为这些按钮要小一些。

How does GUILayout.Button work?

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.

What are the contents of a new item?

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();
    }
}
有传送按钮的编辑器

所见即所得 WYSIWYG

我们的编辑器对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!

好了,本篇unity3d教程到此结束,下篇我们来看下如何自定义实现Undo!

转自:http://www.unity.5helpyou.com/3128.html


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值