After a complete break over the Christmas holiday period, with ~460km between me and my development machine, work is now back in full swing. While the game project is progressing well, my immediate focus is now on releasing the standalone sky rendering Unity package: DeepSky.
As this is intended for pushing out into the world at large, the requirements for usability and robustness are much higher than if it was simply part of the game. This week has therefore been about boring but essential project planning and creating the framework code that allows DeepSky to work nicely in Unity’s editor. So this post is a grab-bag of useful scripting bits and bobs discovered this week.
ScriptableObjects And Custom UI
Often you just need to store a block of data, without any associated logic or having to add it as a game object component. Unity provides a handy base class to derive from that allows you store data as an asset – the ScriptableObject
. This also allows you create data assets that play nicely with the serialization pipeline and make loading/saving data easy.
DeepSky uses a ScriptableObject
to hold all the data about what state the sky is in: all the cloud modelling, lighting, time and weather parameters. By chucking all this in a separate object to the actual DeepSky component, it’s trivial to save/load sky presets and hot-swap skies on the fly.
While this is great, it does present a very minor issue. Typically, the Inspector window will list all the serializable fields of a component (fields that are either public
or tagged with the [SerializeField]
attribute). This still works fine for a field referencing a ScriptableObject
, but the UI for it isn’t particularly nice – you have to expand the field and then the UI is just a little ‘meh’. Normally you’d create a custom editor for a component to draw the Inspector however you’d like, but for a custom type like a ScriptableObject
you can go one better. Enter the PropertyDrawer
class.
In the same way you can use the [CustomEditor(type)]
attribute on an Editor-derived type to override the Inspector UI (and other things) for a component, you can use a custom PropertyDrawer
to override the UI display for a custom field type. This means even if you don’t have a custom editor for a component, any fields of your custom type will still automatically display nicely in the Inspector. Neat.
One thing that took a little while to figure out however – if you just go ahead and draw some custom UI it’s assumed to fit within the confines of a normal field (ie. one row in the Inspector). If you’re customizing the look of a reference to a ScriptableObject
containing multiple fields, it’s necessary to override the GetPropertyHeight
function to make sure Unity knows how much space the UI is actually going to take up. Otherwise Unity doesn’t know your property is actually bigger than normal and will draw all over it.
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return base.GetPropertyHeight(property, label) + EditorGUIUtility.singleLineHeight * 22; //<--- the size of a normal field, plus however much extra you need. }
One slightly annoying issue is you can’t use the ‘Layout’ version of the EditorGUI
class in a PropertyDrawer
to automatically handle the sizing of UI components. This means you have to calculate the Rect
required by each control manually. I do this by simply incrementing an offset each control, to avoid having to manually adjust lots of numbers by hand. Note the use of EditorGUIUtility.singleLineHeight
to get the standard vertical size of a control.
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); int offset = 0; float lineHeight = EditorGUIUtility.singleLineHeight; // Calculate all the Rects upfront. Rect lightingRect = new Rect(position.x, position.y + lineHeight * offset++, position.width, lineHeight); Rect extinctionRect = new Rect(position.x, position.y + lineHeight * offset++, position.width, lineHeight); Rect inScatteringRect = new Rect(position.x, position.y + lineHeight * offset++, position.width, lineHeight); // --- You get the idea... --- // Draw the GUI. EditorGUI.LabelField(lightingRect, "Lighting:", EditorStyles.boldLabel); EditorGUI.PropertyField(extinctionRect, property.FindPropertyRelative("m_Extinction")); EditorGUI.PropertyField(inScatteringRect, property.FindPropertyRelative("m_InScattering")); // --- You get the idea, again... --- EditorGUI.EndProperty(); }
Also note the use of property.FindPropertyRelative
– this is the UI for the ‘parent’ field, ie. the reference to the ScriptableObject
, so to display the actual fields from the ScriptableObject
we have to find them by name.
Now any time a component has a field such as:
public DS_Context myDeepSkyContext;
it will automatically be drawn in the Inspector, custom component editor or not, like so:
Note how the other fields continue drawing as normal before and after our ScriptableObject
. Horay.
Using Unity’s Object Picker Pop-up Properly
For DeepSky, I need the functionality to save and load presets and ideally I wanted to use the standard object picker for consistency and to, you know, avoid work. When you click the little circle next to a field in the Inspector of one of Unity’s object types (eg. GameObject
, Texture2D
etc.), Unity pop’s up a little object picker to allow selection from objects in either the scene or the entire project. It’s a nice little dialog that is filtered to only show valid types for that field – wouldn’t it be great if you could use it for your own custom types? Turns out, you can. You can show it pretty easily with something like (for my custom ScriptableObject
type):
if (GUILayout.Button("Load Preset")) { int ctrlID = EditorGUIUtility.GetControlID(FocusType.Passive); EditorGUIUtility.ShowObjectPicker<DS_ScriptableContext>(null, false, "", ctrlID); }
Note we need to get a new control ID to pass in. This allows you to bring up more than one at a time and distinguish between them. So far, so not too difficult. But how do you actually get the result? The docs are, unfortunately, not too clear on this as there’s no example. You can get the object using EditorGUIUtility.GetObjectPickerObject()
and the picker window uses events to inform the calling OnGUI
function when it’s OK to do so. To do something useful with the picker window, we need to listen out for those events during our OnInspectorGUI
function (or the OnGUI
you called ShowObjectPicker
from). You can update as you go by listening for ObjectSelectorUpdated,
I just need the result so am only listening for ObjectSelectorClosed
like so:
// Check for messages returned by the object picker. if (Event.current.commandName == "ObjectSelectorClosed") { DS_ScriptableContext cxtSO = EditorGUIUtility.GetObjectPickerObject() as DS_ScriptableContext; if (cxtSO != null) { LoadFromContextPreset(cxtSO.Context); } }
So that’s my custom type displaying nicely in the editor, just as if Unity natively knew what to do with it. Epic.