The Unity Editor is (undeniably) an awesome engine for building games, but often gets this close to actually fitting your needs. At times, you probably find yourself wishing for a “do what I mean” button.
Although Unity isn’t quite capable of interpreting brainwaves through some mystical button, there ways to extend this tool to make it more useful and user-friendly.
In this Unity tutorial, you’ll learn how to extend and customize the engine. Aside from the accomplishment of being master of your domain, your changes should have the domino effect of speeding up game development by making the process a little (or a lot) leaner. :]
In particular, you’ll focus on how to use the following:
- Attributes for basic customization
- ScriptableObject to create custom assets and reduce memory usage
- Customize the Inspector using
PropertyDrawer
andEditor
- Create a new editor window to provide your own editing tools
- Draw in a scene with gizmos
Are you doubtful that customizing Unity can be helpful? On Gamasutra, you can find a cool article about Unity and editor tools that show a number of examples of how a custom editor can benefit your game design and creation efforts.
Prerequisites
You need Unity version 5.3 or newer to load the starter project successfully. If you don’t have it, then download Unity from Unity3D.com.
This tutorial is intended for advanced Unity developers, meaning you should know basic scripting and feel comfortable working in the Unity editor. If you’re not quite there, try honing your knowledge with our other Unity tutorials.
Getting Started
Download the starter project, unpack the zip file and open the resulting TowerDefenseEditor folder with Unity. As the folder name suggests, the project is a tower defense game.
Although the game runs smoothly, it’s difficult to edit. For example, the process for adding new open spots for the purpose of placing new “towers” is a bit annoying. Also, you can’t see the path the enemies follow.
In this tutorial, you’ll extend the Unity Editor to add those and a few more features so that it looks a lot like this:
This tutorial goes from smaller to bigger changes. Each section is self-contained, so you may skip around and work with the Editor extensions that pique your interest.
Basic Customizations with Attributes
These give you the power to drastically reorganize the Unity3D editor, but all you need are some minor adjustments, e.g., hiding a field, showing a tooltip or adding a headline. Hiding unwanted fields and protecting others are two strong use cases for attributes.
Hide Public Fields – [HideInInspector]
As per the default, all public fields of a MonoBehavior are exposed in Inspector, but sometimes you don’t want all that. Sometimes, you just want to:
- Remove clutter from Inspector
- Avoid accidental changes to a variable that could mess up your game
Time to get your hands dirty!
Go to the Assets/Prefabs folder and select Bullet1, and then look at Inspector. You should see the following:

Before: Lots of fields you should not touch
You can see the fields Target, Start Position and Target Position. Those variables are public because they need to be assigned by the ShootEnemies script.
However, they don’t need to be editable in Inspector because bullets always fly from the monster to its target; you don’t need these fields exposed.
Open the script BulletBehavior.cs in the Assets/Scripts folder. Import using System;
by inserting it into line 2.
Before:
using UnityEngine; using System.Collections; |
After:
using UnityEngine; using System; using System.Collections; |
Add the [HideInInspector]
and [NonSerialized]
attributes before the lines that contain the definitions of the fields target, startPosition and targetPosition. [NonSerialized] is part of the System NameSpace, hence why you inserted using System;
above.
Before:
public GameObject target; public Vector3 startPosition; public Vector3 targetPosition; |
After:
[HideInInspector] [NonSerialized] public GameObject target; [HideInInspector] [NonSerialized] public Vector3 startPosition; [HideInInspector] [NonSerialized] public Vector3 targetPosition; |
Save your changes.
HideInInspector
removes the field from Inspector and successfully hides the field. However, if you edit the value of the variable in Inspector before you hide things, Unity will remember the value you set. This can be confusing.
To avoid this, use the attribute [NonSerialized]
to stop Unity from remembering the value of the variable.

