Unity自定义编辑器

Tulenber 24 April, 2020 ⸱ Intermediate ⸱ 6 min ⸱ 2019.3.10f1 ⸱

先前有关程序生成的帖子给了我们一个很好的机会,了解定制Unity编辑器的可能性。

Unity Editor是一种强大而灵活的工具,可让您创建并非常灵活地自定义控件。 这些组件的主要目的是提供无需编写代码即可配置所有内容的功能。 从某种意义上讲,这将Unity变成了第五代编程语言。

第五代

编程语言通常分为五代。如果直接进入第五部分,则可以简单地描述为使用可视化开发工具创建应用程序而无需编程知识的系统。在企业界,这是一个普遍的话题。每个有效的经理都梦想着给任何分析师或设计师一个工具,以便他可以在没有程序员参与的情况下做所有事情。

我认为,游戏开发与此类系统的建立最为紧密。当然,最引人注目的示例是UnrealEngine中的蓝图。Unity具有大量用于同一目的的第三方资产,并且已经建立了自己的Visual Scripting引擎已有几年了。2020版承诺提供预览版,尽管此任务并非具有最高优先级。

当然,这种方法的主要优点是降低了进入阈值。如果我们对蓝图采取相同的示例,那么在他们的帮助下,很可能无需使用代码就可以制作出完整的游戏,因此每个人都有相同的可能性。或者至少在开始解决性能问题之前看起来像这样。

作为权衡,当您不得不复制蓝图中来自StackOverflow的图片而不是复制一段代码时,您将获得特定的知识交流。而且,一个相当大的问题是在分布式开发中这些格式的合并。在这种情况下,最常见的解决方案是禁止多个人同时使用蓝图。

如果我们在U​​nity中不考虑可视化编程和其他资产,那么用于更改对象参数显示或添加自定义编辑器的基本机制也可以大大简化使用寿命。例如,让我们使用在中实现的项目上一篇,并进行两项改进:

  • 添加了从编辑器生成高度图的功能,这将允许测试结果而无需进入播放模式
  • 创建一个自定义的色高编辑器,这将大大简化设置

CustomEditor

此编辑器扩展允许您根据需要更改标准组件的外观。例如,我们直接在编辑器中添加地图生成按钮,而无需进入播放模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(NoiseMap))]
public class NoiseMapEditorGenerate : Editor
{
    public override void OnInspectorGUI()
    {
        // 获取组件
        NoiseMap noiseMap = (NoiseMap)target;

        // 绘制标准视图并听取更改以生成新地图
        if (DrawDefaultInspector())
        {
            noiseMap.GenerateMap();
        }

        // 添加按钮以重新生成地图
        if (GUILayout.Button("Generate"))
        {
            noiseMap.GenerateMap();
        }
    }
}

结果,NoiseMap组件将获得一个额外的按钮,该按钮将在Renderer中创建并显示地图,而无需启动场景。
Noise map

CustomPropertyDrawer和EditorWindow

让我提醒您,以前, 根据Perlin Noise生成高度图。
Previous map

颜色调整是通过类型为List的对象进行的,如下所示:
Terrain levels

结果,将颜色添加到范围的中间需要手动转移所有其他值,这不是非常令人愉快的体验。作为原始方法的替代方法,我们可以使用EditorWindow创建自定义颜色编辑器。

  1. 色线类
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class TerrainLevels
{
    // 负责颜色的结构
    [Serializable]
    public struct ColorKey
    {
        [SerializeField] private string name;
        [SerializeField] private Color color;
        [SerializeField] private float height;

        public ColorKey(string name, Color color, float height)
        {
            this.name = name;
            this.color = color;
            this.height = height;
        }

        public string Name => name;
        public Color Color => color;
        public float Height => height;
    }

    [SerializeField] private List<ColorKey> levels = new List<ColorKey>();

    // 可用颜色数
    public int LevelsCount => levels.Count;

    // 基本构造函数
    public TerrainLevels()
    {
        levels.Add(new ColorKey("White", Color.white, 1));
    }

    // 使颜色与高度匹配
    public Color HeightToColor(float height)
    {
        // 底色是最高的颜色
        Color retColor = levels[levels.Count - 1].Color; 
        foreach (var level in levels)
        {
            // 如果我们在下面找到颜色,请选择它
            if (height < level.Height)
            {
                retColor = level.Color;
                break;
            }
        }

        return retColor;
    }

    // 垂直尺的纹理
    public Texture2D GenerateTextureVertical(int height)
    {
        Texture2D texture = new Texture2D(1, height);
        return FillTexture(texture, height);
    }

    // 水平尺的纹理
    public Texture2D GenerateTextureHorizontal(int width)
    {
        Texture2D texture = new Texture2D(width, 1);
        return FillTexture(texture, width);
    }

    // 用高度的颜色填充纹理
    private Texture2D FillTexture(Texture2D texture, int size)
    {
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;

        // 一次设置所有颜色的纹理更加容易
        Color[] colors = new Color[size];
        for (int i = 0; i < size; i++)
        {
            // 填充颜色栏
            colors[i] = HeightToColor((float) i / (size - 1));
        }

        texture.SetPixels(colors);
        texture.Apply();

        return texture;
    }

    // 获取颜色数据
    public ColorKey GetKey(int i)
    {
        return levels[i];
    }

    // 添加颜色
    public int AddKey(Color color, float height)
    {
        ColorKey newKey = new ColorKey("New key", color, height);
        for (int i = 0; i < levels.Count; i++)
        {
            if (newKey.Height < levels[i].Height)
            {
                // 保持列表按高度排序
                levels.Insert(i, newKey);
                return i;
            }
        }
        
        levels.Add(newKey);

        return levels.Count - 1;
    }

    // 去除颜色
    public void RemoveKey(int index)
    {
        if (levels.Count > 1)
        {
            levels.RemoveAt(index);    
        }
    }

    // 更新颜色
    public void UpdateKeyColor(string name, int index, Color newColor)
    {
        levels[index] = new ColorKey(name, newColor, levels[index].Height);
    }

    // 更新刻度上的颜色位置
    public int UpdateKeyHeight(int keyIndex, float newHeight)
    {
        Color col = levels[keyIndex].Color;
        RemoveKey(keyIndex);
        return AddKey(col, newHeight);
    }
}
  1. 自定义属性渲染器,用于我们的颜色标尺类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(TerrainLevels))]
