Unity Custom Inspectors: Grab and Move an Enemy's Patrol Route in the Scene View

Created: 2026-09-12Last updated: 2026-10-10

Build an Inspector with CustomEditor, OnSceneGUI, and Handles that lets you grab and move an enemy's patrol route in the Scene view.

A row of points like an enemy's patrol route is much easier to lay out if you can grab and move it directly in the Scene view. Hold it as a Vector3 array, though, and every time you move one point, you retype numbers in the Inspector. After retyping, you press Play to check the position. With four points, that's twelve numbers to type.

Unity's built-in Colliders and lights let you grab corners and circles in the Scene view to change their shape. Write a CustomEditor, and you can give your own components the same handles.

This article explains how to move the points of a patrol route in the Scene view with CustomEditor and OnSceneGUI.

Twelve numbers in the Inspector side by side with a single polyline in the Scene view: two views of the same four points

What You'll Learn

  • A CustomEditor replaces the default Inspector display
  • Why the Inspector side rewrites through serializedObject while the Scene view side rewrites target
  • The steps for putting grabbable handles in the Scene view with OnSceneGUI and Handles
  • Hands-on: grab and move an enemy's patrol route in the Scene view

Tested with: Unity 6 (6000.x)

Sponsored

What a CustomEditor Is: One Class for the Inspector and the Scene View

You can replace how your own component is displayed by writing one class that inherits Editor. Give the class [CustomEditor(typeof(PatrolRoute))]. Now that class handles the display when PatrolRoute is selected.

PatrolRouteEditor's OnInspectorGUI draws the Inspector, and OnSceneGUI draws the handles in the Scene view

It handles two places. OnInspectorGUI() draws the Inspector panel. OnSceneGUI() draws handles in the Scene view. Both are written in the same class. Put this script in an Editor folder so it isn't included in builds. The rules for where to put it are covered in Unity Editor Extensions.

The first stumbling block is that the moment you write OnInspectorGUI(), the default field display disappears. This method is written with override, a way of replacing the parent class's processing with your own, so the Inspector is "replaced," not "added to." If you just want to add a button, call DrawDefaultInspector() first to have it draw the original items.

By default Script and Waypoints are listed; writing OnInspectorGUI makes them disappear, and DrawDefaultInspector brings them back
// Assets/Editor/PatrolRouteEditor.cs
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PatrolRoute))]
public class PatrolRouteEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();          // Draw the default items as-is

        if (GUILayout.Button("Reset Route"))
            Debug.Log("reset");          // Then add your own controls
    }
}

PatrolRoute is the "component that holds a patrol route" built in the hands-on section. If you're following along, create a class with just public Vector3[] waypoints; first.

In other engines: Godot's EditorInspectorPlugin and Unreal Engine's Details Panel Customization (IDetailCustomization) are similar: mechanisms for adding your own UI to a target class's Inspector.

Drawing the Inspector: serializedObject

When rewriting values in the Inspector, you can rewrite target (the currently selected component) directly. But just rewriting it directly doesn't automatically give you three features: undoing with Ctrl+Z, recording a Prefab's "values for just this object," and applying to multiple selected objects at once.

Going through serializedObject gives you Undo, Prefab overrides, and multi-selection; just touching target directly gives you none of the three

serializedObject is a window onto a copy of the component's values made for the Inspector. The Inspector reads and writes through this window, and along the way Unity takes care of the three features.

[CustomEditor(typeof(PatrolRoute))]
[CanEditMultipleObjects]                 // Use this Inspector even when multiple objects are selected
public class PatrolRouteEditor : Editor
{
    SerializedProperty waypointsProp;

    void OnEnable()
    {
        waypointsProp = serializedObject.FindProperty("waypoints");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();                            // ① Reread the current values
        EditorGUILayout.PropertyField(waypointsProp, true);   // true draws the array's contents too
        serializedObject.ApplyModifiedProperties();           // ② Write them back
    }
}

What you pass to FindProperty() isn't the Inspector's display name but the C# field name. Even though it displays as Waypoints, you pass waypoints, matching case exactly. If the name isn't found, it returns null. Pass that to PropertyField and it throws, and nothing below it gets drawn.

[CanEditMultipleObjects] is an attribute declaring support for multiple selection. Without it, selecting two shows only Multi-object editing not supported. in that area.

Sponsored

OnSceneGUI and Handles: Grabbable Handles in the Scene View

Things you can grab in the Scene view are called handles. The square dots at a Collider's corners and the circle around a light's range are examples. Putting handles on your own component is what OnSceneGUI() does.

Lines drawn with OnDrawGizmos are only visible and can't be grabbed, while handles from OnSceneGUI can be grabbed and moved. OnSceneGUI only appears while selected

Lines drawn with Gizmos' OnDrawGizmos are only visible; you can't grab them. OnSceneGUI() is called only while that component is selected, and handles shown here can be grabbed with the mouse. Unlike OnInspectorGUI(), it's written without override.

To show one handle and write the value back, write it in this order.

