Introduction
Unity has the ScriptableWizard class that can be used to create tools in the editor, just like the EditorWindow class. Actually the ScriptableWizard class, inherits from the EditorWindow class and although it is not as powerful, it is easier to use because it has some things already implemented for us.
The ScriptableWizard will create a window, that can have one or two buttons and some serialized fields that act as fields in any editor window.
The ScriptableWizard class
The most important method is the static DisplayWizard
that creates our wizard. After that we can use the:
OnWizardUpdate
that is called whenever the wizard is opened or a change has happened in the wizard- the
OnWizardCreate
when the first button is clicked - the
OnWizardOtherButton
for when the second button is clicked. (the second button is optional)
Those are the basic methods that are needed for basic functionality. There are of course other methods and properties that can help us with our wizard, like the SaveChanges
method, the helpString
and the errorString
properties. For the full documentation you can check: the ScriptableWizard documentation.
A simple use case
An example is always better, so let suppose that we have the following situation:
We have a simple MonoBehaviour class that we add to all our enemies:
public class Enemy : MonoBehaviour
{
public int maxHp;
public int attackDamage;
// rest of the code here
}
we want a tool that when we click an enemy in our scene view in the editor, can delete our enemy game object, or change its values. This tool will be a floating window, that has only the values of the Enemy class that we want to change and is not filled with information about other components our enemies may have. That way, all the info we need, is neatly organized and contained in a small space.
The script
Let’s start by creating our script:
public class ModifyEnemy : UnityEditor.ScriptableWizard
{
[SerializeField] private int maxHp;
[SerializeField] private int attackDamage;
private Enemy _enemy;
we create serialized fields that will hold the values that can be changed in our wizard. We also have an _enemy
field that will have a reference to our currently selected enemy.
Then we create our wizard:
[MenuItem("Tools/Change Enemy stats")]
private static void CreateWizard() =>
DisplayWizard<ModifyEnemy>("Change Enemy stats", "Delete Enemy", "Apply change");
In the first line, we create a menu item called Change Enemy stats
under a Tools
menu. Whenever we click it, the DisplayWizard<ModifyEnemy>
is called with three parameters:
- The first parameter is the title of our window
- The second is the label of our first button
- The third is the label of our second button and is optional, our wizard could have only one button if we wanted.
Now we need a method that checks if a game object is selected in our editor scene and if that selection is a game object performs the appropriate actions:
private void ShowSelection()
{
if(Selection.activeTransform)
_enemy = Selection.activeTransform.GetComponent<Enemy>();
if (_enemy)
{
errorString = "";
isValid = true;
helpString = _enemy.gameObject.name;
maxHp = _enemy.maxHp;
attackDamage = _enemy.attackDamage;
}
else
{
errorString = "Please chose a game object with the Enemy script!";
isValid = false;
helpString = "Select an Enemy!";
maxHp = 0;
attackDamage = 0;
}
}
Two things are happening here:
First we check if our selection has a transform, that way we know that it is a game object in our scene and not any other object, like a prefab or a non-modifiable object. If we wanted to be able to get those we would have checked with Selection.activeGameObject
.
Second if the active object has an Enemy component then we change our fields to have the same values as our enemy, we change the help text with the helpString
property to have the game object’s name, we make our error string with the errorString
property an empty string and we make the isValid
property true. The isValid
property enables/disables the wizard buttons. If there is not an Enemy component in our game object, then we display an error message and disable the ScriptableWizard buttons.
This ShowSelection
method gets called in two places, in Awake
method and every time the selection in the editor changes, that is the OnSelectionChange
method:
private void Awake() => ShowSelection();
private void OnSelectionChange() => ShowSelection();
Now let’s hook up our two buttons. Our first button deletes the selected Enemy and the second applies the changes we have made in our ScriptableWizard window to the Enemy. The reason that the first button deletes the enemy is that when the first button in a ScriptableWizard is clicked then the window closes. The same doesn’t happen by clicking the second button, this way we can make a lot of changes to our fields, before closing the wizard.
Here is the script for our first button:
private void OnWizardCreate()
{
if(_enemy != null && EditorUtility.DisplayDialog("Confirm","Are you sure you want to delete this enemy?", "Yes", "No"))
DestroyImmediate(_enemy.gameObject);
}
At the beginning we check if an enemy is selected, then we want to present the user with a confirmation dialog, so that he can confirm that deleting the enemy is the desired action. If the user presses yes then the game object is deleted from the scene.
Now let’s see the script for the second button:
private void OnWizardOtherButton()
{
if (!_enemy) return;
hasUnsavedChanges = false;
_enemy.maxHp = maxHp;
_enemy.attackDamage = attackDamage;
}
If an enemy is selected, we assign the Scriptable wizard’s values to the Enemy script values. We also make the hasUnsavedChanges
property true. The hasUnsavedChanges
is an inherited property of the ScriptableWizard class and shows to the user a prompt to save or discard any changes, if he tries to close the window. Here because we apply our changes we make it false.
Where this property gets the value true? In the following method:
private void OnWizardUpdate()
{
if (!_enemy) return;
if (maxHp != _enemy.maxHp || attackDamage != _enemy.attackDamage)
{
hasUnsavedChanges = true;
saveChangesMessage = $"Values have changed in {_enemy.gameObject.name}! PressSave to apply changes the to the selected enemy!";
}
}
the OnWizardUpdate
method gets called whenever the wizard is opened or a change has happened in the wizard. If an Enemy is selected and the Enemy’s values are different from the values of the ScriptableWizard fields we make the hasUnsavedChanges
true and add a description to the saveChangesMessage
property.
This message is displayed if the user tries to close the window when the hasUnsavedChanges
is true. That means that we only need to create one more method. This method is an override of the SaveChanges
method and specifies what happens when the user chooses to save the changes that have happened:
public override void SaveChanges()
{
base.SaveChanges();
OnWizardOtherButton();
}
And we are done!
Here is the whole script for reference:
public class ModifyEnemy : UnityEditor.ScriptableWizard
{
[SerializeField] private int maxHp;
[SerializeField] private int attackDamage;
private Enemy _enemy;
[MenuItem("Tools/Change Enemy stats")]
private static void CreateWizard() =>
DisplayWizard<ModifyEnemy>("Change Enemy stats", "Delete Enemy", "Apply change");
private void Awake() => ShowSelection();
private void OnSelectionChange() => ShowSelection();
private void OnWizardUpdate()
{
if (!_enemy) return;
if (maxHp != _enemy.maxHp || attackDamage != _enemy.attackDamage)
{
hasUnsavedChanges = true;
saveChangesMessage = $"Values have changed in {_enemy.gameObject.name}! Press Save to apply changes the to the selected enemy!";
}
}
private void OnWizardCreate()
{
if(_enemy != null && EditorUtility.DisplayDialog("Confirm","Are you sure you want to delete this enemy?", "Yes", "No"))
DestroyImmediate(_enemy.gameObject);
}
private void OnWizardOtherButton()
{
if (!_enemy) return;
hasUnsavedChanges = false;
_enemy.maxHp = maxHp;
_enemy.attackDamage = attackDamage;
}
public override void SaveChanges()
{
base.SaveChanges();
OnWizardOtherButton();
}
private void ShowSelection()
{
if(Selection.activeTransform)
_enemy = Selection.activeTransform.GetComponent<Enemy>();
if (_enemy)
{
errorString = "";
isValid = true;
helpString = _enemy.gameObject.name;
maxHp = _enemy.maxHp;
attackDamage = _enemy.attackDamage;
}
else
{
errorString = "Please chose a game object with the Enemy script!";
isValid = false;
helpString = "Select an Enemy!";
maxHp = 0;
attackDamage = 0;
}
}
}
Thank you for reading and as always if you have any questions or comments you can use the comments section or contact me directly via the contact form or email, also if you don’t want to miss any of the new articles, you can always subscribe to my newsletter or the RSS feed.