In this tutorial we will show you how to quickly get started building your own custom inspector using Odin. We'll do this by taking a simple game manager and converting it to a more useful and efficient game manager using the power of Odin.
Many games use the design pattern of “managers.” This can be a good way to organize and control what’s going on in your scene or the game as a whole.
A game manager can quickly become a complicated piece of code with many fields that need to be displayed and assigned in the inspector. The game manager may also contain functions that control SFX, music or even the spawning of enemies.
All of this makes a game manager a great example of how to get started with Odin inspector and make your first custom inspector.
If you’d like to follow along you can download the game manager script here.
Quick Disclaimer! The goal is not to show how to make a game manager, but rather how even a simple game manager can be made more useful, easier to maintain and easier to expand with Odin.
The Game Manager
The game manager that We’ve created here has some fields that could be used in a wide variety of games. Such as game state, turns remaining, UI objects for different game states, music, SFX, enemy data and enemy spawn points.
We’ve also added basic functions to play music or SFX, plus change the state of the game.
We're going to walk through step by step how to turn this game manager… into this!
In the process we are going to make a nicer looking inspector that is also easier to use! And we'll do it without a custom inspector script. It’s all going to happen with just a handful of attributes.
//UI elements
[TabGroup("UI"]
[InfoBox("All Canvas Objects are Required")]
[SceneObjectsOnly, Required]
[InlineButton("SelectObject", "Select)"]
public Canvas startButtons;
[TabBroup("Music")]
[Space]
[SowInInspector]
[ValueDropdown("musicList")]
[InlineButton("PlayMusic"), GUIColor(0.5f,0.7f,1f)]
[LabelText("Test Music Clip")]
private AudioClip currentMusicClip;
Basic Game Info
To start the make over we need to add in the Sirenix.OdinInspector namespace to access the Odin attributes.
using Sirenix.OdinInspector;
With that done, we’ll add a [BoxGroup] to visually group some fields together. Let's call the group “Game State Info”
We can now add this attribute to both the GameState and the Turns Remaining.
[BoxGroup("Game State Info")]
[EnumToggleButtons]
[OnValueChanged("StateChange")]
public GameState gameState;
[BoxGroup("Game State Info")]
public int turnsRemaining = 3;
With the grouping attributes it doesn’t matter if all the fields are together in the script, Odin will group them together in the inspector. However, with in that group, Odin will put them in the same order as they are in the script.
Already this looks better, but we can go a couple steps further particularly with the GameState enum.
Adding the attribute [EnumToggleButtons] turns the enum from a dropdown to a set of toggle buttons. This combined with an [OnValueChanged] attribute allows you the developer to not only see the value in the inspector, but also change the state to test the functionality of your code.
The [OnValueChanged] attribute, in this case, will call the function “State Change” if the enum value is changed in the inspector.
The function uses a switch statement to run different code based on the value of the enum.
Statics with [ShowInInspector]
Fields such as game state and turns remaining may be best utilized if they are static fields.
If your design requires or works best with static fields you can add the additional attribute of [ShowInInspector] to get access to the fields in the inspector if they are private or if they are static.
[BoxGroup("Game State Info")]
[EnumToggleButtons]
[OnValueChanged("StateChange")]
[ShowInInspector]
public GameState gameState;
[BoxGroup("Game State Info")]
[ShowInInspector]
public int turnsRemaining = 3;
Tabs
Tabs are really useful. They clean, tidy and organize the inspector very quickly.
Just like other group attributes, the [TabGroup] just requires a name. In this case the names will control which fields are shown together with in a tab.
We're going to add “UI”, “Music” as well as a “SFX” [TabGroup] attributes to the corresponding fields.
With these fields tidied up. Lets add a little more functionality.
All of the UI objects are going to be required AND they will need to be objects that are currently in the scene.
We can force these requirements by the addition of the two attributes [SceneObjectsOnly] and [Required]
These can be added on separate lines
[SceneObjectsOnly]
[Required]
or inline like so,
[SceneObjectsOnly, Required]
Adding attributes inline is not limited to these two attributes and can be done with any attributes. Doing so is a matter of personal preference and style.
Inline Button
Sometimes our scenes can get pretty crowded and it can be hard to find a given object. You can of course search the scene or with Odin you click on the “pen” icon to bring up inspector, but sometimes you want to find the actual object in the scene.
So we’ve added a simple “Select Canvas” function to the game manager that will select a given Canvas object in the hierarchy.
private void SelectObject(Canvas _object)
{
if(_object)
UnityEditor.Selection.activeObject = _object.gameObject;
}
[TabGroup("UI"),SceneObjectsOnly, Required]
[InlineButton("SelectCanvas", "Select")]
public Canvas pauseMenu;
With the [InlineButton] we can call this function for each of the required canvas objects in the game manager.
Next let's move on to the “Current Music Clip” field.
We're going to add a [Space] attribute to add a space between this field and the one above it for a little more visual clarity.
Since the field is private, we’ll add a [ShowInInspector] attribute so that it appears in the inspector.
We can then add a [ValueDropDown] attribute that will allow us to select the value of the “current music clip” from the list of music clips. All we have to do is add the name of the list that contains the music clips and Odin creates a drop down menu for us!
[TabGroup("Music)"]
[Space]
[ShowInInspector]
[ValueDropDown("musicList")]
[InlineButton("PlayMusic")]
[LabelText("Test Music Clip")]
private AudioClip currentMusicClip;
This is a huge time saver. No more digging through the scene or project files - you’re just selecting from a predefined list.
To continue adding functionality, we can add an [InlineButton] attribute that calls a PlayMusic function.
This allows you, the developer, to quickly play any clip in the music list and ensure that you have the right clips selected in your game manager. And it does it all without having to search through project files and click on a bunch of different assets.
It is important to note that if the function has an input type the field type with the inline button must match the type.
public void PlayMusic(AudioClip music)
{
if (musicSource != null && music != null)
{
musicSource.clip = music;
musicSource.Play();
}
}
To show off a little, you can also achieve a similar result by adding the [InlineEditor] attribute with the argument InlineEditorModes.SmallPreview
If you have Unity set to auto play selected clips, when you expand the inline editor the clip will play. When you collapse it the clip will stop playing.
To add similar functionality to the SFX clips, you can add an [InlineButton] that calls the function “PlaySFX” and give the button a label of “Test” by adding a second string argument to the attribute.
[TabGroup("SFX)]
[InlineButton("PlaySFX", "Test")]
public AudioClip weaponShoot;
[TabGroup("SFX")]
[InlineButton("PlaySFX", "Test")]
public AudioClip weaponHit;
[TabGroup("SFX)"]
[InlineButton("PlaySFX", "Test")]
public AudioClip enemySpawn;
This allows quick testing of each SFX in the inspector
The last remaining fields are the “enemyPrefab” which is a generic enemy object that will have data injected into it, “enemy data” is a list that contains scriptable objects for each type of enemy and “spawn points” which is a list of spawn points in the current scene.
The enemy data SO is covered in a different tutorial. you can see it here.
Now let's group these last three fields into tabs, in this case we want to form a second group of tabs.
This can be done, by adding a group name before the tab name within the [TabGroup] attribute, like so…
//enemies
[TabGroup("Enemies", "Enemy Data")]
[AssetsOnly]
public GameObject enemyPrefab;
[TabGroup("Enemies", "Enemy Data")]
[InlineEditor(InlineEditorModes.GUIOnly)]
[AssetsOnly]
public List<EnemyData> enemyList;
//spawn points
[TabGroup("Enemies", "Spawn Points")]
[SceneObjectsOnly]
public List<Transform> spawnPoints;
On the enemy data field, we’ll add the attribute [InlineEditor] with the argument of InlineEditorModes.GUIOnly which will show a collapsible editor for the enemy data which allows easy modification of the data from within the game manager.
Since the enemy data is a scriptable object and the enemy prefabs are all project assets. We can add the attribute [AssetsOnly] to doubly ensure that only project assets are added to those fields.
For the spawn points, we’ll add [SceneObjectsOnly] as the spawn points will need to be in the current scene.
Neither of these last two attributes change the look of the inspector, but can be useful all the same.
The last thing we want to add to the game manager is an “enemy spawn” button - in this case we’ll be adding a button to a function.
Adding buttons to functions a great debugging tool that can help speed up your development.
So above the “SpawnRandomEnemy” function we’ll add the [Button] attribute and in this case we’ll add a ButtonSize argument to create a medium sized button.
[Button(ButtonSizes.Medium)]
[TabGroup("Enemies", "Enemy Data")]
[GUIColor(0.6f, 1f, 0.6f)]
public void SpawnRandomEnemy()
{
if (enemyList.Count == 0 || spawnPoints.Count == 0)
return;
GameObject enemyToSpawn = Instantiate(enemyPrefab);
//inject data
EnemyData data = enemyList[Random.Range(0, enemyList.Count)];
enemyToSpawn.GetComponent<EnemyControl>().SetEnemyData(data);
//set location
enemyToSpawn.transform.position = spawnPoints[Random.Range(0, spawnPoints.Count)].position;
}
Then we’ll add a [TabGroup] attribute so that this button is grouped with the enemy data.
Finally, and just for fun, we’ll add the [GUIColor] attribute, which can be added to any field, to give the button a little more flair!
So there you go! A few attributes have made a dramatic change to the game manager inspector. The choices are of course a matter of personal preference, but we think the end result is easier to read and far easier to use.
If you have any questions, comments or ideas for future tutorials we’d love to hear from you.
And with that, until next time happy Game Designing!