Unity Custom Editors

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

The previous post about procedural generation gives us an excellent opportunity to look at the possibilities of customization of the Unity Editor.

Unity Editor is a potent and flexible tool that allows you to create and quite flexibly customize controls. The primary purpose of such components is to provide the ability to configure everything without code writing. In a sense, this turns Unity into a fifth-generation programming language.

Fifth-generation

Programming languages usually divided into five generations. If you go straight to the fifth, then briefly it can be described as a system for creating application programs using visual development tools, without programming knowledge. In the corporate world, this is a prevalent topic. Every effective manager dreams of giving any analyst or designer a tool so that he can do everything without the participation of a programmer.

In my opinion, game development came to the creation of such systems most closely. The most striking example, of course, are blueprints in the UnrealEngine. Unity has a large set of third-party assets for the same purpose and has been building its Visual Scripting engine for several years now. Version 2020 promises a preview version of it, although this task is not with the highest priority.

Of course, the main advantage of this approach is the lowering of the entry threshold. If we take the same example with blueprints, then with their help, it is quite possible to make a full-fledged game without using code, so everyone has the same possibilities. Or at least it seems like that before you begin resolving performance troubles.

As a tradeoff, you will receive a specific exchange of knowledge when instead of copying a piece of code, you have to repeat the picture from StackOverflow in your blueprint. And also, a rather big problem is the merge of such formats in distributed development. The most common solution, in this case, is the ban on simultaneous work with blueprints for several people.

If we put aside visual programming and additional assets in Unity, then the basic mechanisms for changing the display of object parameters or the addition of custom editors can also greatly simplify life. For example, let’s use the project that we implement in a previous post and create two improvements:

  • Add the ability to generate a heightmap from the editor, which will allow testing the result without going to Playmode
  • Create a custom color-height editor, which will significantly facilitate the settings

CustomEditor

This editor extension allows you to change the appearance of standard components to your needs. As an example, we add the map generation button directly in the editor, without going to Playmode

 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()
    {
        // Get the component
        NoiseMap noiseMap = (NoiseMap)target;

        // Draw the standard view and listen for changes to generate a new map
        if (DrawDefaultInspector())
        {
            noiseMap.GenerateMap();
        }

        // Add a button to regenerate the map
        if (GUILayout.Button("Generate"))
        {
            noiseMap.GenerateMap();
        }
    }
}

As a result, the NoiseMap component will receive an additional button that will create and display a map in Renderer without scene launching.
Noise map

CustomPropertyDrawer and EditorWindow

Let me remind you that previously, we generated a heightmap based on Perlin Noise.
Previous map

The color adjustment took place through an object of type List and looked like this:
Terrain levels

