More than a year has passed since the release of the Unity UI system, so Richard Fine decided to write about its predecessor, IMGUI.
At first glance, this is completely illogical. Why write about an obsolete UI system, if a new one has long been released? Well, the new system does offer a wide range of options for customizing game interfaces, but if you want to add new tools and functions to the editor, IMGUI will surely come in handy.
Getting startedSo the first question: what does
IMGUI mean? IMGUI stands for Immediate Mode GUI. This is worth telling in more detail. There are 2 main approaches to GUI systems: direct (Immediate Mode GUI) and saved (Retained Mode GUI).
')
When using the saved mode, the system remembers the information about the interface. The developer adds various elements: labels, buttons, sliders, text fields, then the data is saved and used to determine the appearance of the screen, event processing, etc. When you change the text on the label or move the button, you actually change the data stored in the system and create a new system state. When you interact with the interface, the system remembers your actions, but does nothing more without an additional request. Unity UI works in this mode. You create components like UI.Labels or UI.Buttons, configure them, and the system takes care of the rest.
When using direct mode, the system does not remember information about the interface, but instead constantly requests and refines information about its elements. Each of them must be specified by calling a function. Reaction to any user action is returned immediately, without additional request. Such a system is ineffective for the user interface of the game and inconvenient for artists due to the dependence on the code. At the same time, it is well suited for the Unity editor, whose interface already depends on the code and does not change in real time, unlike the game. Direct mode also allows you to change the displayed tools in accordance with the new state of the system.
As a digression, you can watch Casey Muratori's
excellent video , in which he examines the basic principles and benefits of direct mode.
Event handlingThe active IMGUI interface always has an event to handle, for example, “user clicks the mouse button” or “need to redraw the interface”. The type of the current event can be found in the value of Event.current.type.
Imagine that you need to add a set of buttons to the interface window. In this case, to handle each event, you must write a separate code. Schematically, this can be represented as:

Writing separate functions for each event takes a lot of time, but in their structure they are very similar. At each stage, we will perform actions with the same elements (button 1, 2 or 3). The concrete action depends on the event, but the structure remains unchanged. This means that instead of the first algorithm, you can make a single function:

