RYO620
DESIGN & DEVELOPMENT
Unityエディタ拡張でキャラクターのグリッド移動ツールを作る
Ryosuke

Unityエディタ拡張でキャラクターのグリッド移動ツールを作る

Unity で GameObject を配置/移動させる際に Commandキーを押しながらドラッグさせると、 現在位置から特定量ずつ の移動ができます(スナップ機能)。 例えば X軸のスナップ量を 16pxにすると、 現在の値が x: 6.4 である場合、 6.4、22.4、38.4、54.4…… という増え方をします。

移動量が固定できる、Unity標準のスナップ機能移動量が固定できる、Unity標準のスナップ機能

ただ、2Dゲームを作る際に移動量固定ではなく、特定量の倍数値しか取らない移動をさせたい = 常に16 x 16pxのグリッドに対して移動させたい ような場合があり、また Commandキーを毎回押すのも面倒 なので、独自の移動ツールを作ってみました。

↑最終的にはこんな感じになります。


もくじ - Unityエディタ拡張 覚書全3回の1回目

  • 独自のグリッド移動ツールを作ろう
    • 特定のコンポーネントに対するエディタ拡張
    • シーンビューをカスタマイズ
    • ハンドル操作量を取得して丸める
    • Undo/Redo を使えるようにする
  • 完成形

第1回: Unityエディタ拡張でキャラクターのグリッド移動ツールを作る (この記事)
第2回: Unityエディタ拡張でシーンビュー上にGameObject生成ボタンUIを作る
第3回: Unityエディタ拡張で NPC の移動ルートを Gizmos を使って可視化し、 シーンビューで編集可能にする


独自のグリッド移動ツールを作ろう

特定のコンポーネントに対するエディタ拡張

今回は SnapSample というコンポーネントを持つ GameObject が、 16 x 16px のグリッドにそうような移動ツールを作ります。

Assets/Scripts/SnapSample.cs
using UnityEngine; public class SnapSample : MonoBehaviour { }

↑このコンポーネントを持っている場合のエディタを拡張したいので、プロジェクトビューで Editor ディレクトリを作成し、その中に SnapObjectEditor.cs を作成します。

Assets/Scripts/Editor/SnapObjectEditor.cs
using UnityEditor; using UnityEngine; [CustomEditor(typeof(SnapSample))] public class SnapSampleEditor : Editor { private SnapSample _instance; private readonly int GridSize = 16; private void OnEnable() { Debug.Log("OnEnable"); _instance = (SnapSample) target; } }

SnapObjectEditor は Editor クラスを継承し CustomEditor 属性でコンポーネントを指定すると、 そのコンポーネントを持つ GameObject がシーンビューなどで選択された場合に、 OnEnable() が呼ばれます。

Editorを継承することで、メンバ変数 target が使えます。 中身は対象となっているコンポーネントのインスタンスです。


シーンビューをカスタマイズ

白い矢印が生成された 標準の移動ツールは非表示になっている白い矢印が生成された 標準の移動ツールは非表示になっている