public class TerrainLevelsPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        Event guiEvent = Event.current;

        // 获取带有颜色标尺的对象
        TerrainLevels terrainLevels = (TerrainLevels) fieldInfo.GetValue(property.serializedObject.targetObject);

        // 计算组件标题的大小和绘制标尺的矩形
        float labelWidth = GUI.skin.label.CalcSize(label).x + 5;
        Rect textureRect = new Rect(position.x + labelWidth, position.y, position.width - labelWidth, position.height);

        // 重画标尺
        if (guiEvent.type == EventType.Repaint)
        {
            GUI.Label(position, label);
            GUI.DrawTexture(textureRect,terrainLevels.GenerateTextureHorizontal((int)position.width));
        }

        // 打开编辑器窗口
        if (guiEvent.type == EventType.MouseDown 
            && guiEvent.button == 0 
            && textureRect.Contains(guiEvent.mousePosition))
        {
            TerrainLevelsEditor window = EditorWindow.GetWindow<TerrainLevelsEditor>();
            window.SetTerrainLevels(terrainLevels);
        }
    }
}
  1. 色彩标尺编辑器
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
using UnityEditor;
using UnityEngine;
using Random = UnityEngine.Random;

public class TerrainLevelsEditor : EditorWindow
{
    // 颜色按高度结构
    private TerrainLevels _terrainLevels;

    // 矩形画尺
    private Rect _levelRulerRect;
    // 最终颜色的矩形
    private Rect[] _keyRects;

    // 标尺宽度
    private const int LevelRullerWidth = 25;

    // 关键尺寸
    private const int BorderSize = 10;
    private const float KeyWidth = 60;
    private const float KeyHeight = 20;

    // 编辑支持
    private bool _mouseDown = false;
    private int _selectedKeyIndex = 0;

    private bool _repaint = false;

    private void OnEnable()
    {
        // 在打开窗口时设置标题和大小参数
        titleContent.text = "Terrain level editor";
        position.Set(position.x, position.y, 300, 400);
        minSize = new Vector2(300, 400);
        maxSize = new Vector2(300, 1500);
    }

    // 关闭编辑器时将场景标记为脏
    private void OnDisable()
    {
        UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
    }

    // 将颜色行传递给编辑器
    public void SetTerrainLevels(TerrainLevels levels)
    {
        _terrainLevels = levels;
    }

    private void OnGUI()
    {
        // 基本编辑器渲染
        Draw();
        Input();

        if (_repaint)
        {
            _repaint = false;
            Repaint();
        }
    }