After: Fields are hidden
Note: Using attributes is simple. You just put them above the field, method or class they should influence, and you also have the option to combine several attributes.
[RequireComponent]: Avoid Accidentally Removing a Component
Try this on the OpenSpot
prefab to make the placement always work as expected.
Open the file PlaceMonster.cs from the folder Assets/Scripts. Edit the code as follows:
Before:
using System.Collections; public class PlaceMonster : MonoBehaviour { |
After:
using System.Collections; [RequireComponent (typeof (AudioSource))] [RequireComponent (typeof(Collider2D))] public class PlaceMonster : MonoBehaviour { |
Save your changes.
In Inspector, try to remove either the Collider2D or the AudioSource component from the OpenSpot
prefab found in the Assets/Prefabs folder. A dialog appears when you try to delete it because of RequireComponent
, it says you can’t remove it and also tells you which script requires it. Neat!
Other Interesting Tags
In addition to the ones you worked with above, there are plenty more cool attributes. Here’s a short overview:
Usage | Attribute |
---|---|
Add items to Unity’s menus | MenuItem, AddComponentMenu, ContextMenu, ContextMenuItem, CreateAssetMenu |
Show or hide information | HeaderHelpURL, Tooltip, HideInInspector |
Layout display in Inspector | Multiline, Range, Space, TextArea |
Define constraints for component usage | RequireComponent, DisallowMultipleComponent |
Specify whether to remember the value of a field or not | NonSerialized, Serializable |
Execute callback functions even when Editor isn’t in playmode | ExecuteInEditMode |
Configure the usage of the ColorPicker | ColorUsage |
ScriptableObject: Create Custom Assets and Reduce Memory Usage
In this section, you’ll store a bullet’s configuration in a ScriptableObject instead of inside the GameObject.
You’re probably thinking: But it worked perfectly fine with GameObject! Why should I change it? To understand why you would want to do this, simply look into the basics of scriptable objects.
Scriptable Objects: The Basics
The intended use case for ScriptableObject is for those times you need to reduce memory usage by avoiding copies of values.
How does it do that? When you create a GameObject, Unity stores all primitive types connected with it by value.
Your bullet has two integer fields: speed
and damage
. One integer requires 2 bytes, so you need 4 bytes total. Now imagine you have 100 bullets — you would already need at least 400 bytes. This is with no change to speed or damage. It’s a great way to quickly consume resources.
ScriptableObject stores these values by reference, so you could use it when:
- You store a lot of data for a particular type of object.
- Values are shared between many GameObjects.
While the major value of ScriptableObject is that it reduces memory requirements, you can also use it to define custom assets for dialogs, level data, tile sets and more. This allows you to decouple data and GameObjects.
Implement a ScriptableObject
In this subsection, you’ll create your first scriptable object.
Inside of Unity and in the Project Browser, open the Assets/Scripts folder, right-click it and select Create > C# Script. Name the script BulletConfig, open it and change its contents as follows:
using UnityEngine; [CreateAssetMenu(menuName = "Bullet", fileName = "Bullet.asset")] public class BulletConfig : ScriptableObject { public float speed; public int damage; } |
And save.
CreateAssetMenu
makes the creation of a ScriptableObject available under the Assets main menu option.
Switch back to Unity, go to the Project Browser and navigate to the Assets/Bullets folder. Click on Assets/Create/Bullet from the main menu. Name the created asset Bullet1 — now you have a nice ScriptableObject.
Create two more bullets named Bullet2 and Bullet3.
Now click on Bullet1 and go into Inspector. Set speed to 10 and damage to 10.
Repeat for Bullet2 and Bullet3 with the following values:
- Bullet2: speed = 10, damage = 15
- Bullet3: speed = 10, damage = 20
Now you’ll replace the speed and damage fields in BulletBehavior
with BulletConfig
. Open BulletBehavior. Remove the fields for speed
and damage
and add this field:
Before:
public float speed = 10; public int damage; |
After:
public BulletConfig bulletConfig; |
Since all your other scripts still need read access to speed
and damage
, you need to create a getter for both variables.
Before the closing brace of the class, add the following:
private int damage { get { return bulletConfig.damage; } } private float speed { get { return bulletConfig.speed; } } |
Save those changes.
Without touching any other script, you improved memory usage by replacing the variables inside the bullets with a scriptable object.
Lastly, you need to assign your scriptable object to your bullet prefabs.
In the Project Browser, select Prefabs / Bullet1. In Inspector, set Bullet Config to the Bullet1.asset. Repeat this for Bullet2 and Bullet3 accordingly. Done!

Adding the scriptable objects to the bullet prefabs.
Run the game to make sure everything still works.
Customize Inspector With PropertyDrawer and Editor
So far, you’ve only changed minor things, but could redraw almost everything in Inspector. This section is all about flexing your redrawing muscles.
Editor
With Editor, you can replace Inspector GUI for a MonoBehavior. The implementation is to display the health of each enemy with a slider, where the slider’s length depends on the maximum health of the enemy, making it impossible to enter invalid values.
Inside of the Assets / Editor folder, create a new C# script named EnemyDataEditor. Open the script in MonoDevelop. Replace the contents with the following:
using UnityEditor; // 1 [CustomEditor(typeof(MoveEnemy))] // 2 public class EnemyDataEditor : Editor { // 3 public override void OnInspectorGUI() { // 4 MoveEnemy moveEnemy = (MoveEnemy)target; HealthBar healthBar = moveEnemy.gameObject.GetComponentInChildren<HealthBar> (); // 5 healthBar.maxHealth = EditorGUILayout.FloatField ("Max Health", healthBar.maxHealth); // 6 healthBar.currentHealth = EditorGUILayout.Slider("Current Health", healthBar.currentHealth, 0, healthBar.maxHealth); // 7 DrawDefaultInspector (); } } |
Save your changes.
This is how each step works:
- Specify that Unity should create a
CustomEditor
forMoveEnemy
in this attribute. - Make
EnemyDataEditor
a subclass ofEditor
so it can access methods such asOnInspectorGUI
. - Add
OnInSpectorGUI
, a method that’s called when drawing the editor. - In
Editor
, you can access the instance that’s currently selected, either in the Project Browser or Hierarchy, viatarget
. Then you cast it toMoveEnemy
. Finally, you get its only child — theHealthBar
. - Use
EditorGUILayout.FloatField
to add a field formaxHealth
that only accepts float values. - Create a slider for the actual current health with
EditorGUILayout.Slider
. This differs from simply using the attribute[Range]
because it allows adjustments to the slider’s range depending on a variable. In this case, it’shealthBar.maxHealth
. - A custom editor replaces everything that would usually appear in the Inspector. In cases where you only want to extend the existing view, you call
DrawDefaultInspector
. It draws any remaining default content in the location where you’ve added the call.
Switch back to Unity and select Prefabs / Enemy in the Project Brower and enjoy your result.
Run the game to make sure everything still works. You should now be able to select an Enemy(Clone) in the Hierarchy and change its health on the slider in the Inspector while the game is in progress. Cheating was never so much fun. :]
![Slide or slider? - I know what I will choose :]](http://cdn1.raywenderlich.com/wp-content/uploads/2016/04/blue-1005940_640.jpg)
Slide or slider? – I know what I will choose :] (Published under CC0 on pixabay)
Property Drawers
With property drawers, you can:
- Customize the GUI for every instance of a Serializable class, such as
Wave.
- Customize the GUI for fields with custom PropertyAttributes, such as
Range
.
In Hierarchy, select Road. Check out all the different waves in the Inspector.
Pretend that in addition to the Enemy Prefab, you also want to show a preview image of that prefab. You can do this with a PropertyDrawer for Wave
.

