Search Unity

Going deep with IMGUI and Editor customization

December 22, 2015 in Engine & platform | 32 min. read
GUI diagram 1
GUI diagram 1
Share

Is this article helpful for you?

Thank you for your feedback!

The new Unity UI system has now been out for over a year. So I thought I'd do a blog post about the old UI system, IMGUI.

Strange timing, you might think. Why care about the old UI system now that the new one is available? Well, while the new UI system is intended to cover every in-game user interface situation you might want to throw at it, IMGUI is still used, particularly in one very important situation: the Unity Editor itself. If you're interested in extending the Unity Editor with custom tools and features, it's very likely that one of the things you'll need to do is go toe-to-toe with IMGUI.

Proceeding Immediately

First question, then: Why is it called ‘IMGUI’? IMGUI is short for Immediate Mode GUI. OK, so, what’s that? Well, there’s two major approaches to GUI systems: ‘immediate’ and ‘retained.’

A retained mode GUI is one in which the GUI system ‘retains’ information about your GUI: you set up your various GUI widgets - labels, buttons, sliders, text fields, etc - and then that information is kept around and used by the system to render the screen, respond to events, and so on. When you want to change the text on a label, or move a button, then you’re manipulating some information which is stored somewhere, and when you’ve made your change then the system carries on working in its new state. As the user changes values and moves sliders, the system simply stores their changes, and it’s up to you to query the values or respond to callbacks. The new Unity UI system is an example of a retained mode GUI; you create your UI.Labels, UI.Buttons and so on as components, set up them up, and then just let them sit there, and the new UI system will take care of the rest.

Meanwhile, an immediate mode GUI is one in which the GUI system generally does not retain information about your GUI, but instead, repeatedly asks you to re-specify what your controls are, and where they are, and so on. As you specify each part of the UI in the form of function calls, it is processed immediately - drawn, clicked, etc - and the consequences of any user interaction returned to you straight away, instead of you needing to query for it. This is inefficient for a game UI - and inconvenient for artists to work with, as everything becomes very code-dependent - but it turns out to be very handy for non-realtime situations (like Editor panels) which are heavily code-driven (like Editor panels) and want to change the displayed controls easily in response to current state (like Editor panels!) so it’s a good choice for things like heavy construction equipment. No, wait. I meant, it’s a good choice for Editor panels.

If you want to know more, Casey Muratori has a great video where he discusses some of the upsides and principles of an Immediate Mode GUI. Or you can just keep reading!

Every Event-uality

Whenever IMGUI code is running, there is a current ‘Event’ being handled - this could be something like ‘user has clicked the mouse button,’ or something like ‘the GUI needs to be repainted.’ You can find out what the current event is by checking Event.current.type.

Imagine what it might look like if you’re doing a set of buttons in a window somewhere and you had to write separate code to respond to 'user has clicked the mouse button' and 'the GUI needs to be repainted.' At a block level it might look like this:

GUI diagram 1

Writing these functions for each separate GUI event is kinda tedious; but you’ll notice that there’s a certain structural similarity between the functions. Each step of the way, we are doing something relating to the same control (button 1, button 2, or button 3). Exactly what we’re doing depends on the event, but the structure is the same. What this means is that we can do this instead:

GUI diagram 2

We have a single OnGUI function which calls library functions like GUI.Button, and those library functions do different things depending on which event we’re handling. Simple!

There are 5 event types that are used most of the time:

EventType.MouseDownSet when the user has just pressed a mouse button.
EventType.MouseUpSet when the user has just released a mouse button.
EventType.KeyDownSet when the user has just pressed a key.
EventType.KeyUpSet when the user has just released a key.
EventType.RepaintSet when IMGUI needs to redraw the screen.

That’s not an exhaustive list - check the EventType documentation for more.

How might a standard control, such as GUI.Button, respond to some of these events?

EventType.RepaintDraw the button in the provided rectangle.
EventType.MouseDownCheck whether the mouse is within the button’s rectangle. If so, flag the button as being down and trigger a repaint so that it gets redrawn as pressed in.
EventType.MouseUpUnflag the button as down and trigger a repaint, then check whether the mouse is still within the button’s rectangle: if so, return true, so that the caller can respond to the button being clicked.

