【Unity】Unityカスタムインスペクター入門:敵の巡回ルートを、シーンビューで掴んで動かす

作成: 2026-09-12最終更新: 2026-10-10

CustomEditorとOnSceneGUI・Handlesで、敵の巡回ルートをシーンビューで掴んで動かせるインスペクターを作ります。

敵の巡回ルートのような点の並びは、Scene ビューで直接つかんで動かせると、配置の調整がずっと楽になります。ところが Vector3 の配列で持たせると、点を1つ動かすたびに、Inspector の数字を打ち直します。打ち直したら Play して、位置を確かめます。4点あれば、打つ数字は12個です。

Unity 純正の Collider やライトは、シーンビューで角や円を掴んで形を変えられます。同じ取っ手は、CustomEditor を書けば自作のコンポーネントにも出せます。

この記事では、CustomEditor と OnSceneGUI で、巡回ルートの点をシーンビューで動かす方法を解説します。

インスペクターの12個の数字と、シーンビューの折れ線1本を並べた図。同じ4点の2つの見え方

この記事でわかること

  • CustomEditor は、既定のインスペクター表示を置き換えるものであること
  • インスペクター側は serializedObject、シーンビュー側は target を書き換える理由
  • OnSceneGUI と Handles で、シーンビューに掴める取っ手を出す手順
  • 実践:敵の巡回ルートを、シーンビューで掴んで動かす

動作確認環境: Unity 6 (6000.x)

Sponsored

CustomEditorとは:インスペクターとシーンビューを受け持つクラス

自作のコンポーネントの見せ方は、Editor を継承したクラスを1つ書くと差し替えられます。クラスには [CustomEditor(typeof(PatrolRoute))] を付けます。これで、PatrolRoute を選んだときの表示を、そのクラスが受け持ちます。

PatrolRouteEditorのOnInspectorGUIがインスペクターを、OnSceneGUIがシーンビューの取っ手を描く図

受け持つ場所は2つです。OnInspectorGUI() は、インスペクターのパネルを描きます。OnSceneGUI() は、シーンビューに取っ手を描きます。どちらも同じクラスに書きます。このスクリプトは、ビルドに入らないよう Editor フォルダに置きます。置き場所のルールは エディタ拡張入門 にまとめてあります。

最初につまずくのは、OnInspectorGUI() を書いた瞬間に、既定のフィールド表示が消えることです。このメソッドは override で書きます。親クラスの処理を自分の処理で置き換える書き方なので、インスペクターは「足す」のではなく「置き換える」ことになります。ボタンを1つ足したいだけなら、最初に DrawDefaultInspector() を呼んで、元の項目を描かせます。

既定ならScriptとWaypointsが並び、OnInspectorGUIを書くと消え、DrawDefaultInspectorで戻る図
// Assets/Editor/PatrolRouteEditor.cs
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PatrolRoute))]
public class PatrolRouteEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();          // 既定の項目をそのまま描く

        if (GUILayout.Button("Reset Route"))
            Debug.Log("reset");          // そのあとに自分の部品を足す
    }
}

PatrolRoute は、実践で作る「巡回ルートを持つコンポーネント」です。手を動かしながら読むなら、先に public Vector3[] waypoints; だけを持つクラスを作っておきます。

他のエンジンでいうと: Godot の EditorInspectorPlugin、Unreal Engine の Details Panel Customization(IDetailCustomization)が近く、対象のクラスのインスペクターに独自の UI を加える仕組みです。

インスペクターの描画:serializedObject

インスペクターで値を書き換えるとき、target(いま選んでいるコンポーネント)を直接書き換えることもできます。ただ、直接書き換えるだけでは、3つの機能が自動では付きません。Ctrl+Z で戻すこと、Prefab の「このオブジェクトだけの値」の記録、複数選択への一括反映です。

serializedObject経由ならUndo・Prefabの差分・複数選択の3つが付き、targetを直接触るだけでは3つとも付かない対比図

serializedObject は、コンポーネントの値をインスペクター用に写し取った窓口です。インスペクターはこの窓口越しに読み書きし、その途中で Unity が3つの機能を済ませてくれます。