Customized Inspector with property drawer – before and after.
Start by creating a new C# Script named WavePropertyDrawer inside the Assets/Editor folder in the Project Browser. Open it in MonoDevelop and replace the entire contents as follows:
//1 using UnityEditor; using UnityEngine; //2 [CustomPropertyDrawer(typeof(Wave))] //3 public class WavePropertyDrawer : PropertyDrawer { //4 private const int spriteHeight = 50; } |
Save your changes.
In this code block, you:
- Import
UnityEditor
to enable access to the Editor’s libraries. - Use an Attribute to tell your class it’s a
CustomPropertyDrawer
forWave
. - Make it a subclass of
PropertyDrawer
. - Create a constant to store the height at which the enemy sprite should be drawn.
Go back in Unity and select Road in the Hierarchy. You should see the message No GUI implemented in Inspector.
Change this and add OnGUI
to WavePropertyDrawer:
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { EditorGUI.PropertyField(position, property, label, true); } |
Overriding OnGUI
lets you implement your own GUI. The method takes three arguments:
position
: A rectangle to draw the GUI intoproperty
: The property you want to drawlabel
: A text that labels the property
You draw the default GUI for the property with the method EditorGUI.PropertyField
; it takes all the variables you get from OnGUI
.
The last parameter is a bool variable that tells Unity to draw the property itself as well as its children.
In addition to OnGUI
, you need to implement GetPropertyHeight()
:
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUI.GetPropertyHeight (property); } |
This method returns the height for the property, and is especially important for properties that can change height depending on whether the property is expanded or not.
If you don’t implement it then Unity assumes the default height. In cases where your property is higher, it will draw on top of other fields in Inspector. This is usually not what you want. :]
Now that you have the basics in place, you’re free to customize the GUI. Add the following code to OnGUI()
:
// 1 if (property.isExpanded) { // 2 SerializedProperty enemyPrefabProperty = property.FindPropertyRelative ("enemyPrefab"); GameObject enemyPrefab = (GameObject) enemyPrefabProperty.objectReferenceValue; // 3 if (enemyPrefab != null) { SpriteRenderer enemySprite = enemyPrefab.GetComponentInChildren<SpriteRenderer> (); // 4 int previousIndentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 2; // 5 Rect indentedRect = EditorGUI.IndentedRect (position); float fieldHeight = base.GetPropertyHeight (property, label) + 2; Vector3 enemySize = enemySprite.bounds.size; Rect texturePosition = new Rect (indentedRect.x, indentedRect.y + fieldHeight * 4, enemySize.x / enemySize.y * spriteHeight, spriteHeight); // 6 EditorGUI.DropShadowLabel(texturePosition, new GUIContent(enemySprite.sprite.texture)); // 7 EditorGUI.indentLevel = previousIndentLevel; } } |
Save your changes.
Step-by-step breakdown:
- You only want to draw the enemy sprite when the details of the property should be shown, for instance, when the user unfolds it in Inspector. In this case,
property.isExpanded
is true. - Get the enemyPrefab. It calls
FindPropertyRelative
first to retrieve the property linked to another property by name. Then it gets theobjectReference
that stores the actual GameObject that contains theenemyPrefab
. - If the
enemyPrefab
is set, then you get its sprite. - Set the
indentLevel
properly. This level specifies how far to the right Unity should start drawing. Since you’ll need to restore its original value after you’re done drawing, you store it in a variable first, and then you setindentLevel
to 2. - Get the position of the
indentedRect
so you can calculate the coordinates required to draw inside a property drawer. Then you get the height of one normal field withGetPropertyHeight
. You need the size of the enemy sprite too. With these three values, you can calculate the rect for drawing the texture. The origin is that of theindentedRect
. However, you add four times thefieldHeight
to reserve space for the fields drawn. Because the height should be the same, no matter the actual size of the enemy, you scale the width accordingly. - Call
DropShadowLabel
to draw a texture within the specified position. - In the end, you reset the indent level.
Go back to Unity to see the enemy sprite in the proper position. It’s there, but it overlaps with the next property. Why?
Go back to Unity and select Road in the Hierarchy. In Inspector, you can now see the sprite of the enemy properly.
Aside from adding one or more sprites, you can change Inspector more significantly. One example is directly from the PropertyDrawer documentation.
Property drawers allow you to customize how Inspector is drawn for any property of a Serializable class. They allow you to dictate how one specific property of your MonoBehavior is drawn without a change to the rest of Inspector.
Property drawers are especially helpful when you want to redraw items in an array.
However, they come with some serious drawbacks. Namely, you don’t have access to EditorGUILayout()
, leaving you to calculate each field’s position by yourself. Additionally, you can only access info stored in the property or its children.
Create a New Editor window to provide Your Own Editing Tools
In Unity, you have several cool Editor window, such as Animator, Inspector and Hierarchy. And of course, you can even make your own.
In this section, you’ll implement an Editor view that allows you to place and delete OpenSpots
with one click.
In the Project Browser, navigate to Assets/Editor and create a new C# Script named LevelEditorWindow. Open it in MonoDevelop. Then replace the entire contents with this:
using UnityEngine; //1 using UnityEditor; //2 public class LevelEditorWindow : EditorWindow { //3 int selectedAction = 0; } |
- Import
UnityEditor
- Make
LevelEditorWindow
a subclass ofEditorWindow
to allow access to methods and fields available toEditorWindow
. - Keep track of which action the user currently has selected with the variable
selectedAction
.
Right now, you couldn’t open this level editor inside of Unity even if your life depended on it, so add the following method to change that:
[MenuItem ("Window/Level Editor")] static void Init () { LevelEditorWindow window = (LevelEditorWindow)EditorWindow.GetWindow (typeof (LevelEditorWindow)); window.Show(); } |
In the first line, you tell Unity to create a MenuItem
named Level Editor
below the Window
menu, and you just click it to run Init
.
With GetWindow
inside, you get an existing open window or create a new one if none exists. Then you display it with Show
.
Save your changes and go back to Unity. You’ll see a warning. That’s alright — you’ll fix it in a minute. Just go and click on the main menu Window/Level Editor to bring up an empty editor view.
Now to fill that empty view with content. Switch back to MonoDevelop and add the following code to LevelEditorWindow:
void OnGUI() { float width = position.width - 5; float height = 30; string[] actionLabels = new string[] {"X", "Create Spot", "Delete Spot"}; selectedAction = GUILayout.SelectionGrid (selectedAction, actionLabels, 3, GUILayout.Width (width), GUILayout.Height (height)); } |
First you get the width and height. For the width, you deduct 5 to leave a little bit space. Next, you create a string
array with the label for each action — “X” stands for no action. This lets you select GameObjects instead of creating or deleting open spots.
From this array, you create a SelectionGrid
, which creates a grid of buttons where only one can be selected at once. The return value of SelectionGrid
is the index of the currently selected button, so you store it in selectedAction
.
Save your changes and have a look at your creation in Unity.
You want to have a different reaction to clicks that’s dependent on selected button in the scene, so add the following method:
void OnScene(SceneView sceneview) { Event e = Event.current; if (e.type == EventType.MouseUp) { Ray r = Camera.current.ScreenPointToRay (new Vector3 (e.mousePosition.x, Camera.current.pixelHeight -e.mousePosition.y)); if (selectedAction == 1) { // TODO: Create OpenSpot Debug.Log("Create OpenSpot"); } else if (selectedAction == 2) { // TODO: Delete OpenSpot Debug.Log("Delete OpenSpot"); } } } |
First you store the current event in a variable. If its type is equal to MouseUp
, you react to the mouse click.
For this, you need to figure out which GameObject was selected, and you’re using Raycasting to do it. But first, you need to create a ray that points towards the scene.
To make a ray, you use the helper method ScreenPointToRay()
, which you call with the mouse position stored in the event. As the y-coordinate is reversed between camera and event coordinate system, you transform it by deducting the original y-coordinate from the height of the current view.
Lastly, you split by selectedAction
and log a short message.
Make sure the method is actually called with:
void OnEnable() { SceneView.onSceneGUIDelegate -= OnScene; SceneView.onSceneGUIDelegate += OnScene; } |
This adds OnScene
as delegate.
Save your changes and switch back to Unity. Select the Create Spot or Delete Spot button inside of the LevelEditorWindow, and then click inside of the Scene. The log message will appear in the Console.
In the Hierarchy, select Background. In the Inspector, set its Layer to Background. Then add a component Physics 2D / Box Collider 2D.
Set the Layer of Prefabs/Openspot to Spots. Now you can determine where a ray collided with the background or an open spot.
Go back to MonoDevelop and replace TODO: Create OpenSpot
in LevelEditorWindow with this code:
RaycastHit2D hitInfo2D = Physics2D.GetRayIntersection (r, Mathf.Infinity, 1 << LayerMask.NameToLayer ("Background")); if (hitInfo2D) { GameObject spotPrefab = AssetDatabase.LoadAssetAtPath<GameObject> ("Assets/Prefabs/Openspot.prefab"); GameObject spot = (GameObject)PrefabUtility.InstantiatePrefab (spotPrefab); spot.transform.position = hitInfo2D.point; Undo.RegisterCreatedObjectUndo (spot, "Create spot"); } |
Here’s what you’re doing in there:
It starts by checking whether the ray intersects with the background image. If yes, then hitInfo2D
isn’t null and you create a new
You get the prefab with LoadAssetPath
. From this instance, you create a new instance of this prefab with PrefabUltility.InstantiatePrefab
.
InstantiatePrefab()
creates a prefab connection, which makes it more suitable in this case then Instantiate
. Then you set the position of the spot to the point of the collision.
Unity always lets you undo actions, so remember that when you create custom editor scripts. To allow undo with a custom action, simply register the action used to create a GameObject with Undo.RegisterCreatedObjectUndo
.
Now to handle deleting spots. Replace // TODO: Delete OpenSpot
with this:
RaycastHit2D hitInfo2D = Physics2D.GetRayIntersection (r, Mathf.Infinity, 1 << LayerMask.NameToLayer ("Spots")); if (hitInfo2D) { GameObject selectedOpenSpot = hitInfo2D.collider.gameObject; Undo.DestroyObjectImmediate (selectedOpenSpot); } |
This detects collisions between your ray and any object inside the Spots
layer. If yes, you get the GameObject that was hit. Then you delete it with Undo.DestroyObjectImmediate
, which removes the spot and registers the action with the undo manager.
Go back to Unity, and check out how quickly you can create and delete spots. Best of all, undo and redo work as expected.
Draw Into the Scene with Gizmos
Gizmos let you to draw into the Scene view, allowing you to provide information to use as a visual debugging or editing aid.
For example, the game contains a set of waypoints for the enemies to follow, but they are invisible during the game and while editing. You simply can’t tell which path they’ll follow without doing some serious investigative work. If you wanted to create another level, you still wouldn’t be able to see the path.
Fortunately, Gizmos allow you to see this invisible path.
Open Assets/Scripts/SpawnEnemy, which stores all waypoints, and add the following method:
// 1 void OnDrawGizmos() { // 2 Gizmos.color = Color.green; // 3 for (int i = 0; i < waypoints.Length - 1; i++) { // 4 Vector3 beginPosition = waypoints[i].transform.position; Vector3 endPosition = waypoints[i+1].transform.position; // 5 Gizmos.DrawLine(beginPosition, endPosition); // 6 UnityEditor.Handles.Label (beginPosition, i + ""); } // 7 Vector3 lastWaypointPosition = waypoints[waypoints.Length - 1].transform.position; UnityEditor.Handles.Label (lastWaypointPosition, (waypoints.Length - 1) + ""); } |
Here are explanations for each step:
- You can only use methods for gizmo drawing in
OnDrawGizmos()
or inOnDrawGizmosSelected()
. You always want the path to draw, so you implementOnDrawGizmos()
. - You set the color for the gizmo’s drawing. Every draw call will use this color until you change it.
- Iterate over all waypoints, except the last one that doesn’t have successor to draw a line to, so you stop at
waypoints.Length - 1
. - You store the position of waypoint i, and its successor i+1. into a variable with every iteration.
- With the static method
Gizmos.Drawline(Vector3, Vector3)
, you draw a line from the current waypoint to the next waypoint. - With
UnityEditor.Handles.Label(string)
, you write the index of the waypoint adjacent to it to display the path and visualize the direction that enemies move. - You get the position of the last waypoint and display its index next to it.
Switch back to Unity to see the path. Play with it a bit by moving waypoints around and you’ll see the path follows along.
Aside from DrawLine()
, Gizmos provide several drawing functions. You can find the full list in the documentation. There is even a DrawRay:
Where To Go From Here?
In this tutorial, you learned how to make parts of the Unity Editor your own. To see the final result, download the finished project here.
As you probably guessed, there are more ways to extend Unity. Here are some additional directions you could go:
- Processing assets on import and after save
- Creating editor wizards
- Getting to know more GUI elements such as ColorField, CurveField, Toggle, or Popup
- Learning more about layouting
Now that you’ve made it to the bottom, you’ve got a good bit of starting knowledge about how to to customize Unity for your own purposes and project.
Join us in the forums to toss around ideas and examples, as well as ask questions about customizing Unity. I’m looking forward to seeing what you come up with!
The post Extend the Unity3d Editor appeared first on Ray Wenderlich.