The reality is more complicated than this - a button also responds to keyboard events, and there is code to ensure that only the button that you initially clicked on can respond to the MouseUp - but this gives you a general idea. As long as you call GUI.Button at the same point in your code for each of these events, with the same position and contents, then the different behaviours will work together to provide all the functionality of a button.

To help with tying these different behaviours together under different events, IMGUI has the concept of a ‘control ID.’ The idea of a control ID is to give a consistent way to refer to a given control across every event type. Each distinct part of the UI that has non-trivial interactive behaviour will request a control ID; it’s used to keep track of things like which control currently has keyboard focus, or to store a small amount of information associated with a control. The control IDs are simply awarded to controls in the order that they ask for them, so, again, as long as you’re calling the same GUI functions in the same order under different events, they’ll end up being awarded the same control IDs and the different events will sync up.

Custom Control Conundrum

If you want to create your own custom Editor classes, your own EditorWindow classes, or your own PropertyDrawer classes, the GUI class - as well as the EditorGUI class - provides a library of useful standard controls that you’ll see used throughout Unity.

(It’s a common mistake for newbie Editor coders to overlook the GUI class - but the controls in that class can be used when extending the Editor just as freely as the controls in EditorGUI. There’s nothing particularly special about GUI vs EditorGUI - they’re just two libraries of controls for you to use - but the difference is that the controls in EditorGUI cannot be used in game builds, because the code for them is part of the Editor, while GUI is a part of the engine itself).

But what if you want to do something that goes beyond what’s available in the standard library?

Let’s explore how we might create a custom user interface control. Try clicking and dragging the coloured boxes in this little demo:

(You’ll need a browser with WebGL support to see the demo, like current versions of Firefox).

These custom sliders each drive a separate ‘float’ value between 0 and 1. You might want to use such a thing in the Inspector as another way of displaying, say, hull integrity for different parts of a spaceship object, where 1 represents ‘no damage’ and 0 represents ‘totally destroyed’ - having the bars represent the values as colours may make it easier to tell, at a glance, what state the ship is in. The code for building this as a custom IMGUI control that you can use like any other control is pretty easy, so let’s walk through it.

The first step is to decide upon our function signature. In order to cover all the different event types, our control is going to need three things:

  • a Rect which defines where it should draw itself and where it should respond to mouse clicks.
  • the current float value that the bar is representing.
  • a GUIStyle, which contains any necessary information about spacing, fonts, textures, and so on that the control will need. In our case that includes the texture that we’ll use to draw the bar. More on this parameter later.

It’s also going to need to return the value that the user has set by dragging the bar. That’s only meaningful on certain events like mouse events, and not on things like repaint events; so by default we’ll return the value that the calling code passed in. The idea is that the calling code can just do “value = MyCustomSlider(... value ...)” without caring about the event that is happening, so if we’re not returning some new value set by the user, we need to preserve the value that currently stands.

So the resulting signature looks like this:

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