[CustomEditor(typeof(PatrolRoute))]
[CanEditMultipleObjects]                 // 複数選択したときも、このインスペクターを使う
public class PatrolRouteEditor : Editor
{
    SerializedProperty waypointsProp;

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

    public override void OnInspectorGUI()
    {
        serializedObject.Update();                            // ① 現在値を読み直す
        EditorGUILayout.PropertyField(waypointsProp, true);   // true で配列の中身まで描く
        serializedObject.ApplyModifiedProperties();           // ② 書き戻す
    }
}

FindProperty() に渡すのは、インスペクターの表示名ではなく、C# のフィールド名です。表示は Waypoints でも、渡すのは waypoints で、大文字小文字までそろえます。名前が見つからないと null が返ります。それを PropertyField に渡すと例外になり、それより下が描かれません。

[CanEditMultipleObjects] は、複数選択への対応を宣言する属性です。付けずに2体選ぶと、その欄には Multi-object editing not supported. とだけ出ます。

Sponsored

OnSceneGUIとHandles:シーンビューの取っ手

シーンビューで掴める取っ手のことを、ハンドルと呼びます。Collider の角にある四角い点や、ライトの届く範囲を囲む円がそれです。自分のコンポーネントにハンドルを出すのが、OnSceneGUI() です。

OnDrawGizmosの線は見えるだけで掴めず、OnSceneGUIの取っ手は掴んで動かせる対比図。OnSceneGUIは選んでいるあいだだけ出る

Gizmos の OnDrawGizmos で描いた線は、見えるだけで掴めません。OnSceneGUI() は、そのコンポーネントを選んでいるあいだだけ呼ばれ、ここで出したハンドルはマウスで掴めます。OnInspectorGUI() と違い、override は付けずに書きます。

ハンドルを1つ出して値を書き戻すときは、次の順番で書きます。

EditorGUI.BeginChangeCheck();                          // ① 動いたかの監視を始める
Vector3 moved = Handles.PositionHandle(world, Quaternion.identity);   // ② 取っ手を描く
if (EditorGUI.EndChangeCheck())                        // ③ 動いたときだけ中へ
{
    Undo.RecordObject(route, "Move Waypoint");         // ④ 代入より前に記録する
    route.waypoints[i] = tr.InverseTransformPoint(moved);   // ⑤ それから代入する
}
BeginChangeCheck、PositionHandle、EndChangeCheck、Undo.RecordObject、代入の5段の順序図

Undo.RecordObject() は、呼んだ時点の値を控えておく仕組みです。代入のあとに呼ぶと、変化が記録されません。呼ばずに target へ代入するだけだと、Ctrl+Z が効きません。Hierarchy のシーン名に、未保存の * も付きません。ドラッグ中は EndChangeCheck() が何度も true を返します。それでも、ドラッグ1回は Ctrl+Z 1回にまとまります。

インスペクター側と違い、この中では serializedObject を使いません。Undo.RecordObject のあとで、target を直接書き換えます。Unity の公式ドキュメントも、OnSceneGUI では target を使う形で書いています。

場所値の書き換え方
OnInspectorGUIserializedObject → PropertyField → ApplyModifiedProperties
OnSceneGUIUndo.RecordObject → target に直接代入

ローカル座標とワールド座標

Handles が受け取り、返すのは、ふだんはワールド座標(シーン全体の中でどこか)です。一方、巡回ルートの点は、ローカル座標(この敵から見てどこか)で持つほうが扱いやすくなります。

ワールド座標で持つと敵を別の場所に置いたときルートだけ残り、ローカル座標で持つとルートが一緒に付いてくる対比図

ワールド座標のまま持つと、敵を Prefab にして別の部屋に置いたとき、ルートだけが元の場所に取り残されます。ローカル座標なら、敵を動かすとルートが一緒に付いてきます。

両方を生かすには、往復させます。Handles へ渡す直前に、transform.TransformPoint() でワールドへ出します。保存する前に、transform.InverseTransformPoint() でローカルへ戻します。

他のエンジンでいうと: シーン上の掴める取っ手は、Godot の EditorNode3DGizmoPlugin、Unreal Engine の Component Visualizer(FComponentVisualizer)が近い役割です。

Sponsored

