2017年12月4日

Unity 意外と楽しいエディター拡張 ~基本編~


最近エディター拡張に少しハマっていたので、忘れないうちに復習がてらの解説。まずはインスペクタにボタンを表示させる方法をメモメモ…


※このページの内容の動作確認にはUnity5.6を使用しています。
インスペクタ上で変数の値を編集していて「配列やリストの要素がいっぱい表示されて見にくいなぁ」とか「値に応じてプレビューを表示させたいなぁ」とか「もっと直感的に操作できないかなぁ」なんて思うことはないでしょうか。そんなときは"Custom Editor"や"Property Drawer"といった「エディター拡張」機能を使ってみましょう!エディター拡張を利用すれば、インスペクタ上の表示を自由に書き換えたり、シーンビューにハンドル(値を変更するためのツマミ)を追加したりすることができます。

あくまで開発者用の補助機能なので、どれだけ時間をかけて凝った拡張を行ったとしてもそれだけではゲーム制作は1ミリも前に進みません。でも、たとえばインスペクタ上の数値を睨みながらポチポチ座標を入力するよりは、エディター拡張をしてシーンビュー上で直接位置を操作できるようにしたほうがきっと開発は楽になるはずです。そして何よりエディター拡張は意外と楽しい!たまには開発効率のことは一旦忘れて、すんごく凝ったツールを作ってみるのも面白いと思います。

てなわけで、基本的な使い方を見ていきましょう。

まず、MonoBehaviourを継承した普通のスクリプトを用意しました。

using UnityEngine;

public class BackAndForth : MonoBehaviour {

 public float speed = 1;
 public Vector3 pos1;
 public Vector3 pos2;

 void Update ()
 {
  // pos1 と pos2 の間を行ったり来たりする
  float t = Mathf.PingPong(Time.time * speed, 1);
  transform.position = Vector3.Lerp(pos1, pos2, t);
 }

 // pos1 と pos2 の高さを合わせる
 public void AlignVertically()
 {
  float height = (pos1.y + pos2.y) / 2f;
  pos1.y = height;
  pos2.y = height;
 }
}

設定したpos1とpos2の間をひたすら行ったり来たりするだけの何の変哲もない単純なスクリプトですね。Mathf.PingPongで0~1の値を取得して、pos1(0)とpos2(1)の間のどの位置に移動するかを決めています。インスペクタ上の表示はこんな感じです。

いたって普通のインスペクタ

では、ここから実際にエディタ拡張を行います。高さを合わせるためのAlignVerticallyというメソッドをせっかく用意したので、これを呼び出すことができるボタンをインスペクタ上に表示してみましょう。

"Editor"という名前のフォルダを用意して、そこに新しいスクリプトを作成します。("Editor"フォルダに入れておかないとビルド時にエラーになります。)

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(BackAndForth))]
public class BackAndForthEditor : Editor {
 
 public override void OnInspectorGUI()
 {
  BackAndForth t = target as BackAndForth;
  
  // 通常の入力欄を表示
  DrawDefaultInspector();

  // ボタンを表示
  if (GUILayout.Button("Align Vertically"))
  {
   Undo.RecordObject(t, "Align Vertically");
   t.AlignVertically();
  }
 }
}

エディタ用のスクリプトはゲームオブジェクトにアタッチしたりする必要はなくこのままでOK。これでインスペクタの表示がこうなるはずです。

下にボタンがついた

"Align Vertically"と書かれたボタンが追加されていて、そのボタンを押すとpos1とpos2のY座標が自動で揃うようになりました。つまり元のクラスのAlignVerticallyメソッドが実行されたわけです。

コードからなんとなくわかると思いますが一応ポイントだけ解説しておきます。

