
Unity で GameObject を配置/移動させる際に Commandキーを押しながらドラッグさせると、 現在位置から特定量ずつ の移動ができます(スナップ機能)。 例えば X軸のスナップ量を 16pxにすると、 現在の値が x: 6.4 である場合、 6.4、22.4、38.4、54.4…… という増え方をします。
移動量が固定できる、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 のグリッドにそうような移動ツールを作ります。
using UnityEngine; public class SnapSample : MonoBehaviour { }
↑このコンポーネントを持っている場合のエディタを拡張したいので、プロジェクトビューで 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 が使えます。 中身は対象となっているコンポーネントのインスタンスです。
シーンビューをカスタマイズ
白い矢印が生成された 標準の移動ツールは非表示になっている
・・・
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
で返します。 その位置ベクトルとハンドルの向きの内積を取り、更にハンドルの向きを掛けることで ハンドル方向の位置ベクトル が得られます。
得られた位置ベクトルを丸めることで、 グリッドにそった移動 が可能になります。
・・・
// ハンドル作成
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()
を使って変更したプロパティを登録する必要があります。
・・・
// 第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を選択した場合も移動できるようにして使いやすくしています。
マゼンタの正円がフリーハンドル、ドラッグすると平面移動できる
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) を持たせています。
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
と複数形になります。