Now we begin implementing the function. The first step is to retrieve a control ID. We’ll use this for certain things when responding to the mouse events. However, even if the event being handled isn’t one we actually care about, we must still request an ID anyway, to ensure that it isn’t allocated to some other control for this particular event. Remember that IMGUI just dishes out IDs in the order they’re requested, so if you don’t ask for an ID it’ll end up being given to the next control instead, causing that control to end up with different IDs for different events, which is likely to break it. So, when requesting IDs, it’s all-or-none - either you request an ID for every event type, or you never request it for any of them (which might be OK, if you're creating a control that is extremely simple or non-interactive).

{
	int controlID = GUIUtility.GetControlID (FocusType.Passive);

The FocusType.Passive being passed as a parameter there tells IMGUI what role this control plays in keyboard navigation - whether it’s possible for the control to be the current one reacting to keypresses. My custom slider doesn’t respond to key presses at all, so it specifies Passive, but controls that respond to key presses could specify Native or Keyboard. Check the FocusType docs for more info on them.

Next, we do what the majority of custom controls will do at some point in their implementation: we branch depending on the event type, using a switch statement. Instead of just using Event.current.type directly, we’ll use Event.current.GetTypeForControl(), passing it our control ID; this filters the event type, to ensure that, for example, keyboard events are not sent to the wrong control in certain situations. It doesn’t filter everything, though, so we will still need to perform some checks of our own as well.

	switch (Event.current.GetTypeForControl(controlID))
	{

Now we can begin implementing the specific behaviours for the different event types. Let’s start with drawing the control:

		case EventType.Repaint:
		{
			// Work out the width of the bar in pixels by lerping
			int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);

			// Build up the rectangle that the bar will cover
			// by copying the whole control rect, and then setting the width
			Rect targetRect = new Rect (controlRect){ width = pixelWidth };

			// Tint whatever we draw to be red/green depending on value
			GUI.color = Color.Lerp (Color.red, Color.green, value);

			// Draw the texture from the GUIStyle, applying the tint
			GUI.DrawTexture (targetRect, style.normal.background);

			// Reset the tint back to white, i.e. untinted
			GUI.color = Color.white;

			break;
		}

At this point you could finish up the function and you’d have a functioning ‘read-only’ control for visualising float values between 0 and 1. But let’s continue and make the control interactive.

To implement a pleasant mouse behaviour for the control, we have a requirement: once you’ve clicked on the control and started to drag it, you shouldn’t need to keep the mouse over the control. It’s much nicer for the user to be able to just focus on where their cursor is horizontally, and not worry about vertical movement. This means that they might move the mouse over other controls while dragging, and we need those controls to ignore the mouse until the user releases the button again.

The solution to this is to make use of GUIUtility.hotControl. It’s just a simple variable which is intended to hold the control ID of the control which has captured the mouse. IMGUI uses this value in GetTypeForControl(); when it’s not 0, then mouse events get filtered out unless the control ID being passed in is the hotControl.

So, setting and resetting hotControl is pretty simple:

		case EventType.MouseDown:
		{
			// If the click is actually on us...
			if (controlRect.Contains (Event.current.mousePosition)
			// ...and the click is with the left mouse button (button 0)...
			 && Event.current.button == 0)
				// ...then capture the mouse by setting the hotControl.
				GUIUtility.hotControl = controlID;

			break;
		}

		case EventType.MouseUp:
		{
			// If we were the hotControl, we aren't any more.
			if (GUIUtility.hotControl == controlID)
				GUIUtility.hotControl = 0;

			break;
		}

Note that when some other control is the hot control - i.e. GUIUtility.hotControl is something other than 0 and our own control ID - then these cases simply won’t be executed, because GetTypeForControl() will be returning ‘ignore’ instead of mouseUp/mouseDown events.

Setting the hotControl is fine, but we still haven’t actually done anything to change the value while the mouse is down. The simplest way to do that is actually to close the switch and then say that any mouse event (clicking, dragging, or releasing) that happens while we’re the hotControl (and therefore are in the middle of click+dragging - though not releasing, because we zeroed out the hotControl in that case above) should result in the value changing:

	if (Event.current.isMouse && GUIUtility.hotControl == controlID) {

		// Get mouse X position relative to left edge of the control
		float relativeX = Event.current.mousePosition.x - controlRect.x;

		// Divide by control width to get a value between 0 and 1
		value = Mathf.Clamp01 (relativeX / controlRect.width);

		// Report that the data in the GUI has changed
		GUI.changed = true;

		// Mark event as 'used' so other controls don't respond to it, and to
		// trigger an automatic repaint.
		Event.current.Use ();
	}

Those last two steps - setting GUI.changed and calling Event.current.Use() - are particularly important, not just to making this control behave correctly, but also to make it play nice with other IMGUI controls and features. In particular, setting GUI.changed to true will allow calling code to use the EditorGUI.BeginChangeCheck() and EditorGUI.EndChangeCheck() functions to detect whether the user actually changed your control’s value or not; but you should also avoid ever setting GUI.changed to false, because that might end up hiding the fact that a previous control had its value changed.

Lastly, we need to return a value from the function. You’ll remember that we said we would return the modified float value - or the original value, if nothing has changed, which most of the time will be the case:

	return value;
}

And we’re done. MyCustomSlider is now a simple functioning IMGUI control, ready to be used in custom Editors, PropertyDrawers, editor windows, and so on. There’s still more we can do to beef it up - like support multi-editing - but we’ll discuss that below.

More than you can Handle

There’s one other particularly important non-obvious thing about IMGUI, and that is its relation to the Scene View. You’ll all be familiar with the helper UI elements that are drawn in the scene view when you go to translate, rotate, and scale objects - the orthogonal arrows, rings, and box-capped lines that you can click and drag to manipulate objects. These UI elements are called ‘Handles.’

What’s not obvious is that Handles are powered by IMGUI as well!

After all, there’s nothing inherent in what we’ve said about IMGUI so far that is specific to 2D or Editors/EditorWindows. The standard controls you find in the GUI and EditorGUI classes are all 2D, certainly, but the basic concepts like EventType and control IDs don’t depend on 2D at all. So while GUI and EditorGUI provide 2D controls aimed at EditorWindows and Editors for components in the Inspector, the Handles class provides 3D controls intended for use in the Scene View. Just as EditorGUI.IntField will draw a control that lets the user edit a single integer, we have functions like:

Vector3 PositionHandle(Vector3 position, Quaternion rotation);

that will allow the user to edit a Vector3 value, visually, by providing a set of interactive arrows in the Scene View. And just as before, you can define your own Handle functions to draw custom user interface elements as well; dealing with mouse interaction is a little more complex, as it’s no longer enough to just check whether the mouse is inside a rectangle or not - the HandleUtility class may be of help to you there - but the basic structure and concepts are all the same.

If you provide an OnSceneGUI function in your custom editor class, you can use Handle functions there to draw into the scene view, and they’ll be positioned correctly in world space as you’d expect. Though bear in mind that it is possible to use Handles in 2D contexts like custom editors, or to use GUI functions in the scene view - you just may need to do things like setting up GL matrices or calling Handles.BeginGUI() and Handles.EndGUI() to set up the context before you use them.

State of the GUInion

In the case of MyCustomSlider, there were only really two pieces of information we needed to keep track of: the current value of the slider (which was passed in by the user and returned to them) and whether the user was in the process of changing the value (which we effectively used hotControl to keep track of). But what if a control needs to keep hold of more information than that?

IMGUI provides a simple storage system for ‘state objects’ that are associated with a control. You define your own class for storing values, and then ask IMGUI to manage an instance of it, associated with your control’s ID. You’re only allowed one state object per control ID, and you don’t instantiate it yourself - IMGUI does that for you, using the state object’s default constructor. State objects also aren’t serialised when reloading editor code - something that happens every time your code is recompiled - so you should only be using them for short-lived stuff. (Note that this is true even if you mark your state objects as [Serializable] - the serializer simply doesn’t visit this particular corner of the heap).

Here’s an example. Suppose we want a button which returns true whenever it’s pressed down, but also flashes red if you’ve been holding it down for longer than two seconds. We’ll need to keep track of the time at which the button was originally pressed; we’ll do this by storing it in a state object. So, here’s our state object 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;
      }
}

