Not so long ago, I participated in the development of the game on Unity. I spent a lot of time on tools for colleagues: a level editor for a game designer, convenient databases for artists.
For obvious reasons, in programming interfaces under Unity, we cannot always use automatic layout ( convenient GUILayout tools), and often we have to manually cut rectangles and draw interfaces using GUI class tools. This work is tedious, associated with a large number of errors, and the code is difficult to maintain. Over time, the thought familiar to every programmer arose: I'll write my bike! "There must be a better way!" . For details, I invite under the cat.
The picture for attracting attention is taken from here .
GUILayout to calculate the size of the drawn fields, calls the OnGUI method twice:
event.type == EventType.Layout
draws nothing, it only collects data about all components to be drawn in order to calculate the size of the rectangle for each of them;Do not forget about the overhead for calculating rectangles. Thus, the understandable reasons here are optimization .
Thanks to this logic, the developer is waiting for the brain-blowing artifacts of drawing and bugs, when in samopisnye editors (most often it refers to the heirs of EditorWindow), he mixes up the use of both GUILayout and GUI methods. For example, if GUILayoutUtility.GetLastRect()
or GUILayoutUtility.GetRect () is used GUILayoutUtility.GetLastRect()
and the result is stored in the field, or the logic is tied to delegates and callbacks.
Problems are well treated by checking in critical places:
if (Event.current.type != EventType.Layout) { ... }
The very first time I read the PropertyDrawer documentation , I was terrified of working with rectangles. They suggest doing it this way:
// Calculate rects var amountRect = new Rect(position.x, position.y, 30, position.height); var unitRect = new Rect(position.x + 35, position.y, 50, position.height); var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height); // Draw fields - passs GUIContent.none to each so they are drawn without labels EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none); EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none); EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);
Often in tutorials they do this (I changed the code a bit to emphasize the particular approach to working with rectangles):
// Draw first field position.width -=200; EditorGUI.PropertyField (position, property.FindPropertyRelative("level"), GUIContent.none); // Shift rect and draw second field position.x = position.width + 20; position.width = position.width - position.width - 10; EditorGUI.PropertyField (position, property.FindPropertyRelative("type"), GUIContent.none );
It should be noted that at that moment I did not know about the possibilities of GUILayout. Imagine my horror when I decided that this is the only way to draw a UI that Unity can offer?
Years passed, I wrote a lot of interfaces using both GUI and GUILayout, but I could not reconcile with this situation. In my next article, I'm going to tell you how I stopped worrying and started cutting rectangles correctly.
When a newcomer to Unity wants to bring these or other settings to the inspector, he simply makes the field public adds to the [SerializeField]
field and enjoys the simplicity and convenience of the editor. But when his database grows, a harsh reality awaits him: the standard inspector is terrible .
Pros:
Minuses:
[Header]
, [Range]
, etc.);Probably every programmer encountered a problem: he added [SerializeField]
, but nothing appeared in the inspector? All the magic is that the class of the field must have the attribute [Serializable]
, which does not always come to mind.
For clarity, consider a simple database of heroes of the popular saga (contains the most important characteristics of the characters):
using System; using UnityEngine; [CreateAssetMenu] public class SimplePeopleDatabase : ScriptableObject { [SerializeField] private Human[] people; [Serializable] public class Human { [SerializeField] private string name; [SerializeField] private Gender gender; [SerializeField, Tooltip(" ")] private int earedness; } public enum Gender { Undefined = 0, Male = 1, Female = 2, } }
It looks good when there are 5 characters in the list, but with an increase in the size of the database, the convenience and beauty of the standard inspector makes itself felt.
Fortunately, the developers of Unity make us an offer that we simply cannot refuse: write your own editor. We will not write the whole CustomEditor, just change the rendering of the Human class: write HumanPropertyDrawer. I put it in the same file for simplicity of presentation, but in real projects I do not advise to do this: I have often met situations when, with a sufficient level of fatigue, I get into git on Friday evening using UnityEditor;
not wrapped in preprocessor directives.
using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [CreateAssetMenu] public class SimplePeopleDatabaseWithCustomEditor : ScriptableObject { [SerializeField] private Human[] people; [Serializable] public class Human { [SerializeField] private string name; [SerializeField] private Gender gender; [SerializeField, Tooltip(" ")] private int earedness; } public enum Gender { Undefined = 0, Male = 1, Female = 2, } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(Human))] public class HumanPropertyDrawer : PropertyDrawer { private const float space = 5; public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) { int indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; var firstLineRect = new Rect( x: rect.x, y: rect.y, width: rect.width, height: EditorGUIUtility.singleLineHeight ); DrawMainProperties(firstLineRect, property); EditorGUI.indentLevel = indent; } private void DrawMainProperties(Rect rect, SerializedProperty human){ rect.width = (rect.width - 2*space) / 3; DrawProperty(rect, human.FindPropertyRelative("name")); rect.x += rect.width + space; DrawProperty(rect, human.FindPropertyRelative("gender")); rect.x += rect.width + space; DrawProperty(rect, human.FindPropertyRelative("earedness")); } private void DrawProperty(Rect rect, SerializedProperty property){ EditorGUI.PropertyField(rect, property, GUIContent.none); } } #endif }
In principle, it looks good. The database even with dozens of characters fits on the screen. But what is good for the game designer is bad for the programmer: how much code he had to write, what can we say about his support. When the number of fields changes, dances begin with cutting a line, when adding a complex field (another class with several fields), one has to describe all its fields.
The complexity of maintaining the code leads to its inertia: every time the database needs to be "slightly changed", there will be many problems with parted positions, changed field names, etc. When reflection appears in the code, you can simply forget about refactoring the code: standard IDE tools can no longer cope. The errors that appear are not detected at the compilation stage, and in order to notice them, it is necessary that someone look into this database in the inspector.
Pros:
Minuses:
[Tootip]
attribute), which no one expected.All the disadvantages of the first option are fully resolved, but there is not a trace of its advantages.
using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [CreateAssetMenu] public class PeopleDatabaseWithCustomEditor : ScriptableObject { [SerializeField] private Human[] people; [Serializable] public class Human { [SerializeField] private string name; [SerializeField] private Gender gender; [SerializeField, Tooltip(" ")] private int earedness; [SerializeField] private Pet[] pets; } public enum Gender { Undefined = 0, Male = 1, Female = 2, } [Serializable] public class Pet { [SerializeField] private PetType type; [SerializeField] private string name; [SerializeField, Tooltip(" ?")] private bool combat; } public enum PetType { Undefined = 0, Wolf = 1, Dragon = 2, Raven = 3, } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(Human))] public class HumanPropertyDrawer : PropertyDrawer { private const float space = 5; public override float GetPropertyHeight(SerializedProperty property, GUIContent label){ return EditorGUIUtility.singleLineHeight + EditorGUI.GetPropertyHeight(property.FindPropertyRelative("pets"), null, true); } public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) { int indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; var firstLineRect = new Rect( x: rect.x, y: rect.y, width: rect.width, height: EditorGUIUtility.singleLineHeight ); DrawMainProperties(firstLineRect, property); var petsRect = new Rect( x: rect.x, y: rect.y + firstLineRect.height, height: rect.height - firstLineRect.height, width: rect.width ); DrawPets(petsRect, property.FindPropertyRelative("pets")); // Separator var lastLine = new Rect( x: rect.x, y: rect.y + rect.height - EditorGUIUtility.singleLineHeight / 2, width: rect.width, height: 1 ); EditorGUI.DrawRect(lastLine, Color.gray); EditorGUI.indentLevel = indent; } private void DrawMainProperties(Rect rect, SerializedProperty human){ rect.width = (rect.width - 2*space) / 3; DrawProperty(rect, human.FindPropertyRelative("name")); rect.x += rect.width + space; DrawProperty(rect, human.FindPropertyRelative("gender")); rect.x += rect.width + space; DrawProperty(rect, human.FindPropertyRelative("earedness")); } private void DrawProperty(Rect rect, SerializedProperty property){ EditorGUI.PropertyField(rect, property, GUIContent.none); } private void DrawPets(Rect rect, SerializedProperty petsArray){ var countRect = new Rect( x: rect.x, y: rect.y, height: EditorGUIUtility.singleLineHeight, width: rect.width ); var label = new GUIContent("Pets"); petsArray.arraySize = EditorGUI.IntField(countRect, label, petsArray.arraySize); var petsRect = new Rect( x: rect.x + EditorGUIUtility.labelWidth, y: rect.y, width: rect.width - EditorGUIUtility.labelWidth, height: 18 ); petsArray.isExpanded = true; for (int i = 0; i < petsArray.arraySize; i++){ petsRect.y += petsRect.height; EditorGUI.PropertyField(petsRect, petsArray.GetArrayElementAtIndex(i)); } petsArray.isExpanded = true; } } [CustomPropertyDrawer(typeof(Pet))] public class PetPropertyDrawer : PropertyDrawer { private const float space = 2; public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) { int indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; float combatFlagWidth = 20; rect.width = (rect.width - 2*space - combatFlagWidth) / 2; DrawProperty(rect, property.FindPropertyRelative("type")); rect.x += rect.width + space; DrawProperty(rect, property.FindPropertyRelative("name")); rect.x += rect.width + space; rect.width = combatFlagWidth; DrawProperty(rect, property.FindPropertyRelative("combat")); EditorGUI.indentLevel = indent; } private void DrawProperty(Rect rect, SerializedProperty property){ EditorGUI.PropertyField(rect, property, GUIContent.none); } } #endif }
Although the result looks good, it is difficult to convey in words all the emotions regarding this code. And even worse to imagine what will happen next.
The first thing that comes to mind: someone has already encountered such a problem, let's go look in the AssetStore. And we find in it a lot of assets, aimed at improving the standard inspector: they allow placing buttons (calling methods of the object) in the inspector, preview of textures, add the possibility of branching and much more. But almost all of them are paid, and most of them follow the “one field - one line” paradigm, and the functions added by them only further increase the vertical size of the database.
In general, enough introductions, it's time to talk about why we all gathered here. I wrote my asset: OneLine
.
What does OneLine do? Draws in one line the field marked by the attribute, along with all the nested fields (it will be clear in the examples below).
Add the attribute [OneLine]
to draw in one line and [HideLabel]
to hide the unnecessary in our case title, eating up a third of the length of the line.
using System; using UnityEngine; using OneLine; [CreateAssetMenu] public class SimplePeopleDatabaseWithOneLine : ScriptableObject { [SerializeField, OneLine, HideLabel] private Human[] people; [Serializable] public class Human { [SerializeField] private string name; [SerializeField] private Gender gender; [SerializeField, Tooltip(" ")] private int earedness; } public enum Gender { Undefined = 0, Male = 1, Female = 2, } }
Stretching margins in width in OneLine looks weird with dynamic arrays (and the list of pets is just that), so we rigidly set the width to all fields with the [Width]
attribute. This time the picture is clickable.
using System; using UnityEngine; using OneLine; [CreateAssetMenu] public class PeopleDatabaseWithOneLine : ScriptableObject { [SerializeField, OneLine, HideLabel] private Human[] people; [Serializable] public class Human { [SerializeField, Width(130)] private string name; [SerializeField, Width(75)] private Gender gender; [SerializeField, Tooltip(" "), Width(25)] private int earedness; [SerializeField] private Pet[] pets; } public enum Gender { Undefined = 0, Male = 1, Female = 2, } [Serializable] public class Pet { [SerializeField, Width(60)] private PetType type; [SerializeField, Width(100)] private string name; [SerializeField, Tooltip(" ?")] private bool combat; } public enum PetType { Undefined = 0, Wolf = 1, Dragon = 2, Raven = 3, } }
It turns out quite widely, but when working with a database, it is quite possible to stretch it to the full screen. In future versions, I plan to give OneLine the ability to draw too large fields in a few lines.
How does all this work? Mainly due to the ability to write a PropertyDrawer for an attribute. On what the attribute hangs, it draws and draws. Plus a bit of reflection, the help of the decompiled code of Unity libraries on the githaba and a little imagination. I will write about the main difficulties on the way later.
OneLine features:
[Tooltip]
) in the code;OneLine Restrictions:
At first everything seemed simple: recursively go through all the fields and draw everything that meets on the way, in one line. In life, everything turned out to be more complicated:
property.isArray == true
? Here I did not know;property.propertyPath
and reflection to get all the attributes hanging on the field (and again the crutches associated with arrays);[HideLabel]
and [Highlight]
;It should be noted that many of the problems on the road are obstacles that Unity itself is putting before us. As a result, some kind of workaround is necessary to solve problems, including relying on the knowledge of the implementation of certain parts of the editor and reflection.
This approach leads to the fragility of the code, because in the next version of the editor OneLine may suddenly stop working correctly. On the one hand, I hope this does not happen. On the other hand, problems need to be solved in any case, and the open API necessary for this is not and is not expected.
When I wrote databases for artists, I constantly set up detailed field hints with [Tooltip]
, replacing the documentation. With this attribute in the inspector it is enough to hover the mouse on the field, and a pop-up little cloud will tell you everything about it.
Problem: as I wrote earlier, a simple call to `EditorGUI.PropertyField (rect, property, content) does not draw hints. Interestingly, SerializedProperty contains the tootlip property (note the comprehensive documentation), but it always turned out to be empty when I was accessing it. Chyadt ?
Solution: we take the SerializedProperty.propertyPath
and with reflexion in our hands we crawl along the path from the very beginning (pay attention to the arrays), when we crawl to the end, we can learn all the attributes of the field. I use this method of getting field attributes not only for working with prompts.
public static T[] GetCustomAttributes<T>(this SerializedProperty property) where T : Attribute { string[] path = property.propertyPath.Split('.'); bool failed = false; var type = property.serializedObject.targetObject.GetType(); FieldInfo field = null; for (int i = 0; i < path.Length; i++) { field = type.GetField(path[i], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.FlattenHierarchy | BindingFlags.Instance); type = field.FieldType; // : .fieldName // : .fieldName.Array.data[0] int next = i + 1; if (next < path.Length && path[next] == "Array") { i += 2; if (type.IsArray) { type = type.GetElementType(); } else { type = type.GetGenericArguments()[0]; } } } return field.GetCustomAttributes(typeof(T), false) .Cast<T>() .ToArray(); }
In a large database, a long line containing many fields may look too "even", that is, the problem "there is nothing to catch on." I was faced with the task of providing a mechanism for highlighting particularly important fields in a line.
In accordance with the policy of the party, the selection of the field will produce an attribute [Highlight]
.
The implementation is very simple:
[Highlight]
attribute on the field;EditorGUI.DrawRect(rect, color);
and tint the rectangle with the desired color.Problem: as a result of the known Unity bug (which is marked Won't fix ), the backlight appears and disappears.
Solution: described here . It looks easy. Interestingly, for what reason did Unity developers refuse to repair it?
The main feature of OneLine is that it should work out of the box: add one attribute and everything is drawn in one line. But life always suits us all sorts of pod. And one of them: decorators. These attributes are [Header]
, [Space]
(and any others that you can write yourself, extending DecoratorDrawer). It turned out that with the usual call of `EditorGUI.PropertyField (rect, property, content) all decorators are also drawn.
Problem:
Solution: At first I tried to find a workaround and even asked for Unity Answers , but with no result. Then I rummaged in UnityEditor.dll decompiled source code ( here ), and got the following solution:
typeof(EditorGUI) .GetMethod("DefaultPropertyField", BindingFlags.NonPublic | BindingFlags.Static) .Invoke(null, new object[]{rect, property, GUIContent.none});
The current version of OneLine v0.2.0 .
You can follow the situation on the githaba . The readme functional is described in more detail.
Source: https://habr.com/ru/post/340536/
All Articles