Unity Editor Tools: The Place Objects Tool
In this article, we’ll develop a custom editor tool to streamline placing objects in the scene. We’ll cover several helpful editor APIs, including EditorTool, PrefabUtility, Handles and HandleUtility.
What’s the goal?
As mentioned, we’re building a tool to streamline placing objects in the scene. I want to drop an object every time I click. The object can either be a prefab or a clone of a GameObject from the scene. So, we’ll need a field to specify which thing we’re placing. Finally, I’d like to select a new object by right-clicking to pick the GameObject under the mouse cursor.
Given these requirements, we’ll build a simple tool. I hope this process demonstrates how to make quick and dirty tools to streamline your workflows and increase productivity. When you build tools like this, it’s ok for them to be rough. The goal is to create something that benefits our project, not make a generic commercial tool. Project-specific tools are significantly faster to write. Additionally, when you tailor a tool to your needs, it can outperform generic tools.
So let’s build!
The EditorTool
There are several ways to extend the Unity Editor. For this particular tool, I chose the EditorTool
API. Subclassing EditorTool
creates a new tool in the main toolbar. By that, I mean the toolbar containing the Move, Rotate, Scale, Hand, etc. Since our tool relies on the left and right mouse buttons to function, it makes sense to make it an EditorTool
; otherwise, we’d fight with whichever tool the user has selected at any given moment. So, create a new class called PlaceObjectsTool
that inherits from EditorTool. By the way, since this is for Editor use only, you must put the script file in a folder called “Editor”. Doing so prevents the script from being compiled into builds of the project, which would fail because we rely on several Editor-only APIs.
Here’s our barebones script starting point.
using System;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
[EditorTool("Place Objects Tool")]
class PlaceObjectsTool : EditorTool
{
[SerializeField] Texture2D _toolIcon;
GUIContent _iconContent;
void OnEnable()
{
_iconContent = new GUIContent()
{
image = _toolIcon,
text = "Place Objects Tool",
tooltip = "Place Objects Tool"
};
}
public override GUIContent toolbarIcon
{
get { return _iconContent; }
}
public override void OnToolGUI(EditorWindow window)
{
}
}
By the way, you can override toolbarIcon
to give your tool an icon, but I won’t bother. Aside from that, the most important method is OnToolGUI
. This method is essentially the EditorTool
equivalent of Update
in that it runs every time an editor window repaints.
OnToolGUI
Let’s start by blocking out what we want to do.
- Ensure:
- We’re the active tool
- In the scene view
- Have a placeable object
- Draw a Handle, so the user knows where the object will go if they click.
- If we receive a click, clone the placeable object and place it at the current mouse position.
- Force the window to repaint.
Some of this work is more complicated than it seems, so let’s take it one step at a time.
First, we’ll ensure the preconditions are met.
public override void OnToolGUI(EditorWindow window)
{
//If we're not in the scene view, exit.
if (!(window is SceneView))
return;
//If we're not the active tool, exit.
if (!ToolManager.IsActiveTool(this))
return;
//If we don't have a placeable object, exit.
if (!HasPlaceableObject)
return;
...
Pay attention to the ToolManager.IsActiveTool, because the ToolManager
is a valuable class when writing EditorTools. Here we’re using it to make sure our tool is the active tool. The reason we do this is that the editor could continue to call OnToolGUI
on our tool after being initialized, even if it’s not currently the actively selected tool. There’s also a HasPlaceableObject
property that we’ll return to later.
Next, let’s draw a circle at the current mouse position in world space.
The Handles API
We’ll use the Handles
class to draw our GUI in the Scene View. If you haven’t used Handles
before, you’re in for a treat. This class contains many ways to draw debug tools in the Scene View, including shapes, lines, labels, and existing Unity tools like the Move, Scale and Rotate tools. I highly encourage you to check it out. So follow up the previous line of code with this.
//Draw a positional Handle.
Handles.DrawWireDisc(GetCurrentMousePositionInScene(), Vector3.up, 0.5f);
...
Here we pass in a position, normal and radius for wireframe disc. To calculate the position, I wrote a helper function that returns the position at which an object would spawn if we were to drag it into the scene. In other words, if it finds a surface, it’ll snap to it, and otherwise, it’ll choose a position in space.
Vector3 GetCurrentMousePositionInScene()
{
Vector3 mousePosition = Event.current.mousePosition;
var placeObject = HandleUtility.PlaceObject(mousePosition, out var newPosition, out var normal);
return placeObject ? newPosition : HandleUtility.GUIPointToWorldRay(mousePosition).GetPoint(10);
}
Here we see another valuable class: HandleUtility
. This class contains tons of utility functions that are the backbone of the built-in Scene View tools. So using HandleUtility
does the heavy lifting and keeps your tools consistent with the built-in ones.
Object Instantiation
The next step is to instantiate our selected object. There are a couple of cases to handle. If the selected object is a prefab or belongs to a prefab, we’ll instantiate a linked prefab. Otherwise, we’ll instantiate a clone of the original game object. Append this code after the DrawWireDisc
line.
//If the user clicked, clone the selected object, place it at the current mouse position.
if (_receivedClickUpEvent)
{
var newObject = `_prefabObjectField.value;
GameObject newObjectInstance;
if (PrefabUtility.IsPartOfAnyPrefab(newObject))
{
var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(newObject);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
newObjectInstance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
}
else
{
newObjectInstance = Instantiate((GameObject)newObject);
}
newObjectInstance.transform.position = GetCurrentMousePositionInScene();
Undo.RegisterCreatedObjectUndo(newObjectInstance, "Place new object");
Event.current.Use();
_receivedClickUpEvent = false;
}
There are couple of unfamiliar fields here: _receivedClickUpEvent
and _prefabObjectField
. We’ll come back to these later. The main thing I want to introduce right now is the PrefabUtility
class. This class contains a bunch of prefab-related utility functions for the editor. For example, you can use PrefabUtility.InstantiatePrefab
to create a linked prefab in the scene instead of an unlinked game object clone.
So we take the selected object and check if it’s part of a prefab. If it is, we get the path to the prefab asset, load the prefab asset, and instantiate it. Otherwise, we instantiate a regular game object clone. After instantiating the object, set its position to the mouse position. To be a good editor citizen, we notify the Undo
system that we created a new object so Ctrl+Z will remove it.
Finally, we Use()
the current event. By the way, calling Use()
on events is extremely important because it notifies the other editor systems to ignore the event. For example, when we receive a MouseUp
event, we want to place an object, but the Scene View selects the object under the mouse by default. So, by calling Use()
, we tell the Scene View to ignore it instead of picking an object.
The last step in OnToolGUI
is to repaint the window.
//Force the window to repaint.
window.Repaint();
The reason is that the Scene View doesn’t automatically update at a real-time frame rate, like 60fps. Instead, it updates whenever it receives an event worthy of an update. We want our tool to feel responsive, so we’ll repaint as long as it’s the active tool.
Here’s the entire OnToolGUI
function.
public override void OnToolGUI(EditorWindow window)
{
//If we're not in the scene view, we're not the active tool, we don't have a placeable object, exit.
if (!(window is SceneView))
return;
if (!ToolManager.IsActiveTool(this))
return;
if (!HasPlaceableObject)
return;
//Draw a positional Handle.
Handles.DrawWireDisc(GetCurrentMousePositionInScene(), Vector3.up, 0.5f);
//If the user clicked, clone the selected object, place it at the current mouse position.
if (_receivedClickUpEvent)
{
var newObject = _prefabObjectField.value;
GameObject newObjectInstance;
if (PrefabUtility.IsPartOfAnyPrefab(newObject))
{
var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(newObject);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
newObjectInstance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
}
else
{
newObjectInstance = Instantiate((GameObject)newObject);
}
newObjectInstance.transform.position = GetCurrentMousePositionInScene();
Undo.RegisterCreatedObjectUndo(newObjectInstance, "Place new object");
Event.current.Use();
_receivedClickUpEvent = false;
}
//Force the window to repaint.
window.Repaint();
}
There’s a lot more to cover, but this is a good place for a break. Since the code in this post alone won’t be enough to compile, I shared the complete tool at the end. You can explore the finished code and try the tool while I write the next section.
So in this post, we learned about some useful APIs to build Editor tools, including EditorTool
, PrefabUtility
, Handles
and HandleUtility
. We learned how to differentiate between plain Game Objects and Prefabs. We also learned how to instantiate a linked Prefab. Finally, we learned how to draw GUI in the Scene View. Later we’ll go deeper into editor events, build a simple GUI with UIToolkit and create a custom context menu.
Check out the finished project here on Github. If you want to be notified when the next part is released, join my mailing list.
G3oX
Very good! Nice tutorial about an interesting topic. I have not touched it before, so I will follow all the next part of the tutorial to get deeper into it. As always, thanks for your time and for sharing your knowleges with us ๐
bronson
Thank you so much! I’m truly grateful to hear when my work is helpful! ๐
Nigel
Event.current.Use(); !!!
I was trying to work out how to stop other parts of Unity using events and couldn’t find anything. Had to come up with a dirty way of doing it. This alone makes the article worth while but the rest is also really helpful and well written and explained. Thanks so much.
bronson
You’re welcome! I’m happy to help! ๐
Jewel John
I’ve been searching for this topic my whole day and you’re the only one I found. Thank you! ๐
bronson
You’re welcome!! I’m happy to help ๐