The single OnGUI function calls the library functions (for example,
GUI.Button ), which perform various actions depending on the type of event being processed. Nothing complicated!
The most commonly used events are the following five types:
EventType.MouseDown - The user pressed the mouse button.
EventType.MouseUp - The user pressed the mouse button.
EventType.KeyDown - The user has pressed a key.
EventType.KeyUp - The user has pressed a key.
EventType.Repaint - Need to redraw the interface
For a complete list of event types, see the
EventType documentation.How does the standard GUI.Button control respond to events?
EventType.Repaint - Redrawing the button in the specified rectangle
EventType.MouseDown - If the coordinates of the cursor coincide with the area of ​​the button, check the “button pressed” checkbox and start the button redrawing when pressed.
EventType.MouseUp - Uncheck the box “pressed” and start redrawing the button in the released form. If the cursor is in the button area, return TRUE (the button was pressed and released).
In practice, everything is not so simple. For example, buttons should respond to keystroke events. In addition, you need to add a code that stops the response to the MouseUp from any buttons except the one that the cursor was hovering at the time of clicking. In any case, if the GUI.Button is called at the same place in the code and retains the same coordinates and content, the code snippets will work together to define the button behavior.
For the unified model of the interface behavior at various events in IMGUI, the control identifier is used - control ID. Using this identifier, you can make uniform calls to the interface elements in any event. A Control ID is assigned to each interface element with non-trivial interactive behavior. The assignment is performed depending on the order of requests, therefore, if the interface functions are called in the same order from different events, they will be assigned the same identifiers, and the events will be synchronized.
Creating interface elementsThe
GUI and
EditorGUI classes provide a library of standard Unity interface elements. You can use them to create your own Editor, EditorWindow, or PropertyDrawer classes.
Novice developers often neglect the GUI class, preferring to use EditorGUI. In fact, the elements of both classes are equally useful and can be shared to enhance the functionality of the editor. The main difference is that EditorGUI elements cannot be used in game interfaces, as they are part of the editor, while GUI elements are part of the engine itself.
But what if you don’t have enough standard library resources?
Let's take a look at what a unique user interface element looks like on the example of this demo application (viewing requires a browser with WebGL support, for example, the latest version of Firefox).
The color scroll bars in the demo are associated with floating variables that have a value from 0 to 1. You can use them in the Unity Inspector to display the state of individual parts of a game object, for example, a spacecraft (suppose the value 1 means “no damage” and 0 critical damage "). The color of the bars changes depending on the value, so that the user can quickly understand the situation. These interface elements are easily created using IMGUI.
First you need to decide how the function signature will look. Our element will need 3 components to fully cover the various types of events:
• Rect, which determines the coordinates of drawing the element and reading the mouse clicks;
• float, a floating variable that the color bar represents;
• GUIStyle, containing the necessary information about indents, fonts, textures, etc. In our case, this will be the texture used when drawing the strip. Next we look at this parameter in more detail.
The function will have to return the new value of the floating number set after moving the slider. This makes sense for mouse-related events, but not for element redraw events, so by default the function will return the value passed in the call. In this case, calls of the form “value = MyCustomSlider (... value ...)” are independent of the event, and the value of the variable remains unchanged.
As a result, the function signature takes the following form:
public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)
We proceed to the implementation of the function. First of all, we need to get the control ID that will be used when reacting to mouse events. At the same time, even if the current event does not interest us, it still needs to request an identifier so that it is not assigned to another element in the context of this event, since the IMGUI assigns identifiers in the order in which requests are received. To avoid problems with the use of different identifiers for the same element in the context of different events, it is necessary to request them for all types of events (or not to ask for one if the element is not interactive, but this is not our case).
{ int controlID = GUIUtility.GetControlID (FocusType.Passive);
The FocusType.Passive parameter defines the role of the element in keyboard navigation. Passive means that our bar does not respond to keyboard input. Otherwise, Native or Keyboard is used. More information about the
FocusType parameter can be found in the corresponding documentation.
Now we use the branch statement to separate the code needed for the different types of events. Instead of using Event.current.type directly, we will use
Event.current.GetTypeForControl () to give it a control ID. In this way, we filter the event types so that, for example, a keyboard event does not refer to the wrong element. However, this filtering is not universal, so later we will have to add additional checks.
switch (Event.current.GetTypeForControl(controlID)) {
So, you can start implementing behaviors for different types of events. Let's start with drawing:
case EventType.Repaint: {
At this point one could stop and get a ready-made item for visualization of floating values ​​from 0 to 1 in read-only mode. But let's continue and make it interactive.
To make the use of the element convenient, after the slider is captured by pressing the button, you should consider only the horizontal movement of the mouse, regardless of whether the cursor remains within the bar. In this case, you need to do the following thing: until the user releases the mouse button, its movement should not affect other elements of the interface.
To do this, we will use the
GUIUtility.hotControl variable containing the control ID that is currently interacting with the mouse. IMGUI uses it in the GetTypeForControl () function. If it is not zero, mouse events are filtered out (assuming that the control ID passed does not match the value of hotControl).
Installing and resetting hotControl is very simple:
case EventType.MouseDown: {
Note: if any other element is hotControl (for example, GUIUtility.hotControl is non-zero and contains a different identifier), GetTypeForControl () will not return mouseUp / mouseDown, but simply ignore these events.
Now you need to create code to change the floating variable while the mouse button is held down. The easiest way is to close the branch and indicate that any event associated with the mouse and occurring while the element identifier is in hotControl (i.e. while the drag is in progress and the mouse button has not yet been released) should change the value of the variable:
if (Event.current.isMouse && GUIUtility.hotControl == controlID) {
The last two steps — setting the
GUI.changed and calling the
Event.current.Use () —are especially important for correct interaction within the set of IMGUI elements. Setting the value to TRUE for GUI.changed allows you to use the
EditorGUI.BeginChangeCheck () and
EditorGUI.EndChangeCheck () functions to test the change in the value of a variable by user actions. FALSE is better not to use, so as not to miss previous changes.
Finally, our function should return the new value of the floating variable. Most often it will be different from the previous value:
return value; }
MyCustomSlider is ready. We have a simple functional element IMGUI, which can be used in custom editors, PropertyDrawers, EditorWindows, etc. But this is not all. Next we will talk about how you can extend its functionality, for example, add the ability to multi-edit.