Overview
Custom inspectors help you to change the way the inspectors look and work. They also allow you to add a lot of flexibility and certain behaviours to your regular inspectors.
We’ll create a custom inspector and make a very basic 2D tile map editor using the same. Map editors or levels editors are tools that are used to quickly create map layouts for level design. They can be used for prototyping a level too. Although, there is a plethora of map editors already available on the asset store and Unity is already working on their own version, creating one yourself will help you learn the basics of Editor scripting in Unity.
Scripting
We’ll start in a new, empty scene. Create a new game object and call it “Map Editor”. Create a new C# script, name it “Map”, and attach it to the newly created game object. Remove the Start() and Update() methods. We won’t be needing them.
The Map.cs script should look like this:
using UnityEngine;
using System.Collections;
public class Map : MonoBehaviour
{
}
Create a new Folder named “Editor” in the Assets folder. Scripts put inside this folder will be treated as editor scripts which help add functionality to the Unity editor and hence these scripts are not included in the final, finished game.
Inside the editor folder, create another C# script and call it “MapEditor”. Remove the Start() and Update() methods from this one as well. Also, since this is an editor script, it needs to extend from the “Editor” class instead of the “MonoBehaviour” class. You’ll need to add the UnityEditor namespace at the top.
We also want this script to know what class the Editor is being used for. It can be done by adding the “CustomEditor” attribute to the class with the name of the MonoBehaviour class as the type.
The MapEditor.cs script should looks like this:
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(Map))]
public class MapEditor : Editor
{
}
Next, we’ll need to declare an array for prefabs that are to be loaded and used as tiles. There’ll also be a seletedPrefab game object to store the selected prefab reference and then there’ll a game object list to store all the instantiated game objects.
GameObject[] prefabs;
GameObject selectedPrefab;
List spawnedGO = new List();
To be able to add our own components to the Custom Inspector, we’ll need to override the OnInspectorGUI() method. We call the DrawDefaultInspector() method inside it.
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.Collections.Generic;
[CustomEditor(typeof(Map))]
public class MapEditor : Editor
{
GameObject[] prefabs;
GameObject selectedPrefab;
List spawnedGO = new List();
public override void OnInspectorGUI()
{
DrawDefaultInspector ();
}
}
Now, we’ll need a few prefabs which are to be loaded and shows in the inspector. First, we create a new folder called “Resources” in the “Assets” folder. Inside it, create a “Prefabs” folder. This is where we’ll put our prefabs.
Get a few sprites that you’d like to use in your game. I used a few from this awesome and free platformer art pack by Kenney.
Drag and drop a few sprites in the scene and add BoxCollider2D component to them. We’ll need the colliders later to detect raycasts. Create prefabs for each of those. Place those prefabs in the “Prefabs” folder we created a bit earlier. Remove the game objects from the scene.
Next, we need to load all the prefabs available in the “Prefabs” folder.
//Load all prefabs as objects from the 'Prefabs' folder
Object[] obj = Resources.LoadAll ("Prefabs",typeof(GameObject));
//initialize the game object array
prefabs = new GameObject[obj.Length];
//store the game objects in the array
for(int i=0; i < obj.Length; i++)
{
prefabs[i]=(GameObject)obj[i];
}
To display the loaded prefabs as buttons in the inspector, we create one button for each loaded prefab. We also get the image used in the prefabs and put it over our button so we know which buttons selects which prefab. The GUILayout.BeginHorizontal () method begins a horizontal control group. All the content inside this will be placed horizontally next to each other. Add the following code inside the OnInspectorGUI() method.
GUILayout.BeginHorizontal ();
if(prefabs!=null)
{
for( int i=0; i ().sprite.texture;
//create one button for each prefab
//if a button is clicked, select that prefab and focus on the scene view
if(GUILayout.Button(prefabTexture,GUILayout.MaxWidth(50), GUILayout.MaxHeight(50)))
{
selectedPrefab = prefabs[i];
EditorWindow.FocusWindowIfItsOpen();
}
}
}
GUILayout.EndHorizontal();
If a button is clicked, the selectedPrefab variable will be supplied with the relevant prefab selected and the focus will move to the Scene View, where we can place/spawn/instantiate our selected prefab.
The inspector should look like this:
See the problem? The buttons are placed horizontally but they continue beyond the width of the inspector. A quick and dirty fix will be counting the number of buttons in a row, and if if goes past a certain number of buttons, end the current horizontal group and start a new one. If you want it to be dynamic, you can check the width of the inspector using Screen.width and set the number of buttons accordingly, like so:
GUILayout.BeginHorizontal ();
if(prefabs!=null)
{
int elementsInThisRow=0;
for( int i=0; i ().sprite.texture;
//create one button for earch prefabs
//if a button is clicked, select that prefab and focus on the scene view
if(GUILayout.Button(prefabTexture,GUILayout.MaxWidth(50), GUILayout.MaxHeight(50)))
{
selectedPrefab = prefabs[i];
EditorWindow.FocusWindowIfItsOpen();
}
//move to next row after creating a certain number of buttons so it doesn't overflow horizontally
if(elementsInThisRow>Screen.width/70)
{
elementsInThisRow=0;
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal ();
}
}
}
GUILayout.EndHorizontal();
Now, we need to move to the Scene View. Create an OnSceneGUI() method. It handles all the events in the scene view.
In this method, we check if the Key “E” is pressed and instantiate the selected prefab as a game object at the position of the mouse pointer inside the scene view.
void OnSceneGUI()
{
Vector3 spawnPosition = HandleUtility.GUIPointToWorldRay (Event.current.mousePosition).origin;
//if 'E' pressed, spawn the selected prefab
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.E)
{
Spawn(spawnPosition);
}
}
The Spawn() method simply takes the mouse position and instantiates the prefab at that position. It also renames the instantiated game object and adds it to the spawnedGO list we created at the beginning and sets the newly created game object as the selectedGameObject. We’ll use this later in the script. Following is the code for the Spawn() method.
GameObject selectedGameObject;
void Spawn(Vector2 _spawnPosition)
{
GameObject go = (GameObject)Instantiate(selectedPrefab,new Vector3(_spawnPosition.x, _spawnPosition.y, 0),
selectedPrefab.transform.rotation);
selectedGameObject = go;
go.name = selectedPrefab.name;
spawnedGO.Add(go);
}
At this point, you have your basic tile editor ready to use. Just select the Map Editor game object in the hierarchy, click on a button to select a prefab to spawn, go to the scene view and press ‘E’ anywhere you want to spawn the selected prefab. It’s usable, but we can make it better by adding a few nifty features.
To add an undo feature, we check if the “X” key is pressed. Then, we destroy the last element in the spawnedGO list and remove it from the list too:
//if 'X' is pressed, undo (remove the last spawned prefab)
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.X)
{
if(spawnedGO.Count>0)
{
DestroyImmediate(spawnedGO[spawnedGO.Count-1]);
spawnedGO.RemoveAt(spawnedGO.Count-1);
}
}
We can add a bit of GUI in the scene view itself to indicate if the tile edit mode is active and if a prefab has been selected to spawn.
Handles.BeginGUI();
GUILayout.Box("Map Edit Mode");
if(selectedPrefab==null)
{
GUILayout.Box("No prefab selected!");
}
Handles.EndGUI();
My favorite feature is the snapped spawning, where the game objects are spawned next to each other so you don’t need to adjust their positions every time you spawn one.
The following code checks for a directional key press (W,S,A,D; not the arrow keys. You can use them too though) and spawns the selected game object next to the last spawned game object. The code checks for the width and height of both the spawned and to be spawned game objects so they are placed right next to each other even if they are of different widths and heights. Note that selectedPrefab is the prefab selected using the buttons in the inspector while the selectedGameObject is the last spawned game object in the scene.
if(selectedPrefab!=null)
{
if(selectedGameObject!=null)
{
float selectedGameObjectWidth = selectedGameObject.GetComponent().bounds.size.x;
float selectedGameObjectHeight = selectedGameObject.GetComponent().bounds.size.y;
float selectedPrefabWidth = selectedPrefab.GetComponent().bounds.size.x;
float selectedPrefabHeight = selectedPrefab.GetComponent().bounds.size.y;
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.W)
{
spawnPosition = new Vector3(selectedGameObject.transform.position.x, selectedGameObject.transform.position.y+
(selectedGameObjectHeight/2)+(selectedPrefabHeight/2), 0);
Spawn(spawnPosition);
}
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.S)
{
spawnPosition = new Vector3(selectedGameObject.transform.position.x, selectedGameObject.transform.position.y-
(selectedGameObjectHeight/2)-(selectedPrefabHeight/2), 0);
Spawn(spawnPosition);
}
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.A)
{
spawnPosition = new Vector3(selectedGameObject.transform.position.x-(selectedGameObjectWidth/2)-
(selectedPrefabWidth/2), selectedGameObject.transform.position.y, 0);
Spawn(spawnPosition);
}
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.D)
{
spawnPosition = new Vector3(selectedGameObject.transform.position.x+(selectedGameObjectWidth/2)+
(selectedPrefabWidth/2), selectedGameObject.transform.position.y, 0);
Spawn(spawnPosition);
}
}
}
But what if we want to spawn a prefab next to a different game object as opposed to the last spawned one? To achieve this, we can shoot a ray below the mouse pointer in the scene view every time we press the key, say, “R”, and check for a game object at that point. If a game object is found, we set it as the selected game object, so the next time we try snapped spawning, the game object is spawned to the newly selected one. Note that the raycast won’t work if your game object doesn’t have a collider.
if (Event.current.type==EventType.KeyDown &&Event.current.keyCode == KeyCode.R)
{
Vector2 mouseWorldPosition = new Vector2(spawnPosition.x, spawnPosition.y);
RaycastHit2D hitInfo = Physics2D.Raycast(mouseWorldPosition, Vector2.zero);
if(hitInfo.collider != null)
{
selectedGameObject = hitInfo.collider.gameObject;
}
}
To indicate which game object is currently selected, we add a handle at its position using the following code. This will show an “X” mark over the currently selected game object in the Scene View.
if(selectedGameObject!=null)
{
Handles.Label(selectedGameObject.transform.position, "X");
}
You can also add a small circle handle below the mouse pointer to see what the current spawn position is. At the end of the OnSceneGUI() method, call the SceneView.RepaintAll() method to update the scene view.
//used to indicate the exact point where the prefab will be instantiated
Handles.CircleCap (0, spawnPosition, Quaternion.Euler(0,0,0), .05f);
//…
//...
SceneView.RepaintAll();
That should do it. Here’s a glorious .gif.
Harshit Dubey | Unity Game Developer
I am Unity game developer and a casual gamer. I am fond of story-driven games and creating one based on strong and compelling narrative structure is my dream.