敵の巡回ルートのような点の並びは、Scene ビューで直接つかんで動かせると、配置の調整がずっと楽になります。ところが Vector3 の配列で持たせると、点を1つ動かすたびに、Inspector の数字を打ち直します。打ち直したら Play して、位置を確かめます。4点あれば、打つ数字は12個です。
Unity 純正の Collider やライトは、シーンビューで角や円を掴んで形を変えられます。同じ取っ手は、CustomEditor を書けば自作のコンポーネントにも出せます。
この記事では、CustomEditor と OnSceneGUI で、 巡回ルートの点をシーンビューで動かす方法を解説します。
この記事でわかること
CustomEditorは、既定のインスペクター表示を置き換えるものであること- インスペクター側は
serializedObject、シーンビュー側はtargetを書き換える理由OnSceneGUIとHandlesで、シーンビューに掴める取っ手を出す手順- 実践:敵の巡回ルートを、シーンビューで掴んで動かす
動作確認環境: Unity 6 (6000.x)
CustomEditorとは:インスペクターとシーンビューを受け持つクラス
自作のコンポーネントの見せ方は、Editor を継承したクラスを1つ書くと差し替えられます。クラスには [CustomEditor(typeof(PatrolRoute))] を付けます。これで、PatrolRoute を選んだときの表示を、そのクラスが受け持ちます。

受け持つ場所は2つです。OnInspectorGUI() は、インスペクターのパネルを描きます。OnSceneGUI() は、シーンビューに取っ手を描きます。どちらも同じクラスに書きます。このスクリプトは、ビルドに入らないよう Editor フォルダに置きます。置き場所のルールは エディタ拡張入門 にまとめてあります。
最初につまずくのは、OnInspectorGUI() を書いた瞬間に、既定のフィールド表示が消えることです。このメソッドは override で書きます。親クラスの処理を自分の処理で置き換える書き方なので、インスペクターは「足す」のではなく「置き換える」ことになります。ボタンを1つ足したいだけなら、最初に 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 は、コンポーネントの値をインスペクター用に写し取った窓口です。インスペクターはこの窓口越しに読み書きし、その途中で 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. とだけ出ます。
OnSceneGUIとHandles:シーンビューの取っ手
シーンビューで掴める取っ手のことを、ハンドルと呼びます。Collider の角にある四角い点や、ライトの届く範囲を囲む円がそれです。自分のコンポーネントにハンドルを出すのが、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); // ⑤ それから代入する
}

Undo.RecordObject() は、呼んだ時点の値を控えておく仕組みです。代入のあとに呼ぶと、変化が記録されません。呼ばずに target へ代入するだけだと、Ctrl+Z が効きません。Hierarchy のシーン名に、未保存の * も付きません。ドラッグ中は EndChangeCheck() が何度も true を返します。それでも、ドラッグ1回は Ctrl+Z 1回にまとまります。
インスペクター側と違い、この中では serializedObject を使いません。Undo.RecordObject のあとで、target を直接書き換えます。Unity の公式ドキュメントも、OnSceneGUI では target を使う形で書いています。
| 場所 | 値の書き換え方 |
|---|---|
OnInspectorGUI | serializedObject → PropertyField → ApplyModifiedProperties |
OnSceneGUI | Undo.RecordObject → target に直接代入 |
ローカル座標とワールド座標
Handles が受け取り、返すのは、ふだんはワールド座標(シーン全体の中でどこか)です。一方、巡回ルートの点は、ローカル座標(この敵から見てどこか)で持つほうが扱いやすくなります。

ワールド座標のまま持つと、敵を Prefab にして別の部屋に置いたとき、ルートだけが元の場所に取り残されます。ローカル座標なら、敵を動かすとルートが一緒に付いてきます。
両方を生かすには、往復させます。Handles へ渡す直前に、transform.TransformPoint() でワールドへ出します。保存する前に、transform.InverseTransformPoint() でローカルへ戻します。
他のエンジンでいうと: シーン上の掴める取っ手は、Godot の EditorNode3DGizmoPlugin、Unreal Engine の Component Visualizer(
FComponentVisualizer)が近い役割です。
実践:巡回ルートをシーンで掴んで動かす
見下ろしアクションの敵の巡回、ステルスゲームの警備員の見回り、レースゲームの周回コース——空間の点をつないだ輪を配列で持つデータは、ジャンルを問わず出てきます。ここでは敵の巡回ルートを、シーンビューでドラッグして直せるよう にします。

① 置くもの
- 新しいシーンに
GameObject > 3D Object > Planeで床を置きます GameObject > 3D Object > Cubeで Cube を1つ置きます。名前は Enemy、Position は(0, 0.5, 0)です- 障害物として、もう1つ Cube を置き、Position を
(2, 0.5, 0)にします Assets/Scripts/PatrolRoute.csとAssets/Editor/PatrolRouteEditor.csを、下の②のコードで作ります。前の節でPatrolRouteEditorを書いていたら、そのファイルを書き換えます。同じクラス名のファイルが2つあると、コンパイルが通りません- コンパイルが終わったら、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)。

仕上げに、Enemy の Position を (-3, 0.5, 0) に変えてみてください。ルートは形を保ったまま、箱を回り込んだ形ごと一緒に移動します。インスペクターの Element の値は1つも変わりません。点を敵から見た位置で持っているからです。数字12個を打ち直していた作業が、点を掴んで動かす作業に変わりました。
このコードが作るのは、経路を編集する機能だけです。Play しても、敵はまだ歩きません。
ポイントは2つです。
- 場所で書き分ける: インスペクター側は
serializedObjectを通します。シーンビュー側は、Undo.RecordObjectのあとでtargetへ直接代入します - 点はローカル座標で持つ:
TransformPointでワールドへ出し、InverseTransformPointでローカルへ戻します。敵を動かすと、ルートが付いてきます。歩かせる側は NavMeshでAIを動かす へ続きます
うまくいかないときは
| 症状 | 原因 | 直し方 |
|---|---|---|
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 に打ち込んでいる場所はどこでしょうか。