We’ll store the time at which the mouse was pressed in ‘mouseDownAt’ when MouseDownNow() is called, and then use the IsFlashing function to tell us ‘should the button be colored red right now’ - as you can see, it will definitely not be red if it’s not the hotControl or if fewer than 2 seconds have passed since it was clicked, but after that we make it change color every 0.1 seconds.

Here’s the code for the actual button control itself:

public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style)
{
        int controlID = GUIUtility.GetControlID (FocusType.Native);

        // Get (or create) the state object
        var state = (FlashingButtonInfo)GUIUtility.GetStateObject(
                                             typeof(FlashingButtonInfo),
                                             controlID);

        switch (Event.current.GetTypeForControl(controlID)) {
                case EventType.Repaint:
                {
                        GUI.color = state.IsFlashing (controlID)
                            ? Color.red
                            : Color.white;
                        style.Draw (rc, content, controlID);
                        break;
                }
                case EventType.MouseDown:
                {
                        if (rc.Contains (Event.current.mousePosition)
                         && Event.current.button == 0
                         && GUIUtility.hotControl == 0)
                        {
                                GUIUtility.hotControl = controlID;
                                state.MouseDownNow();
                        }
                        break;
                }
                case EventType.MouseUp:
                {
                        if (GUIUtility.hotControl == controlID)
                                GUIUtility.hotControl = 0;
                        break;
                }
        }

        return GUIUtility.hotControl == controlID;
}

Pretty straightforward - you should recognise the code in the mouseDown/mouseUp cases as being very similar to what we did for capturing the mouse in the custom slider, above. The only differences are the call to state.MouseDownNow() when pressing down the mouse, and changing GUI.color in the repaint event.

