2017年12月5日

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


エディター拡張について自分の頭を整理するための解説その2。今回はカスタムエディタでの変数の扱い方についてメモメモ…


※このページの内容の動作確認にはUnity5.6を使用しています。
前回はインスペクタにボタンを追加して自作のメソッドを実行できるようにしました。今回はDrawDefaultInspector()を使わずに、フィールドを1つ1つ表示してみたいと思います。

元になるスクリプトは前回と全く同じです。

  1. using UnityEngine;
  2.  
  3. public class BackAndForth : MonoBehaviour {
  4.  
  5. public float speed = 1;
  6. public Vector3 pos1;
  7. public Vector3 pos2;
  8.  
  9. void Update ()
  10. {
  11.   // pos1 と pos2 の間を行ったり来たりする
  12.   float t = Mathf.PingPong(Time.time * speed, 1);
  13.   transform.position = Vector3.Lerp(pos1, pos2, t);
  14. }
  15.  
  16. // pos1 と pos2 の高さを合わせる
  17. public void AlignVertically()
  18. {
  19.   float height = (pos1.y + pos2.y) / 2f;
  20.   pos1.y = height;
  21.   pos2.y = height;
  22. }
  23. }


では、次にさっそく今回のカスタムエディタの例を見ていくのですが、大事なのは

フィールド(変数)に対する操作は基本的にSerializedObjectクラスを通して行う

ということです。ぜひこれを念頭において次のコードをご覧ください。

  1. using UnityEngine;
  2. using UnityEditor;
  3.  
  4. [CustomEditor(typeof(BackAndForth))]
  5. public class BackAndForthEditor : Editor {
  6.  
  7. SerializedProperty speedProp;
  8. SerializedProperty pos1Prop;
  9. SerializedProperty pos2Prop;
  10.  
  11. private void OnEnable()
  12. {
  13.   speedProp = serializedObject.FindProperty("speed");
  14.   pos1Prop = serializedObject.FindProperty("pos1");
  15.   pos2Prop = serializedObject.FindProperty("pos2");
  16. }
  17. public override void OnInspectorGUI()
  18. {
  19.   BackAndForth t = target as BackAndForth;
  20.  
  21.   serializedObject.Update();
  22.  
  23.   EditorGUI.BeginChangeCheck();
  24.   
  25.   // フィールドを表示
  26.   EditorGUILayout.PropertyField(speedProp);
  27.   EditorGUILayout.PropertyField(pos1Prop, new GUIContent("Position 1"));
  28.   EditorGUILayout.PropertyField(pos2Prop, new GUIContent("Position 2"));
  29.   
  30.   // 値に変更があったときは speedが必ず0以上になるように調整
  31.   if(EditorGUI.EndChangeCheck())
  32.   {
  33.    speedProp.floatValue = Mathf.Max(0, speedProp.floatValue);
  34.   }
  35.  
  36.   // ボタンを表示
  37.   if (GUILayout.Button("Align Vertically"))
  38.   {
  39.    Undo.RecordObject(t, "Align Vertically");
  40.    t.AlignVertically();
  41.   }
  42.   
  43.   serializedObject.ApplyModifiedProperties();
  44. }
  45. }

SerializedObjectって?

コードの中になにやらSerializedPropertyとかserializedObjectとかがいっぱい出てきました。Serializeって一体何なんでしょう?

「シリアライズ(シリアル化)」というのは、すんごくざっくり言うと「データをUnity側で保存や管理がしやすい形式に直すこと」です。UnityではあらゆるデータをSerializedObjectという形式で扱っています。インスペクタに表示されて値の編集や保存ができるのは、それがシリアライズされているからです。自作のクラスや構造体のフィールドをインスペクタに表示させたいとき[System.Serializable]というアトリビュートをつけますが、あれは「このクラス(構造体)を保存できる形式にしといて」と頼んでいるんですね。

カスタムエディタにおいても、SerializedObjectを通して値を書き換えるようにすれば変更点をまとめて保存してくれたり自動でUndoに登録してくれたりするのでとても便利なのです。

  1. SerializedProperty speedProp;
  2. SerializedProperty pos1Prop;
  3. SerializedProperty pos2Prop;
  4.  
  5. private void OnEnable()
  6. {
  7.  speedProp = serializedObject.FindProperty("speed");
  8.  pos1Prop = serializedObject.FindProperty("pos1");
  9.  pos2Prop = serializedObject.FindProperty("pos2");
  10. }

