More than a year has passed since the release of the Unity UI system, so Richard Fine decided to write about its predecessor, IMGUI. In the last part of the material we covered how to create a MyCustomSlider. We have a simple functional element IMGUI, which can be used in custom editors, PropertyDrawers, EditorWindows, etc. But this is not all. In the second part of the article we will talk about how to expand its functionality, for example, add the ability to multi-edit.
Control functions')
Another important point is the relationship of IMGUI with the Scene View component. You must be familiar with auxiliary UI elements, such as orthogonal arrows, rings, or lines that allow you to move, rotate, and scale objects. These elements are called control functions. Interestingly, they are also supported in IMGUI.
The standard GUI and EditorGUI class elements used in Unity Editor / EditorWindows are two-dimensional, but the basic concepts of IMGUI, such as control identifiers and event types, are not tied to either the Unity editor or 2D. The control functions for the three-dimensional Scene View elements are represented by the
Handles class, which replaces the GUI and EditorGUI. For example, instead of the
EditorGUI.IntField function that creates an element for editing a single integer, you can use the function that allows you to edit the value of Vector3 using the interactive arrows in Scene View:
Vector3 PositionHandle(Vector3 position, Quaternion rotation);
Control functions can also be used to create custom interface elements. In this case, the basic concepts remain the same as when creating elements of the editor, although interaction with the mouse is somewhat complicated: in a three-dimensional environment, it is not enough to simply check that the coordinates of the cursor correspond to the rectangle. This is where the
HandleUtility class
comes in handy.By writing the OnSceneGUI function in the custom class of the editor, you can use the control functions in the editors, and the functions from the GUI in the Scene View. To do this, you will have to make additional efforts: install GL-matrices or use
Handles.BeginGUI () and
Handles.EndGUI () to set the context.
State objectsIn the case of MyCustomSlider, we needed to track 2 things: the floating value of the slider (which was transmitted by the user and returned to it) and the change of the slider at a specific point in time (for this we used the hotControl element). But what if the item contains much more information?
IMGUI provides a simple storage system for so-called state objects associated with interface elements. To do this, define a new class that will be used to store data, and associate a new object with the control identifier. Each object can be assigned no more than one identifier, and IMGUI does it by itself - using the built-in constructor. When you load the editor code, such objects are not serialized (even if the [Serializable] label is set), so they cannot be used for long-term data storage.
Suppose we need a button that returns TRUE each time it is pressed, but turns red when holding it for longer than two seconds. To track the time a button is pressed, we use the state object. Let's declare a class:
public class FlashingButtonInfo { private double mouseDownAt; public void MouseDownNow() { mouseDownAt = EditorApplication.timeSinceStartup; } public bool IsFlashing(int controlID) { if (GUIUtility.hotControl != controlID) return false; double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt; if (elapsedTime < 2f) return false; return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0; } }
The time a button is pressed will be stored in the mouseDownAt property when MouseDownNow () is called, and the IsFlashing function will determine whether the button should be red-lit at the moment. Naturally, if hotControl is not involved or less than two seconds have passed since the button was pressed, the button will not light up. But otherwise, its color will change every 0.1 seconds.
Now we will write the code for the button itself:
public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style) { int controlID = GUIUtility.GetControlID (FocusType.Native);
Everything is very simple. Note that the code snippets for the response to mouseDown and mouseUp are very similar to those we used earlier to handle the capture of the slider in the scroll bar. The only differences are the state.MouseDownNow () call when the mouse button is pressed, and the GUI.color value is changed when the button is redrawn.
You may have noticed another difference related to the redraw event, namely the style.Draw () call. This is worth talking in more detail.
GUI stylesWhen creating our first element, we used
GUI.DrawTexture to draw the slider itself. But with the FlashingButton element, everything is not so simple - the button should include not only an image in the form of a rounded rectangle, but also an inscription. We could try to draw a button using GUI.DrawTexture and place GUI.Label on top of it, but there is a better way. Let's try to use the
GUI.Label image drawing technique without using the GUI.Label itself.
The
GUIStyle class contains information about the visual properties of an interface element: from font and text color to the spacing between elements. In addition, GUIStyle stores functions used to determine the width and height of objects using a style, as well as to directly draw elements on the screen.
GUIStyle can include different styles for drawing an element: when the cursor is hovering over it, when it has received keyboard focus, when it is disabled or when it is active (with the mouse button held down). For any state, you can define a color and a background image, and GUIStyle will substitute them when drawing an element, based on its control ID.
There are 4 ways to use GUIStyles for drawing interface elements:
• Write a new style (new GUIStyle ()), setting the required values.
• Use one of the built-in styles of the
EditorStyles class (if you want your custom elements to look like standard).
• If you need to slightly change the existing style, for example, align the button text to the right. You can copy the desired style of the EditorStyles class and change the desired property manually.
• Extract style from
GUISkin .
GUISkin is a large collection of GUIStyle objects that can be created in the project itself as a separate resource and edited using the Unity Inspector. Having created a new GUISkin and opening it, you will see slots for all standard interface elements: buttons, text boxes, switches, etc. But the section on user styles is of particular interest. Here you can put any number of GUIStyle objects with unique names that can be retrieved using the
GUISkin.GetStyle (“style_name”) method. It remains to figure out how to load GUISkin objects from code. There are several ways to do this. If the object is in the Editor Default Resources folder, use the
EditorGUIUtility.LoadRequired () function; to load from another directory use
AssetDatabase.LoadAssetAtPath () . The main thing - in no case do not put resources intended only for the editor, in resource packages or in the Resources folder.
Now that we have a GUIStyle, we can draw a
GUIContent containing the desired text, image, and tooltip using
GUIStyle.Draw () . The arguments are the coordinates of the rectangle in which the drawing is performed, the GUIContent itself and the control identifier.
IMGUI markupYou may have noticed that each of the interface elements we examined had a Rect parameter that determines its position on the screen. At the same time, we just talked about the fact that GUIStyle includes markup properties. This begs the question: do you really need to manually calculate all the values of Rect, taking into account the peculiarities of the markup? In principle, it is possible. But IMGUI offers a simpler solution - a markup engine that does this automatically.
For this there is a special type of event - EventType.Layout. After IMGUI sends such an event to the interface, its elements call markup functions:
GUILayoutUtility.GetRect () ,
GUILayout.BeginHorizontal /
Vertical , and
GUILayout.EndHorizontal /
Vertical and others. IMGUI remembers the results of these calls as a tree that contains all the interface elements and the space they need. After the tree is built, it is recursively traversed, during which the sizes of the elements and their position relative to each other are calculated.
When any other event is fired, for example EventType.Repaint, the elements call the markup functions again. But this time IMGUI repeats the “recorded” calls and returns the finished rectangles. In other words, if during the Layout event the parameters of the rectangles have already been calculated using the GUILayoutUtility.GetRect () function, when another event is triggered, it will simply substitute the previously saved result.
By analogy with identifiers of control elements, when executing a Layout event and other events, it is important to follow the order of calls to markup functions so that elements do not receive data from foreign rectangles. It is also worth considering that the values returned by the GUILayoutUtility.GetRect () call during the Layout event are useless, since IMGUI will not know which element each rectangle corresponds to until the end of the event and the processing of the tree.
So let's add some markup for our bar with a slider. This is easy: by getting a square from IMGUI, we can call the ready code:
public static float MyCustomSlider(float value, GUIStyle style) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style); return MyCustomSlider(position, value, style); }
If GUILayoutUtility.GetRect is called during the Layout event, IMGUI remembers that a certain style is needed for empty content (empty because no image or text is specified for it). During other events, GetRect returns an existing rectangle. It turns out that during the Layout event our element MyCustomSlider will be called with an irregular rectangle, but this is unimportant, because without it we still cannot call GetControlID ().
All data on the basis of which IMGUI determines the size of the rectangle is contained in the style. But what if the user wants to set one or more parameters manually?
To do this, use the class
GUILayoutOption . Objects of this class are a kind of instructions for the marking system, indicating how exactly the rectangle should be calculated (for example, have a certain height / width value or fill the available space vertically / horizontally). To create such an object, you need to call the factory functions of the class GUILayout, such as
GUILayout.ExpandWidth () or
GUILayout.MinHeight () , and transfer them to GUILayoutUtility.GetRect () as an array. They are then stored in the markup tree and taken into account when processing it.
Instead of creating custom arrays from GUILayoutOption objects, we use the C # params keyword, which allows you to call a method with any number of parameters from which an array is automatically constructed. Here is the new feature of our band:
public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts); return MyCustomSlider(position, value, style); }
As you can see, all data entered by the user is passed straight to GetRect.
A similar method of combining the function of an IMGUI element with a version of the same function using automatic markup placement is applicable to any IMGUI element, including the built-in GUI class. It turns out that the
GUILayout class provides hosted versions of elements from the GUI class (and we use the
EditorGUILayout class corresponding to the EditorGUI).
In addition, elements placed automatically and manually can be combined. The space is reserved using GetRect, after which it can be divided into separate sections for different elements. The markup system does not use control identifiers, so several elements can be placed on one rectangle (or vice versa). Sometimes this approach works much faster than with fully automatic placement.
Note that when writing PropertyDrawers it is not recommended to use markup; instead, it is better to use a rectangle passed to the PropertyDrawer.OnGUI () overload. The point is that the Editor class itself does not use markup, but computes a simple rectangle that shifts down for each next property. Therefore, if markup is used for PropertyDrawer, the Editor will not know about the previous properties and, therefore, will not place the rectangle correctly.
Using serialized propertiesSo, you can already create your own IMGUI element. It remains to discuss a couple of points that will help bring it to the Unity quality standard.
First is the use of
SerializedProperty . We'll talk about the serialization system in more detail in the next article, but for now let's summarize: the SerializedProperty interface allows you to access any property that the Unity serialization system (load and save) is connected to. Thus, we can use any variable from scripts or objects displayed in the Unity Inspector.
SerializedProperty provides access not only to the value of a variable, but also to various kinds of information, for example, comparing the current and initial values of a variable or the state of a variable with its child fields in the Inspector window (minimized / expanded). In addition, the interface integrates any user-defined variable value changes into the Undo and scene-dirtying systems. It does not use a managed version of the object, which has a positive effect on performance. Therefore, the use of SerializedProperty is necessary for the full functioning of any complex interface elements.
The signature of EditorGUI class methods that take SerializedProperty objects as arguments is somewhat different from the usual one. Such methods do not return anything, because changes are made directly to the SerializedProperty. An improved version of our band will look like this:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)
Now we have no value parameter, prop is passed instead to SerializedProperty. With
prop.floatValue, we can retrieve the value of the floating number when drawing the strip and change it when dragging the slider.
There are other benefits of using SerializedProperty in IMGUI code. Suppose the value of
prefabOverride shows changes in the value of a property in a template object. By default, the modified properties are in bold, but we can set a different display style using GUIStyle.
Another important possibility is editing multiple objects, that is, displaying several values at once with the help of a single element. If
EditorGUI.showMixedValue is set to TRUE, the item is used to display multiple values.
Using the prefabOverride and showMixedValue mechanisms requires setting the context for a property using
EditorGUI.BeginProperty () and
EditorGUI.EndProperty () . As a rule, if an element's method accepts an argument of the SerializedProperty class, it must itself call BeginProperty and EndProperty. If it accepts “pure” values (for example, the EditorGUI.IntField method, which accepts an int and does not work with properties), calls to BeginProperty and EndProperty should be contained in the code that calls this method.
public class MySliderDrawer : PropertyDrawer { public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } private GUISkin _sliderSkin; public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { if (_sliderSkin == null) _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin"); MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label); } }
ConclusionI hope this article will help you understand the basics of IMGUI. To become a true professional, you have to master many other aspects: the SerializedObject / SerializedProperty system, the features of working with CustomEditor / EditorWindow / PropertyDrawer, the use of the Undo class, etc. Anyway, IMGUI allows you to unlock the widest potential of Unity to create custom tools for sale on the Asset Store or personal use.