The eagle-eyed amongst you might have noticed that there’s one other key difference about the repaint event - that call to style.Draw(). What’s up with that?

Doing GUI with Style

When we were building the custom slider control, we used GUI.DrawTexture to draw the bar itself. That worked OK, but our FlashingButton needs to have a caption on it, in addition to the ‘rounded rectangle’ image that is the button itself. We could try and arrange something with GUI.DrawTexture to draw the button image and then GUI.Label on top of that to draw the caption… but we can do better. We can use the same technique that GUI.Label uses to draw itself, and cut out the middleman.

A GUIStyle contains information about the visual properties of a GUI element - both basic things like the font or text color it should use, and more subtle layout properties like how much spacing to give it. All of this information is stored in a GUIStyle alongside functions to work out the width and height of some content using the style, and the functions to actually draw the content to the screen.

In fact, GUIStyle doesn’t just take care of one style for a control: it can take care of rendering it in a bunch of situations that a GUI element might find itself in - drawing it differently when it’s being hovered over, when it has keyboard focus, when it’s disabled, and when it’s “active” (for example, when a button is in the middle of being pressed). You can provide the color and background image information for all of these situations, and the GUIStyle will pick the appropriate one at drawing-time based on the control ID.

There’s four main ways to get hold of GUIStyles that you can use to draw your controls:

  • Construct one in code (new GUIStyle()) and set up the values on it.
  • Use one of the built-in styles from the EditorStyles class. If you want your custom controls to look like the built-in ones - drawing your own toolbars, Inspector-style controls, etc - then this is the place to look.
  • If you just want to create a small variation on an existing style - say, a regular button but with right-aligned text - then you can clone the styles in the EditorStyles class (new GUIStyle(existingStyle)) and then just change the properties you want to change.
  • Retrieve them from a GUISkin.

A GUISkin is essentially a big bundle of GUIStyle objects; importantly, it can be created as an asset in your project and edited freely through the Inspector. If you create one and take a look, you’ll see slots for all the standard control types - boxes, buttons, labels, toggles, and so on - but as a custom control author, direct your attention to the ‘custom styles’ section near the bottom. Here you can set up any number of custom GUIStyle entries, giving each one a unique name, and then later you can retrieve them using GUISkin.GetStyle(“nameOfCustomStyle”). The only missing piece of the puzzle is figuring out how to get hold of your GUISkin object from code in the first place; if you keep your skin in the ‘Editor Default Resources’ folder, you can use EditorGUIUtility.LoadRequired(); alternatively, you could use a method like AssetDatabase.LoadAssetAtPath() to load from elsewhere in the project. (Just don’t put your editor-only assets somewhere that packs them into asset bundles or the Resources folder by mistake!)

Armed with a GUIStyle, you can then draw a GUIContent - a mix of text, icon, and tooltip - using GUIStyle.Draw(), passing it the rectangle you’re drawing into, the GUIContent you want to draw, and the control ID that should be used to figure out whether the content has things like keyboard focus.

Laying Out the Positions

You’ll have noticed that all of the GUI controls we’ve discussed and written so far include a Rect parameter that determines the control’s position on screen. And, now that we’ve discussed GUIStyle, you might have paused when I said that a GUIStyle includes “layout properties like how much spacing it needs.” You might be thinking: “uh oh. Does this mean we have to do a bunch of work to calculate our Rect values so that the spacing values are respected?”

That’s certainly an approach which is available to us; but there’s an easier way. IMGUI includes a ‘layouting’ mechanism which can automatically calculate appropriate Rect values for our controls, taking things like spacing into account. So how does it work?

The trick is an extra EventType value for controls to respond to: EventType.Layout. IMGUI sends the event to your GUI code, and the controls you invoke respond by calling IMGUI layout functions - GUILayoutUtility.GetRect(), GUILayout.BeginHorizonal / Vertical, and GUILayout.EndHorizontal / Vertical, amongst others - which IMGUI records, effectively building up a tree of the controls in your layout and the space they require. Once it’s finished and the tree is fully built, IMGUI then does a recursive pass over the tree, calculating the actual widths and heights of elements and where they are in relation to one another, positioning successive controls next to one another and so on.