実践:巡回ルートをシーンで掴んで動かす

見下ろしアクションの敵の巡回、ステルスゲームの警備員の見回り、レースゲームの周回コース——空間の点をつないだ輪を配列で持つデータは、ジャンルを問わず出てきます。ここでは敵の巡回ルートを、シーンビューでドラッグして直せるようにします。

左のインスペクターにElement 0〜3の12個の数字、右のシーンビューに敵を囲む折れ線が1本描かれた図

① 置くもの

  1. 新しいシーンに GameObject > 3D Object > Plane で床を置きます
  2. GameObject > 3D Object > Cube で Cube を1つ置きます。名前は Enemy、Position は (0, 0.5, 0) です
  3. 障害物として、もう1つ Cube を置き、Position を (2, 0.5, 0) にします
  4. Assets/Scripts/PatrolRoute.cs と Assets/Editor/PatrolRouteEditor.cs を、下の②のコードで作ります。前の節で PatrolRouteEditor を書いていたら、そのファイルを書き換えます。同じクラス名のファイルが2つあると、コンパイルが通りません
  5. コンパイルが終わったら、Enemy に Add Component で PatrolRoute を付けます。前の節ですでに付けていたら、コンポーネントの︙から Reset を選び、配列を初期の4点に戻します

② 組み立て

まず、ルートを持つコンポーネントです。エディタ拡張ではないので、Editor フォルダの外に置きます。

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

public class PatrolRoute : MonoBehaviour
{
    // 敵からの相対座標で持つ。敵を動かせばルートも一緒に付いてくる
    public Vector3[] waypoints =
    {
        new Vector3(-2f, 0f, -2f),
        new Vector3( 2f, 0f, -2f),
        new Vector3( 2f, 0f,  2f),
        new Vector3(-2f, 0f,  2f),
    };

    // 選んでいないときも経路が見えるようにしておく
    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]);   // 最後の点から最初の点へ戻して輪にする
            Gizmos.DrawLine(a, b);
            Gizmos.DrawSphere(a, 0.15f);
        }
    }
}

次が、エディタ側です。インスペクターとシーンビューの両方を、このクラスが受け持ちます。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");
    }

    // インスペクター側:serializedObject を通す
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.HelpBox("シーンビューで点をドラッグして動かせます", MessageType.Info);
        EditorGUILayout.PropertyField(waypointsProp, true);
        serializedObject.ApplyModifiedProperties();
    }

    // シーンビュー側:Undo.RecordObject のあとで target を書き換える
    void OnSceneGUI()
    {
        PatrolRoute route = (PatrolRoute)target;   // その回の対象は1体だけ
        if (route.waypoints == null) return;       // 配列を空にしても落ちないように

        Transform tr = route.transform;

        for (int i = 0; i < route.waypoints.Length; i++)
        {
            Vector3 world = tr.TransformPoint(route.waypoints[i]);   // ローカル → ワールド

            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");                // 代入より前に記録
                route.waypoints[i] = tr.InverseTransformPoint(moved);     // ワールド → ローカル
                PrefabUtility.RecordPrefabInstancePropertyModifications(route);
            }
        }
    }
}

最後の1行は、敵を Prefab から置いたときのためのものです。target を直接書き換えたときは、これを呼ばないと「このインスタンスだけの値」として記録されません。シーンを保存して開き直したときに、Prefab 側のルートへ戻ります。渡すのは GameObject ではなく、waypoints を持つコンポーネントです。

③ 動かして確かめる

Hierarchy で Enemy を選びます。インスペクターの Patrol Route 欄に「シーンビューで点をドラッグして動かせます」の案内が出ます。その下の Waypoints を開いておきます。見るのは3つです。

まず、4つの点に移動ハンドルが出て、点の上に WP 0 から WP 3 のラベルが付きます。ルートは一辺4の正方形です。WP 1 と WP 2 を結ぶ右の辺が、(2, 0.5, 0) の箱を突き抜けています。ハンドルが出ないなら、Scene ビュー上部の Gizmos がオンか、その横のドロップダウンで PatrolRoute にチェックが入っているかを見ます。オフのあいだは、OnSceneGUI そのものが呼ばれません。