As a result, adding color to the middle of the range requires manual transfer of all other values, which is not a very pleasant experience. As an alternative to the original method, we can create a custom color editor using EditorWindow.

  1. Color Line Class
  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
{
    // The structure responsible for color
    [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>();

    // Number of available colors
    public int LevelsCount => levels.Count;

    // Base constructor
    public TerrainLevels()
    {
        levels.Add(new ColorKey("White", Color.white, 1));
    }

    // Match color to height
    public Color HeightToColor(float height)
    {
        // Base is the highest color
        Color retColor = levels[levels.Count - 1].Color; 
        foreach (var level in levels)
        {
            // If we find the color below, then select it
            if (height < level.Height)
            {
                retColor = level.Color;
                break;
            }
        }

        return retColor;
    }

    // Texture for vertical ruler
    public Texture2D GenerateTextureVertical(int height)
    {
        Texture2D texture = new Texture2D(1, height);
        return FillTexture(texture, height);
    }

    // Texture for the horizontal ruler
    public Texture2D GenerateTextureHorizontal(int width)
    {
        Texture2D texture = new Texture2D(width, 1);
        return FillTexture(texture, width);
    }

    // Fill the texture with the color of heights
    private Texture2D FillTexture(Texture2D texture, int size)
    {
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;

        // Easier to set the texture from an entire array of colors at once
        Color[] colors = new Color[size];
        for (int i = 0; i < size; i++)
        {
            // Fill the color bar
            colors[i] = HeightToColor((float) i / (size - 1));
        }

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

        return texture;
    }

    // Get color data
    public ColorKey GetKey(int i)
    {
        return levels[i];
    }

    // Add color
    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)
            {
                // Keep List sorted by heights
                levels.Insert(i, newKey);
                return i;
            }
        }
        
        levels.Add(newKey);

        return levels.Count - 1;
    }

    // Remove the color
    public void RemoveKey(int index)
    {
        if (levels.Count > 1)
        {
            levels.RemoveAt(index);    
        }
    }

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

    // Update the color position on the scale
    public int UpdateKeyHeight(int keyIndex, float newHeight)
    {
        Color col = levels[keyIndex].Color;
        RemoveKey(keyIndex);
        return AddKey(col, newHeight);
    }
}
  1. Custom property renderer for our color ruler class
 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;

        // Get an object with a ruler of colors
        TerrainLevels terrainLevels = (TerrainLevels) fieldInfo.GetValue(property.serializedObject.targetObject);

        // Calculate the size of the component title and the rectangle to draw the ruler
        float labelWidth = GUI.skin.label.CalcSize(label).x + 5;
        Rect textureRect = new Rect(position.x + labelWidth, position.y, position.width - labelWidth, position.height);

        // Redraw the ruler
        if (guiEvent.type == EventType.Repaint)
        {
            GUI.Label(position, label);
            GUI.DrawTexture(textureRect,terrainLevels.GenerateTextureHorizontal((int)position.width));
        }

        // Open the editor window
        if (guiEvent.type == EventType.MouseDown 
            && guiEvent.button == 0 
            && textureRect.Contains(guiEvent.mousePosition))
        {
            TerrainLevelsEditor window = EditorWindow.GetWindow<TerrainLevelsEditor>();
            window.SetTerrainLevels(terrainLevels);
        }
    }
}
  1. Color Ruler Editor
  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
{
    // Colors by height structure
    private TerrainLevels _terrainLevels;

    // Rectangle to draw the ruler
    private Rect _levelRulerRect;
    // Rectangles of final colors
    private Rect[] _keyRects;

    // Ruler width
    private const int LevelRullerWidth = 25;

    // Key dimensions
    private const int BorderSize = 10;
    private const float KeyWidth = 60;
    private const float KeyHeight = 20;

    // Editing support
    private bool _mouseDown = false;
    private int _selectedKeyIndex = 0;

    private bool _repaint = false;

    private void OnEnable()
    {
        // Set the title and size parameters on window opening
        titleContent.text = "Terrain level editor";
        position.Set(position.x, position.y, 300, 400);
        minSize = new Vector2(300, 400);
        maxSize = new Vector2(300, 1500);
    }

    // Mark the scene dirty when closing the editor
    private void OnDisable()
    {
        UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEngine.SceneManagement.SceneManager.GetActiveScene());
    }

    // Pass the line of colors to the editor
    public void SetTerrainLevels(TerrainLevels levels)
    {
        _terrainLevels = levels;
    }

    private void OnGUI()
    {
        // Basic editor rendering
        Draw();
        Input();

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

    private void Draw()
    {
        // Draw a colors ruler
        _levelRulerRect = new Rect(BorderSize,BorderSize, LevelRullerWidth, position.height - BorderSize * 2);
        GUI.DrawTexture(_levelRulerRect, _terrainLevels.GenerateTextureVertical((int)_levelRulerRect.height));

        // Draw specific colors
        _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);

            // For the currently selected color, draw a frame
            if (i == _selectedKeyIndex)
            {
                EditorGUI.DrawRect(new Rect(keyRect.x - 2, keyRect.y - 2, keyRect.width + 4, keyRect.height + 4), Color.black);
            }

            // Draw the color
            EditorGUI.DrawRect(keyRect, key.Color);

            // Depending on the brightness of a particular color, select the font color, for readability purpose
            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;

            // On the rectangle with the color, we impose the height value
            EditorGUI.LabelField(keyRect, key.Height.ToString("F"), style);
            
            _keyRects[i] = keyRect;
        }

        // Rectangle to display color settings fields
        Rect settingsRect = new Rect(BorderSize*3 + LevelRullerWidth + KeyWidth, BorderSize, position.width - (BorderSize*4 + LevelRullerWidth + KeyWidth), position.height - BorderSize * 2);
        GUILayout.BeginArea(settingsRect);
        // Listen to the change of parameters
        EditorGUI.BeginChangeCheck();

        // Name input field for color
        GUILayout.BeginHorizontal();
        GUILayout.Label("Name");
        string nameText = EditorGUILayout.TextField(_terrainLevels.GetKey(_selectedKeyIndex).Name);
        GUILayout.EndHorizontal();

        // Select the color itself
        Color newColor = EditorGUILayout.ColorField(_terrainLevels.GetKey(_selectedKeyIndex).Color);
        // When changing the parameters, update the color in the ruler
        if (EditorGUI.EndChangeCheck())
        {
            _terrainLevels.UpdateKeyColor(nameText, _selectedKeyIndex, newColor);
        }

        // Button to delete color
        if (GUILayout.Button("Remove"))
        {
            _terrainLevels.RemoveKey(_selectedKeyIndex);
            if (_selectedKeyIndex >= _terrainLevels.LevelsCount)
            {
                _selectedKeyIndex--;
                _repaint = true;
            }
        }
        GUILayout.EndArea();
    }

    private void Input()
    {
        Event guiEvent = Event.current;
        // Add color by mouse click on the editor
        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)
            {
                // Initialize with random color
                Color randomColor = new Color(Random.value, Random.value, Random.value);
                // Define the height for the color in the range [0,1], according to the coordinates relative to the ruler
                float keyHeight = Mathf.InverseLerp(_levelRulerRect.y, _levelRulerRect.yMax, guiEvent.mousePosition.y);
                // The color scale is directed back to the coordinates of the editor
                _selectedKeyIndex = _terrainLevels.AddKey(randomColor, 1 - keyHeight);
                _mouseDown = true;
                _repaint = true;
            }
        }

        // Reset Click Tracking
        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)
        {
            // Define the height for the color in the range [0,1], according to the coordinates relative to the ruler
            float keyHeight = Mathf.InverseLerp(_levelRulerRect.y, _levelRulerRect.yMax, guiEvent.mousePosition.y);
            // The color scale is directed back to the coordinates of the editor
            _selectedKeyIndex = _terrainLevels.UpdateKeyHeight(_selectedKeyIndex, 1 - keyHeight);
            _repaint = true;
        }
    }
}

As a result, our custom property in the component will look like a horizontal ruler
Property drawer

And the editor will make it easy to edit
Editor

Conclusion

Unity has almost endless possibilities for customizing the interface to simplify your life; however, do not forget about the cost of development. In our example, we replaced ten lines of the original version with List to 300 lines of custom editor code. So when deciding on the creation of custom editors, do not forget that any work takes time, and not only time for production, but also support. There is always the possibility of new bugs and API changes with the latest version of Unity. See you next time! =)



Privacy policyCookie policyTerms of service
Tulenber 2020