というわけで、変数を直接いじるのではなくシリアライズされたものを操作していくことにします。11行目のOnEnable()というのは、このオブジェクト(BackAndForthEditor)が読み込まれたとき、つまりゲームオブジェクトを選択してインスペクタが表示された瞬間に実行される関数です。そこで、対象のオブジェクト(BackAndForth)から該当する名前のプロパティを探して取得しています。別に毎フレーム読み込む必要はないのでできるだけ最初に一括して取得しておきましょう。

注意したいのはserializedObject.FindPropertyの引数がstringであること。変数名を打ち間違えたり、変数名を途中で変更した場合は気づかずエラーのもとになってしまいます。対策を考えた結果「もしかしてnameof演算子を使えばエラーを防げるのでは!?」と思いついたのでウキウキ気分で試してみようとしたのですが、C#のバージョンが4だったため無理でした(nameofはC#6からの機能)。一応Unity5.5以上であればゴニョゴニョするとC#6も使えるらしいのですが、めんどくさいので今回はスルーすることにします。

SerializedObjectを更新/適用する

  1. serializedObject.Update();
  1. serializedObject.ApplyModifiedProperties();

SerializedObjectやSerializedPropertyの値を変更する前にはserializedObject.Update()を使用して最新の情報に更新しておきましょう。また、すべての処理が終わった後にはserializedObject.ApplyModifiedProperties()を呼び出して変更済みのプロパティを適用するようにします。この2行を忘れると入力した値が消えたりして変な挙動になるので必ず書くようにしましょう。

フィールドを表示する

  1. EditorGUILayout.PropertyField(speedProp);
  2. EditorGUILayout.PropertyField(pos1Prop, new GUIContent("Position 1"));
  3. EditorGUILayout.PropertyField(pos2Prop, new GUIContent("Position 2"));

実際に入力欄を表示しているのはこの部分。EditorGUILayout.PropertyFieldを使えばデフォルトのフィールドを表示することができます。元々の変数がどういう型でどういう入力欄を描画すればいいのかを自動で割り出してくれるんですね。便利~!また、引数でいろいろオプションを指定できるのでたとえば28~29行目のようにラベル(表示名)をデフォルトの"Pos 1"から"Position 1"のように自由に変えることもできます。

ラベルやツールチップなども好きなように設定可能

あるいは、たとえば

  1. EditorGUILayout.Slider(speedProp, 0, 10);

のように書けばスライダー形式で表示することも可能です。

[Range(0,10)]と同じ

ほかにもIntSliderやMinMaxSliderなど便利な形式が用意されているので必要に応じてじゃんじゃん利用したいですね!

 変更があった場合のみ追加の処理をする

  1. EditorGUI.BeginChangeCheck();
  1. if(EditorGUI.EndChangeCheck())
  2. {
  3.  speedProp.floatValue = Mathf.Max(0, speedProp.floatValue);
  4. }

EditorGUI.BeginChangeCheck()とEditorGUI.EndChangeCheck()で処理を挟むと、挟んだ部分の要素が変更されたかどうかをチェックすることができます(変更されていればEditorGUI.EndChangeCheck()がtrueを返します)。今回はためしにspeedの値がマイナスにならないように処理をしてみました。変数Aの値に応じて変数Bの値を調整する、みたいなことも可能です。

Speedの値をマイナスにさせない

値へのアクセスには専用のプロパティを使う

それから、speedProp.floatValueという書き方を見てなんとなく察してもらえるかとは思いますがSerializedPropertyの保持している値を読み込んだり書き換えたりするには専用のプロパティを通す必要があります。値がfloatならfloatValue、stringならstringValue、Vector2ならvector2Value…といった具合です。たとえばpos1PropのY座標を取得したい場合はpos1Prop.vector3Value.yとなります。ちょっとめんどくさいですねー。

SerializedObjectを通さず変数を直接書き換えるのは極力避けたほうがいいと思います。

// これはなるべく避ける
t.speed = Mathf.Max(0, t.speed);

というのは、serializedObject.ApplyModifiedProperties()で結局値が上書きされるので書き換えるタイミングしだいでは値の変更が反映されなかったりするからです。また、SerializedObjectを通していればUndoの処理が自動で登録されるのですが、そうでない場合は40行目のように手動で登録しないといけません。

以上、説明としてはこんな感じでしょうか。もしかしたら勘違いしている部分があるかもしれないので、眉に唾つけながら、話半分に読んでいただけると嬉しいです。

<次回> Unity 意外と楽しいエディター拡張 ~すっきり配列編~

0 件のコメント:

コメントを投稿