Then, when it’s time to do an EventType.Repaint event - or indeed any other kind of event - controls call the same IMGUI layout functions. Only this time, instead of recording the calls, IMGUI ‘plays back’ the calls it previously recorded on the Layout event, returning the rectangles it computed; having called GUILayoutUtility.GetRect() during the layout event to register that you need a rectangle, you call it again during the repaint event and it actually returns the rectangle you should use.

Like with control IDs, this means you need to be consistent about the layout calls you make between Layout events and other events - otherwise you’ll end up retrieving computed rectangles for the wrong controls. It also means that the values returned by GUILayoutUtility.GetRect() during a Layout event are useless, because IMGUI won’t actually know the rectangle it’s supposed to give you until the event has completed and the layout tree has been processed.

What does this look like for our custom slider control? We can actually write a Layout-enabled version of our control really easily, as once we’ve got a rectangle back from IMGUI we can just call the code we already wrote:

public static float MyCustomSlider(float value, GUIStyle style)
{
	Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
	return MyCustomSlider(position, value, style);
}

The call to GUILayoutUtility.GetRect will do two things: during a Layout event, it will record that we want to use the given style to draw some empty content - empty because there is no specific text or image that we need to make room for - and during other events, it will retrieve an actual rectangle for us to use. This does mean that during a layout event we’re calling MyCustomSlider with a bogus rectangle, but it doesn’t matter - we still need to do it, in order to make sure that the usual calls are made to GetControlID(), and the rectangle isn't actually used for anything in there during a Layout event.

You might be wondering how IMGUI can actually work out the size of the slider, given ‘empty’ content and just a style. It’s not a lot of information to go on - we’re relying on the style having all the necessary information specified, that IMGUI can use to work out the rectangle to assign. But what if we wanted to let the user control that - or, say, to use a fixed height from the style but let the user control the width. How would we do that?

The answer is in the GUILayoutOption class. Instances of this class represent directives to the layout system that a particular rectangle should be calculated in a particular way; for example, “should have height 30” or “should expand horizontally to fill the space” or “must be at least 20 pixels wide.” We create them by calling factory functions in the GUILayout class - GUILayout.ExpandWidth(), GUILayout.MinHeight(), and so on - and pass them to GUILayoutUtility.GetRect() as an array. They’re stored into the layout tree and taken into account when the tree is processed at the end of the layout event.

To make it easy for the user to provide as few or as many GUILayoutOption instances as they like without having to create and manage their own arrays, we take advantage of the C# ‘params’ keyword, which lets you call a method passing any number of parameters, and have those parameters arrive within the method packed into an array automatically. Here’s our modified slider now:

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, we just take whatever the user’s given us and pass it onwards to GetRect.

The approach we’ve used here - of wrapping a manually-positioned IMGUI control function in an auto-layouting version - works for pretty much any IMGUI control, including the built-in ones in the GUI class. In fact, the GUILayout class uses exactly this approach to provide auto-layouted versions of the controls in the GUI class (and we offer a corresponding EditorGUILayout class to wrap controls in the EditorGUI class). You might want to follow this twin-class convention when building your own IMGUI controls.

It’s also completely viable to mix auto-layouted and manually positioned controls. You can call GetRect to reserve a chunk of space, and then do you own calculations to divide that rectangle up into sub-rectangles that you then use to draw multiple controls; the layout system doesn’t use control IDs in any way, so there’s no problem with having multiple controls per layout rectangle ( or even multiple layout rectangles per control). This can sometimes be much faster than using the layout system fully.

Also, note that if you’re writing PropertyDrawers, you should not use the layout system; instead, you should just use the rectangle passed to your PropertyDrawer.OnGUI() override. The reason for this is that under the hood, the Editor class itself does not actually use the layout system, for performance reasons; it just calculates a simple rectangle itself, moving it down for each successive property. So, if you did use the layout system in your PropertyDrawer, it wouldn’t have any knowledge of any of the properties that had been drawn before yours, and would end up positioning you on top of them. Which is not what you want!

Leeloo Dallas Multi-Property

So far, everything we’ve discussed would equip you to build your own IMGUI control that would work pretty smoothly. There’s just a couple more things to discuss for when you really want to polish what you’ve built to the same level as the Unity built-in controls.