[CustomEditor(typeof(BackAndForth))]
public class BackAndForthEditor : Editor {

4行目で「このクラスをtypeofで指定したクラスのカスタムエディタとして扱うよ」ということを伝えています。ここの指定をミスると動きません。また、MonoBehaviourではなくEditorクラスを継承していることに注意しましょう。スクリプトを新規作成するとデフォルトでMonoBehaviour継承になっているのでついうっかりそのままにしちゃうのだ…

public override void OnInspectorGUI()

そしてOnInspectorGUI()をoverrideして実装していくことでインスペクタを好きなように表示させることができるというわけです。

BackAndForth t = target as BackAndForth;

9行目で急に現れたtargetというのは「現在インスペクタの表示対象となっているオブジェクト」つまりこの場合はBackAndForthオブジェクトを指しています。使いやすいようあらかじめ型変換して取得しておきます。

DrawDefaultInspector();

12行目のDrawDefaultInspector()はめちゃめちゃ便利で、デフォルトのインスペクタをそのまま表示させることができます。今回のように追加でボタンを表示するだけみたいな場合は各変数をいじる必要がないので、これを使えば楽ちんです。

if (GUILayout.Button("Align Vertically"))
{
 Undo.RecordObject(t, "Align Vertically");
 t.AlignVertically();
}

15行目でボタンを追加。GUILayoutクラスは、入力欄やボタンをどの位置に表示するかをわざわざRectとかを使って指定しなくても勝手に計算してうまいこと配置してくれる便利クラスです。GUILayout.Button(文字列)と書くだけでインスペクタの幅に合わせたボタンを表示してくれます。GUILayout.Buttonはボタンが押されたときにtrueを返すので、そのときだけ t.AlignVertically を実行して高さを揃えています。

気になるのがその前にあるUndo...の部分ですが、これはコレです↓



つまり「ひとつ前に戻す」とき用の操作を登録しているわけです。これを書いておけばAlignVerticallyボタンを押した後でもCtrl+Zを押せば元の数値に戻るようになります。

また、この処理は「指定されたオブジェクトのプロパティが変更されたかどうか」を勝手にチェックして、実際に差分がある場合のみ変更点を保存してくれるようになっています。たとえばもしボタンを連打したとしても、pos1とpos2の値が変わらない限りUndoに登録される操作は1回だけということになります。気が利く~。

Undo.RecordObject(これからプロパティを変更する対象のオブジェクト, Undoのメニューに表示される履歴の名前)ということなので、上の例ではインスペクタに表示されているオブジェクト(t)のプロパティが変更されたかどうかを監視しているわけですね。

ただし、注意したいことがあります。「オブジェクトのプロパティを変更する」ときはこのUndo.RecordObjectでいいのですが「新しくゲームオブジェクトを作成する」「ゲームオブジェクトを破棄する」「コンポーネントを追加する」「親子関係を変更する」場合はそれぞれ専用のメソッドが用意されていて、登録したい操作に応じてそれらを使い分けなければならないのです。うむむー、気が利かん~!詳しくはマニュアルをご覧ください。


そんなわけで、とりあえず最もシンプルなカスタムエディタはこんな感じです。ボタンを表示するだけでもかなり便利になると思います。まあ、単にメソッドを実行するだけならMenuItem属性とかContextMenu属性を使えばもっと簡単なんですけどね…

それから、エディター拡張を学ぶうえで個人的にめちゃくちゃ参考になったのが「エディター拡張入門」!説明が丁寧で具体例も多く本当にわかりやすかったです。とにかくこれとUnityの公式マニュアルさえ読めばやりたいことはほとんど実現できると思います。無料でWeb版が公開されているので是非読んでみてください。

<次回> Unity 意外と楽しいエディター拡張 ~シリアライズって何?編~





補足その1 - Editorフォルダについて
今回は拡張用のスクリプトを別ファイルにして"Editor"フォルダに入れましたが、実は元のスクリプトと同じファイルに書いても構いません。ただしその状態でビルドしようとするとコンパイルエラーになります。Editorクラスはビルド時に除外されてしまうからです。これを防ぐにはエディタ用の箇所を #if UNITY_EDITOR ~ #endif でまるごと囲む必要があるのですが…正直あんまり綺麗じゃないので"Editor"フォルダを利用したほうがいいと思います("Editor"と名前のつくフォルダに入れておくとUnity側で勝手にうまいこと対応してくれるのです)。でもできれば同じファイル内に書きたいですよね…使いまわすものでもないし…う~ん…

補足その2 - 変更内容が保存されない場合
Undo.RecordObjectを使用すれば、Undoに登録されると同時に変更内容も自動的に保存されます。ほとんどの場合はこれで問題なく動作するのですが、配列要素の中身を書き換える場合は、これだけでは変更内容が保存されません。一見するときちんと変更されたようにみえても実行ボタン(プレイボタン)を押すと勝手に変更前の値に戻ってしまいます。これを解決するにはこのページが参考になります→ Inspector拡張でスクリプトから変更した値がちゃんと保存されるようにする。 - Qiita …つまりEditorUtility.SetDirtyと併用すればいいというわけです。ただ、公式のドキュメントを見ると「Unity5.3以降では基本的に使うな。プロパティの変更にはSerializedObject使え。それ以外はUndo.RecordObjectかEditorSceneManager.MarkSceneDirty使え」みたいなことが書かれてあって、正直ほんとに使っていいのか自信がないのですが、今のところ手っ取り早い解決策がこれしかないのできっとこれが正解なのでしょう。そういうことにしておきましょう。

0 件のコメント:

コメントを投稿