    private void Draw()
    {
        // 画一个颜色标尺
        _levelRulerRect = new Rect(BorderSize,BorderSize, LevelRullerWidth, position.height - BorderSize * 2);
        GUI.DrawTexture(_levelRulerRect, _terrainLevels.GenerateTextureVertical((int)_levelRulerRect.height));

        // 绘制特定颜色
        _keyRects = new Rect[_terrainLevels.LevelsCount];
        for (int i = 0; i < _terrainLevels.LevelsCount; i++)
        {
            TerrainLevels.ColorKey key = _terrainLevels.GetKey(i);
            Rect keyRect = new Rect(_levelRulerRect.xMax + BorderSize, _levelRulerRect.height - _levelRulerRect.height * key.Height, KeyWidth, KeyHeight);

            // 对于当前选择的颜色,画一个框架
            if (i == _selectedKeyIndex)
            {
                EditorGUI.DrawRect(new Rect(keyRect.x - 2, keyRect.y - 2, keyRect.width + 4, keyRect.height + 4), Color.black);
            }

            // 画颜色
            EditorGUI.DrawRect(keyRect, key.Color);

            // 根据特定颜色的亮度,选择字体颜色以提高可读性
            float brightness = key.Color.r * 0.3f + key.Color.g * 0.59f + key.Color.b * 0.11f;
            GUIStyle style = new GUIStyle();
            style.normal.textColor = brightness < 0.4 ? Color.white: Color.black;

            // 在带有颜色的矩形上,我们施加高度值
            EditorGUI.LabelField(keyRect, key.Height.ToString("F"), style);
            
            _keyRects[i] = keyRect;
        }

        // 矩形显示颜色设置字段
        Rect settingsRect = new Rect(BorderSize*3 + LevelRullerWidth + KeyWidth, BorderSize, position.width - (BorderSize*4 + LevelRullerWidth + KeyWidth), position.height - BorderSize * 2);
        GUILayout.BeginArea(settingsRect);
        // 听参数的变化
        EditorGUI.BeginChangeCheck();

        // 颜色的名称输入字段
        GUILayout.BeginHorizontal();
        GUILayout.Label("Name");
        string nameText = EditorGUILayout.TextField(_terrainLevels.GetKey(_selectedKeyIndex).Name);
        GUILayout.EndHorizontal();

        // 选择颜色本身
        Color newColor = EditorGUILayout.ColorField(_terrainLevels.GetKey(_selectedKeyIndex).Color);
        // 更改参数时,更新标尺中的颜色
        if (EditorGUI.EndChangeCheck())
        {
            _terrainLevels.UpdateKeyColor(nameText, _selectedKeyIndex, newColor);
        }

        // 按钮删除颜色
        if (GUILayout.Button("Remove"))
        {
            _terrainLevels.RemoveKey(_selectedKeyIndex);
            if (_selectedKeyIndex >= _terrainLevels.LevelsCount)
            {
                _selectedKeyIndex--;
                _repaint = true;
            }
        }
        GUILayout.EndArea();
    }

    private void Input()
    {
        Event guiEvent = Event.current;
        // 通过在编辑器上单击鼠标来添加颜色
        if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0)
        {
            for (int i = 0; i < _keyRects.Length; i++)
            {
                if (_keyRects[i].Contains(guiEvent.mousePosition))
                {
                    _mouseDown = true;
                    _selectedKeyIndex = i;
                    _repaint = true;
                    break;
                }
            }

            if (!_mouseDown)
            {
                // 用随机颜色初始化
                Color randomColor = new Color(Random.value, Random.value, Random.value);
                // 根据相对于标尺的坐标,在[0,1]范围内定义颜色的高度
                float keyHeight = Mathf.InverseLerp(_levelRulerRect.y, _levelRulerRect.yMax, guiEvent.mousePosition.y);
                // 色标直接返回到编辑器的坐标
                _selectedKeyIndex = _terrainLevels.AddKey(randomColor, 1 - keyHeight);
                _mouseDown = true;
                _repaint = true;
            }
        }

        // 重置点击跟踪
        if (guiEvent.type == EventType.MouseUp && guiEvent.button == 0)
        {
            _mouseDown = false;
        }

        // Drag and drop colors when moving the mouse
        if (_mouseDown && guiEvent.type == EventType.MouseDrag && guiEvent.button == 0)
        {
            // 根据相对于标尺的坐标,在[0,1]范围内定义颜色的高度
            float keyHeight = Mathf.InverseLerp(_levelRulerRect.y, _levelRulerRect.yMax, guiEvent.mousePosition.y);
            // 色标直接返回到编辑器的坐标
            _selectedKeyIndex = _terrainLevels.UpdateKeyHeight(_selectedKeyIndex, 1 - keyHeight);
            _repaint = true;
        }
    }
}

结果,组件中的自定义属性看起来像水平标尺
Property drawer

编辑器将使编辑变得容易
Editor

结论

Unity几乎有无穷的可能性来定制界面以简化您的生活。但是,不要忘记开发成本。在我们的示例中,我们用List替换了原始版本的10行,将其替换为300行的自定义编辑器代码。因此,在决定创建自定义编辑器时,请不要忘记任何工作都需要时间,不仅要花费生产时间,还需要支持。最新版本的Unity总是存在新的错误和API更改的可能性。下次见!^_^


如果您喜欢这篇文章,可以为它提供支持



Privacy policyCookie policyTerms of service
Tulenber 2020