The first is the use of SerializedProperty. I don’t want to go into the SerializedProperty system in too much detail in this post - we’ll leave that for another time - but just to summarize quickly: A SerializedProperty ‘wraps’ a single variable handled by Unity’s serialization (load and save) system. Every variable on every script you write that shows up in the Inspector - as well as every variable on every engine object that you see in the Inspector - can be accessed via the SerializedProperty API, at least in the Editor.

SerializedProperty is useful because it doesn’t just give you access to the variable’s value, but also information like whether the variable’s value is different to the value on a prefab it came from, or whether a variable with child fields (e.g. a struct) is expanded or collapsed in the Inspector. It also integrates any changes you make to the value into the Undo and scene-dirtying systems. It lets you do this without ever actually creating the managed version of your object, too, which can help performance greatly. So, if we want our IMGUI controls to play nice and easy with a slew of editor functionality - undo, scene dirtying, prefab overrides, etc - we should make sure we support SerializedProperty.

If you look through the EditorGUI methods that take a SerializedProperty as an argument, you’ll see the signature is slightly different. Instead of the ‘take a float, return a float’ approach of our previous custom slider, SerializedProperty-enabled IMGUI controls just take a SerializedProperty instance as an argument, and don’t return anything. That’s because any changes they need to make to the value, they just apply directly to the SerializedProperty themselves. So our custom slider from before can now look like this:

public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)

The ‘value’ parameter we used to have is gone, along with the return value, and instead, the ‘prop’ parameter is there to pass in the SerializedProperty. To retrieve the current value of the property in order to draw the slider bar, we just access prop.floatValue, and when the user changes the slider position we just assign to prop.floatValue.

Having the whole SerializedProperty present in the IMGUI control code has other benefits, though. For example, consider the way that modified properties in prefab instances are shown in bold. Just check the prefabOverride property on the SerializedProperty, and if it’s true, do whatever you need to do to display the control differently. Happily, if making text bold really is all you want to do, then IMGUI will take care of that for you automatically as long as you don’t specify a font in your GUIStyle when you draw. (If you do specify a font in your GUIStyle, then you’re going to need to take care of this yourself - having regular and bold versions of your font and selecting between them based on prefabOverride when you want to draw).

The other major feature you need is support for multi-object editing - i.e. handling things gracefully when your control needs to display multiple values simultaneously. Test for this by checking the value of EditorGUI.showMixedValue; if it’s true, your control is being used to depict multiple different values simultaneously, so do whatever you need to do to indicate that.

Both the bold-on-prefabOverride and showMixedValue mechanisms require that context for the property has been set up using EditorGUI.BeginProperty() and EditorGUI.EndProperty(). The recommended pattern is to say that if your control method takes a SerializedProperty as an argument, then it will make the calls to BeginProperty and EndProperty itself, while if it deals with ‘raw’ values - similar to, say, EditorGUI.IntField, which takes and returns ints directly and doesn’t work with properties - then the calling code is responsible for calling BeginProperty and EndProperty. (It makes sense, really, because if your control is dealing with 'raw' values then it doesn't have a SerializedProperty value it can pass to BeginProperty anyway).

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);

    }
}

// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label)
{
    label = EditorGUI.BeginProperty (controlRect, label, prop);
    controlRect = EditorGUI.PrefixLabel (controlRect, label);

    // Use our previous definition of MyCustomSlider, which we’ve updated to do something
    // sensible if EditorGUI.showMixedValue is true
    EditorGUI.BeginChangeCheck();
    float newValue = MyCustomSlider(controlRect, prop.floatValue, style);
    if(EditorGUI.EndChangeCheck())
        prop.floatValue = newValue;

    EditorGUI.EndProperty ();
}

That’s all for now

I hope this post has shed some light on some of the core parts of IMGUI that you’ll need to understand if you want to really take your editor customisation to the next level. There’s more to cover before you can be an Editor guru - the SerializedObject / SerializedProperty system, the use of CustomEditor versus EditorWindow versus PropertyDrawer, the handling of Undo, etc - but IMGUI plays a large part in unlocking Unity’s immense potential for creating custom tools - both with a view to selling on the Asset Store, and with a view to empowering developers on your own teams.

Give me your questions and feedback in the comments!

December 22, 2015 in Engine & platform | 32 min. read

Is this article helpful for you?

Thank you for your feedback!