次に、WP 1 と WP 2 の赤い矢印(x 方向)を掴み、x が4くらいになるまで外へドラッグします。水色の折れ線が、その場で箱を回り込みます。インスペクターの Element 1 と Element 2 の x も、一緒に変わります。手を離してから Ctrl+Z を押すと、最後にドラッグした WP 2 だけが元へ戻ります。ドラッグ1回が、Ctrl+Z 1回です。確かめたら Ctrl+Y で戻し直します(macOS は Cmd+Shift+Z)。

初期状態では右の辺が箱を突き抜け、WP 1とWP 2を外へドラッグすると折れ線が箱を回り込む図

仕上げに、Enemy の Position を (-3, 0.5, 0) に変えてみてください。ルートは形を保ったまま、箱を回り込んだ形ごと一緒に移動します。インスペクターの Element の値は1つも変わりません。点を敵から見た位置で持っているからです。数字12個を打ち直していた作業が、点を掴んで動かす作業に変わりました。

このコードが作るのは、経路を編集する機能だけです。Play しても、敵はまだ歩きません。

ポイントは2つです。

  • 場所で書き分ける: インスペクター側は serializedObject を通します。シーンビュー側は、Undo.RecordObject のあとで target へ直接代入します
  • 点はローカル座標で持つ: TransformPoint でワールドへ出し、InverseTransformPoint でローカルへ戻します。敵を動かすと、ルートが付いてきます。歩かせる側は NavMeshでAIを動かす へ続きます
Sponsored

うまくいかないときは

症状原因直し方
CustomEditor を書いたのに、PatrolRoute の表示が何も変わらない[CustomEditor(typeof(X))] の X に別のクラスを書いていて、そちらのクラスに付いているtypeof の中を、対象のコンポーネントのクラス名にそろえる
点を掴もうとすると、敵本体が動く画面上で、点の矢印と標準の Move ツールの矢印が重なり、そちらを掴んでいる点を本体から離して置くか、視点を寄せて矢印が重ならない向きから掴む
Play 中に動かした点が、止めると元に戻るPlay 中のシーンの変更は、Play を止めた時点で捨てられる点の編集は Play を止めてから行う
敵を歩かせたら、ルートまで一緒に動いたり回ったりするルートを、歩く敵自身から見た座標で持っている。敵が進めば目的地も進み、向きを変えればルートも回るPatrolRoute を歩かない空の GameObject に付け、敵と分ける。Prefab にするなら、その空の GameObject と敵を同じ親の子にする

おまけ:先に知っておくと良いこと

  • 1つの項目の見た目だけ変えるなら PropertyDrawer: 特定の型や属性が付いたフィールド1つだけの描き方を差し替えます。自作の [ReadOnly] 属性のような例は Attribute入門 にあります
  • 点を空の子オブジェクトにする手もある: 点ごとに空の GameObject を置き、Vector3[] の代わりに Transform[] で持てば、Hierarchy で点を選んで標準の移動ツールで動かせます。点が増えて Hierarchy が埋まるのが嫌なら、この記事のように配列とハンドルで持ちます
  • 点が増えてきたら、小さな点で掴む: 点が10個を超えると、矢印が重なってどれを掴んでいるか分からなくなります。ふだんは Handles.FreeMoveHandle の小さな点にして、選んだ1点だけ矢印を出します。純正の「Edit Collider」に近い手触りです

まとめ

  • CustomEditor は置き換え: 既定の項目を残したいなら、DrawDefaultInspector() を先に呼ぶ
  • インスペクター側は serializedObject: Undo・Prefab の差分・複数選択が、まとめて付いてくる
  • シーンビュー側は target に直接代入: その前に Undo.RecordObject を呼ぶ
  • ハンドルは監視 → 記録 → 代入の順: 動いたのを確かめてから、Undo.RecordObject、代入の順に書く
  • 空間の座標はローカルで持ってシーンで掴む: 数字12個が、折れ線1本になる

自分のプロジェクトで、Vector3 の配列を Inspector に打ち込んでいる場所はどこでしょうか。

次に読む次のカテゴリ「アニメーション」へ
アニメーション

Unity Animator Controller入門:アニメーション状態を自在に制御する

この記事を読む
Unity このセクションのノート100