EditorGUI.BeginChangeCheck();                          // ① Start watching for movement
Vector3 moved = Handles.PositionHandle(world, Quaternion.identity);   // ② Draw the handle
if (EditorGUI.EndChangeCheck())                        // ③ Go inside only when it moved
{
    Undo.RecordObject(route, "Move Waypoint");         // ④ Record before assigning
    route.waypoints[i] = tr.InverseTransformPoint(moved);   // ⑤ Then assign
}
A five-step order diagram: BeginChangeCheck, PositionHandle, EndChangeCheck, Undo.RecordObject, then assign

Undo.RecordObject() notes the values at the moment it's called. Call it after assigning, and the change isn't recorded. Skip it and just assign to target, and Ctrl+Z doesn't work. The scene name in the Hierarchy doesn't get the unsaved * either. During a drag, EndChangeCheck() returns true many times. Even so, one drag becomes one Ctrl+Z.

Unlike the Inspector side, you don't use serializedObject in here. After Undo.RecordObject, rewrite target directly. Unity's official documentation also writes OnSceneGUI using target.

PlaceHow to rewrite values
OnInspectorGUIserializedObject → PropertyField → ApplyModifiedProperties
OnSceneGUIUndo.RecordObject → assign directly to target

Local and World Coordinates

What Handles takes and returns is normally world coordinates (where something is in the whole scene). Patrol route points, on the other hand, are easier to work with in local coordinates (where something is as seen from this enemy).

Held in world coordinates, the route stays behind when the enemy is placed elsewhere; held in local coordinates, the route comes along with it

Keep them in world coordinates, and when you make the enemy a Prefab and place it in another room, only the route is left behind at the original location. In local coordinates, moving the enemy brings the route along.

To get the best of both, convert back and forth. Right before passing to Handles, go out to world with transform.TransformPoint(). Before saving, come back to local with transform.InverseTransformPoint().

In other engines: For grabbable handles in the scene, Godot's EditorNode3DGizmoPlugin and Unreal Engine's Component Visualizer (FComponentVisualizer) play similar roles.

Sponsored

Hands-On: Grab and Move the Patrol Route in the Scene

An enemy's patrol in a top-down action game, a guard's rounds in a stealth game, a lap course in a racing game — data that holds a loop of connected points in space as an array shows up in every genre. Here we make an enemy's patrol route fixable by dragging in the Scene view.

Twelve numbers from Element 0 to 3 in the Inspector on the left, and a single polyline around the enemy drawn in the Scene view on the right

① What to place

  1. In a new scene, place a floor with GameObject > 3D Object > Plane
  2. Place one Cube with GameObject > 3D Object > Cube. Name it Enemy, with Position (0, 0.5, 0)
  3. As an obstacle, place another Cube with Position (2, 0.5, 0)
  4. Create Assets/Scripts/PatrolRoute.cs and Assets/Editor/PatrolRouteEditor.cs with the code in ② below. If you wrote PatrolRouteEditor in the previous section, rewrite that file. Two files with the same class name won't compile
  5. Once compiling finishes, attach PatrolRoute to Enemy with Add Component. If you already attached it in the previous section, choose Reset from the component's ⋮ to return the array to its initial four points

② Assemble it

First, the component that holds the route. It isn't an editor extension, so put it outside the Editor folder.

// Assets/Scripts/PatrolRoute.cs
using UnityEngine;

public class PatrolRoute : MonoBehaviour
{
    // Held relative to the enemy. Move the enemy and the route comes along
    public Vector3[] waypoints =
    {
        new Vector3(-2f, 0f, -2f),
        new Vector3( 2f, 0f, -2f),
        new Vector3( 2f, 0f,  2f),
        new Vector3(-2f, 0f,  2f),
    };

    // Keep the path visible even when not selected
    void OnDrawGizmos()
    {
        if (waypoints == null || waypoints.Length < 2) return;

        Gizmos.color = Color.cyan;
        for (int i = 0; i < waypoints.Length; i++)
        {
            Vector3 a = transform.TransformPoint(waypoints[i]);
            Vector3 b = transform.TransformPoint(waypoints[(i + 1) % waypoints.Length]);   // Back from the last point to the first, making a loop
            Gizmos.DrawLine(a, b);
            Gizmos.DrawSphere(a, 0.15f);
        }
    }
}

Next, the editor side. This class handles both the Inspector and the Scene view. Put it in Assets/Editor/.

// Assets/Editor/PatrolRouteEditor.cs
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PatrolRoute))]
[CanEditMultipleObjects]
public class PatrolRouteEditor : Editor
{
    SerializedProperty waypointsProp;

    void OnEnable()
    {
        waypointsProp = serializedObject.FindProperty("waypoints");
    }