SnapObjectEditor.cs
・・・ private void OnSceneGUI() { // シーンビューでのツールを未選択にする(=非表示にするため) Tools.current = Tool.None; EditorGUI.BeginChangeCheck(); // ハンドル作成 Handles.Slider(_instance.transform.position, Vector3.up); if (EditorGUI.EndChangeCheck()) { Debug.Log("ハンドルによる操作が行われたよ"); } } ・・・

シーンビューの表示カスタマイズは OnSceneGUI() の中で行います。 まずは標準の移動ツールを非表示にして、独自移動ツールと被らないようにします。

Handles.Slider() で上向きのハンドル(矢印)を作成します。 第1引数はハンドルの位置、第2引数の Vector3.up は上向きを意味します。 そのハンドルをユーザが操作した場合に呼ばれる処理は、

EditorGUI.BeginChangeCheck(); // ハンドルを作成するコード if (EditorGUI.EndChangeCheck()) { // ハンドルが操作された場合に呼ばれる処理 }

上記のような if文 内に記述します。


ハンドル操作量を取得して丸める

Handles.Slider() は、ユーザがハンドルを掴んで操作した後の新しい位置ベクトルを Vector3 で返します。 その位置ベクトルとハンドルの向きの内積を取り、更にハンドルの向きを掛けることで ハンドル方向の位置ベクトル が得られます。

得られた位置ベクトルを丸めることで、 グリッドにそった移動 が可能になります。

SnapObjectEditor.cs
・・・ // ハンドル作成 var value = Handles.Slider(_instance.transform.position, Vector3.up); if (EditorGUI.EndChangeCheck()) { var dot = Vector2.Dot(value, Vector3.up); if ((Mathf.Abs(dot) <= Mathf.Epsilon)) return; // 移動量が小さい場合を除く // ハンドル方向の位置ベクトル var vec3 = dot * Vector3.up; // GameObjectは GridSize のグリッドにそって移動する _instance.transform.position = new Vector3( Mathf.RoundToInt(vec3.x / GridSize) * GridSize, Mathf.RoundToInt(vec3.y / GridSize) * GridSize, 0 ); } ・・・

Undo/Redo を使えるようにする

移動させることはできましたが、現状のままだと Undo が効きません。 プロパティを変更する前に、 Undo.RecordObject() を使って変更したプロパティを登録する必要があります。

SnapObjectEditor.cs
・・・ // 第1引数に変更したプロパティ、第2引数にヒストリーのラベル Undo.RecordObject(_instance.transform, "オブジェクトの移動"); _instance.transform.position = new Vector3( Mathf.RoundToInt(vec3.x / GridSize) * GridSize, Mathf.RoundToInt(vec3.y / GridSize) * GridSize, 0 ); ・・・

これで Undo/Redo が使えるようになります。


完成形

実際には 軸に依存しないフリーハンドル を使って平面を自由に移動できたり、 [CanEditMultipleObjects] 属性を使って複数のGameObjectを選択した場合も移動できるようにして使いやすくしています。

マゼンタの正円がフリーハンドル、ドラッグすると平面移動できるマゼンタの正円がフリーハンドル、ドラッグすると平面移動できる

SnapSample.cs
using UnityEngine; [SelectionBase] public class SnapSample : MonoBehaviour { // GameConfig.GridSize を1目盛りとした場合の、グリッド座標 [SerializeField] public Vector2Int gridPos = Vector2Int.zero; /// <summary> /// グリッド移動量を指定して移動する /// </summary> public void Move(Vector2Int vec2) { gridPos += vec2; transform.position = GetGlobalPosition(gridPos); } /// <summary> /// グリッド座標をGlobal座標に変換する /// </summary> public static Vector3 GetGlobalPosition(Vector2Int gridPos) { return new Vector3( gridPos.x * GameConfig.GridSize, gridPos.y * GameConfig.GridSize, 0); } }

↑ゲーム内でよく使うため、 SnapSample には グリッド座標(Vector2Int) を持たせています。

SnapObjectEditor.cs
using System.Linq; using UnityEditor; using UnityEngine; [CustomEditor(typeof(SnapSample))] [CanEditMultipleObjects] public class SnapSampleEditor : Editor { private SnapSample[] _instances; private Vector3 _center = Vector3.zero; private void OnEnable() { _instances = targets.Cast<SnapSample>().ToArray(); } /// <summary> /// シーンビューのGUI /// </summary> private void OnSceneGUI() { Tools.current = Tool.None; _center = GetCenterOfInstances(_instances); // フリーハンドル FreeHandle(); // X軸 AxisHandle(Color.red, Vector2Int.right); // Y軸 AxisHandle(Color.green, Vector2Int.up); } /// <summary> /// フリーハンドルの描画 /// </summary> private void FreeHandle() { Handles.color = Color.magenta; // フリー移動ハンドルの作成 EditorGUI.BeginChangeCheck(); var pos = Handles.FreeMoveHandle(_center, Quaternion.identity, 10f, Vector3.one, Handles.CircleHandleCap); if (EditorGUI.EndChangeCheck()) { MoveObject(pos - _center); } } /// <summary> /// 複数のインスタンスの中心を返す /// </summary> private static Vector3 GetCenterOfInstances(SnapSample[] instances) { float x = 0f, y = 0f; foreach (var ins in instances) { var position = ins.transform.position; x += position.x; y += position.y; } return new Vector3(x / instances.Length, y / instances.Length, 0); } /// <summary> /// 軸ハンドルの描画 /// </summary> private void AxisHandle(Color color, Vector2 direction) { // ハンドルの作成 Handles.color = color; EditorGUI.BeginChangeCheck(); var deltaMovement = Handles.Slider(_center, new Vector3(direction.x, direction.y, 0)) - _center; if (EditorGUI.EndChangeCheck()) { var dot = Vector2.Dot(deltaMovement, direction); if (!(Mathf.Abs(dot) > Mathf.Epsilon)) return; MoveObject(dot * direction); } } /// <summary> /// スナップしてオブジェクトを動かす /// </summary> private void MoveObject(Vector3 vec3) { var vec2 = new Vector2Int(Mathf.RoundToInt(vec3.x / GameConfig.GridSize), Mathf.RoundToInt(vec3.y / GameConfig.GridSize)); if (vec2 == Vector2.zero) return; foreach (var ins in _instances) { Object[] objects = {ins, ins.transform}; Undo.RecordObjects(objects, "オブジェクトの移動"); ins.Move(vec2); } } }

複数の GameObject を選択した場合のインスタンスは _instances = targets.Cast<SnapSample>().ToArray(); のようにして取得しています。 プロパティの変更方法は単体時と同じです。

単体時は transform の変更を Undo.RecordObject(_instance.transform, "オブジェクトの移動"); のように登録していました。 完成形では SnapSampleグリッド座標(Vector2Int) も同時に変更しているため、 Undo.RecordObjects(Object[], "オブジェクトの移動"); のように、 変更した全てのプロパティを配列にして登録します。 メソッド名も RecordObject ではなく RecordObjects と複数形になります。