    // Inspector side: go through serializedObject
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.HelpBox("Drag the points in the Scene view to move them", MessageType.Info);
        EditorGUILayout.PropertyField(waypointsProp, true);
        serializedObject.ApplyModifiedProperties();
    }

    // Scene view side: rewrite target after Undo.RecordObject
    void OnSceneGUI()
    {
        PatrolRoute route = (PatrolRoute)target;   // Each call handles just one object
        if (route.waypoints == null) return;       // Don't crash even if the array is emptied

        Transform tr = route.transform;

        for (int i = 0; i < route.waypoints.Length; i++)
        {
            Vector3 world = tr.TransformPoint(route.waypoints[i]);   // Local → world

            Handles.Label(world + Vector3.up * 0.4f, $"WP {i}");

            EditorGUI.BeginChangeCheck();
            Vector3 moved = Handles.PositionHandle(world, Quaternion.identity);
            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(route, "Move Waypoint");                // Record before assigning
                route.waypoints[i] = tr.InverseTransformPoint(moved);     // World → local
                PrefabUtility.RecordPrefabInstancePropertyModifications(route);
            }
        }
    }
}

The last line is for when the enemy is placed from a Prefab. When you rewrite target directly, unless you call this, it isn't recorded as "values for just this instance." When you save and reopen the scene, it goes back to the Prefab's route. What you pass is the component holding waypoints, not the GameObject.

③ Run it and check

Select Enemy in the Hierarchy. The note "Drag the points in the Scene view to move them" appears in the Inspector's Patrol Route area. Expand Waypoints below it. There are three things to check.

First, move handles appear on the four points, with the labels WP 0 through WP 3 above them. The route is a square with sides of 4. The right edge joining WP 1 and WP 2 runs straight through the box at (2, 0.5, 0). If no handles appear, check that Gizmos at the top of the Scene view is on, and that PatrolRoute is checked in the dropdown next to it. While it's off, OnSceneGUI itself isn't called.

Next, grab the red arrow (the x direction) on WP 1 and WP 2, and drag them outward until x is about 4. The cyan polyline goes around the box on the spot. The x values of Element 1 and Element 2 in the Inspector change along with it. Release, then press Ctrl+Z, and only WP 2, the last one you dragged, goes back. One drag is one Ctrl+Z. Once you've checked, redo it with Ctrl+Y (Cmd+Shift+Z on macOS).

In the initial state the right edge runs through the box; dragging WP 1 and WP 2 outward makes the polyline go around the box

To finish, try changing Enemy's Position to (-3, 0.5, 0). The route keeps its shape and moves along with it, detour around the box included. Not a single Element value in the Inspector changes. That's because the points are held as positions seen from the enemy. What used to be retyping twelve numbers has become grabbing and moving points.

What this code builds is only the ability to edit the path. Even if you press Play, the enemy doesn't walk yet.

There are two key points.

  • Write differently depending on the place: the Inspector side goes through serializedObject. The Scene view side assigns directly to target after Undo.RecordObject
  • Hold points in local coordinates: go out to world with TransformPoint, and come back to local with InverseTransformPoint. Move the enemy and the route comes along. The walking side continues in moving AI with NavMesh
Sponsored

Troubleshooting

SymptomCauseFix
I wrote a CustomEditor, but nothing about PatrolRoute's display changedThe X in [CustomEditor(typeof(X))] names a different class, so it attached to that class insteadMake the name inside typeof match the target component's class name
Trying to grab a point moves the enemy itselfOn screen, the point's arrow overlaps the standard Move tool's arrow, and you grabbed that onePlace the points away from the body, or zoom in and grab from an angle where the arrows don't overlap
Points moved during Play go back when I stopChanges to the scene during Play are thrown away when you stop PlayEdit points after stopping Play
When the enemy walks, the route moves or rotates along with itThe route is held in coordinates relative to the walking enemy itself. As the enemy advances, so do the destinations; when it turns, so does the routeAttach PatrolRoute to an empty GameObject that doesn't walk, separate from the enemy. If you make it a Prefab, make that empty GameObject and the enemy children of the same parent

Bonus: Good to Know First

  • To change only how one field looks, use a PropertyDrawer: it replaces how a single field of a specific type, or with a specific attribute, is drawn. An example like a homemade [ReadOnly] attribute is in Unity Attributes
  • You can also make the points empty child objects: put an empty GameObject at each point and hold them as Transform[] instead of Vector3[], and you can select points in the Hierarchy and move them with the standard Move tool. If you don't want the Hierarchy filling up as points multiply, hold them in an array with handles, as this article does
  • As points multiply, grab them with small dots: past 10 points, the arrows overlap and you can't tell which one you're grabbing. Normally use small Handles.FreeMoveHandle dots, and show arrows only for the one point you've selected. It feels close to the built-in "Edit Collider"

Summary

  • A CustomEditor replaces things: to keep the default items, call DrawDefaultInspector() first
  • The Inspector side uses serializedObject: Undo, Prefab overrides, and multi-selection all come with it
  • The Scene view side assigns directly to target: call Undo.RecordObject before that
  • Handles go watch → record → assign: after confirming it moved, write Undo.RecordObject, then the assignment
  • Hold spatial coordinates locally and grab them in the scene: twelve numbers become one polyline

Where in your project are you typing Vector3 arrays into the Inspector?

Read nextOn to Animation
Animation

Getting Started with Unity Animator Controller: Mastering Animation State Control

Read this note
Unity Notes in this section100