Chapter 02

インスペクター拡張の基礎

kumaS
kumaS
2021.02.25に更新

インスペクター拡張はなにがうれしい?

Unity では、コードを書くと public な変数はだいたい自動的に Inspector で表示され、いじれるようになる。だけど、デフォルトの Inspector の表示じゃ困るときがある。例えば、「この変数は public じゃないといけないけど Inspector から変更されたくない。」とか、「この変数を調整する範囲を制限したい。」とか、「この変数はあの変数によって表示するかしないか変えたい」とか。
インスペクター拡張をすると、そういったことができるようになる。主に、インスペクター拡張をするのに二種類方法がある。属性によるインスペクター拡張と、カスタムエディターによるインスペクター拡張だ。
属性によるインスペクター拡張はとても簡単にできる反面、できることが限られる。カスタムエディターによるインスペクター拡張は、ややめんどくさいがほぼなんでもできる。
それでは、見ていこう。

属性によるインスペクター拡張

属性によるインスペクター拡張は非常に簡単だ。そもそも、属性というのは以下の大かっこで囲われたものだ。

using UnityEngine;

public class Example1 : MonoBehaviour
{
    [HideInInspector]
    public int p = 5;
}

配列以外で大かっこを見たら属性だと思えばいい。属性は、その下のもの(クラスや関数、変数)に属性を与えるものだ。この例では、int p にインスペクターで表示しないという属性を与えていることとなる。
複数の属性を与える時には以下のようにすればいい。

using UnityEngine;

public class Example2 : MonoBehaviour
{
    [SerializeField, Range(0, 5)]
    private int a = 5;
    
    [SerializeField]
    [Range(0, 5)]
    private int b = 5;
}

これはどちらも同じことをやっている。int aint b に、インスペクターに表示されるようにし、0 ~ 5 の範囲でしか操作できないようにさせている。
コンマで区切って複数の属性をつなげるか、属性を複数付けるかだ。どちらでもいいけど、どちらかに統一しておいた方が見やすくなる。
実は、属性によるインスペクター拡張はたったこれだけだ。どんな属性があるかはチャプター 5 に書いてあるからそれを見るのもいいし、公式のドキュメント(Attributes のところ)を見てもいい。というより見れるなら最新の公式ドキュメントの方がいい。

カスタムエディターによるインスペクター拡張

属性によるエディター拡張はできることに限りがあるから結局カスタムエディターを作ることになる場合もよくある。けど、カスタムエディターはやりたい放題できてしまうのでちょっと楽しかったりもする。今回は以下のクラスのカスタムエディター作るよ。

CustomSample.cs
using UnityEngine;

public class CustomSample : MonoBehaviour
{
    public string enemyName;
    public int hp;
    public int power;
}

そして、下のスクショのようになるよ。それでは見ていこう。

CustomSample

インスペクターに表示するためのクラスを作る

インスペクターに表示するためのクラスを作るのだけど、重要なことがある。そのクラスはEditor クラスを継承して作るということだ。つまり、Editor フォルダを作り、その下にそのクラスのファイルを作るか、そのクラスと using UnityEditor;#if UNITY_EDITOR#endifで囲わなければいけない。

例では、#if UNITY_EDITOR#endif で囲むことにするけど、Editor フォルダ以下にある場合は #if UNITY_EDITOR#endif は書かない。二か所に分けているのは、だいたいこの二つの間に何か書くからだね。

CustomInspectorSample.cs
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 public class CustomInspectorSample : Editor
 {

 }
 #endif

どのクラスのインスペクターか書く

さて、Editor を継承したクラスを作っただけではどのクラスのインスペクターなのかわかんない。なので、ちゃんとこのクラスの拡張だよ!と書いてあげる必要がある。それを書くと下のようになる。

CustomInspectorSample.cs
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
+[CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {

 }
 #endif

そう。 このクラスのカスタムエディターだよとクラスの属性として与えればいいのである。

インスペクターを書き換える

インスペクターを書き換えるには Editor クラスの OnInspectorGUI() をオーバーライドすればいい。オーバーライドはこんな感じ。

CustomInspectorSample.cs
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 [CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {
+    public override void OnInspectorGUI()
+    {
+
+    }
 }
 #endif

この状態で CustomSample クラスのインスペクターを見ると何も表示されないはず。だって、何もしないように上書きしたもの。

インスペクターにいろいろ表示する

下のようにするとインスペクターにプロパティが表示できるようになるよ。

CustomInspectorSample.cs
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 [CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {
     public override void OnInspectorGUI()
     {
+        serializedObject.FindProperty(nameof(CustomSample.enemyName)).stringValue = EditorGUILayout.TextField("Enemy name", serializedObject.FindProperty(nameof(CustomSample.enemyName)).stringValue);
+        serializedObject.FindProperty(nameof(CustomSample.hp)).intValue = EditorGUILayout.IntField("HP", serializedObject.FindProperty(nameof(CustomSample.hp)).intValue);
+        EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(CustomSample.power)));
     }
 }
 #endif

順番に説明するよ。一つ目の EditorGUILayout.TextField() で一行の入力欄を作れる。引数として一つ目にラベル、二つ目に現在のテキストを与えているんだ。この関数の返り値として入力された文字が来るよ。
serializedObject というのは、対象のクラスのオブジェクトの変数[1]を持っているものだと思ってもらっていいかな。これの FindProperty() に変数名を引数として与えるとその変数についてのオブジェクト(SerializedProperty)が返ってくる。これの stringValue で文字列を取り出し(書き込み)ができるというわけ。
 nameof() としているのは、public な変数であれば入力補完が効かせれてタイポしにくくなるから。タイポ嫌いだもん。もちろん文字列直打ちでも大丈夫。
二つ目はただ、stringint に変わっただけであまり言うことないかな。
三つめは、EditorGUILayout.PropertyField() という関数に、SerializedProperty を引数として与えると、デフォルトのやつができあがるよ。

楽をする

さっきの長くない?私はあんなに長いのは嫌だ。だからいつも私は下のようにキャッシュして短くしてるよ。(私が楽をするためにやっていることだから、やらなくても大丈夫だよ。)

CustomInspectorSample.cs
+using System.Collections.Generic;
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 [CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {
+    Dictionary<string, SerializedProperty> property = new Dictionary<string, SerializedProperty>();
    
+    private void OnEnable()
+    {
+        property.Add(nameof(CustomSample.enemyName), serializedObject.FindProperty(nameof(CustomSample.enemyName)));
+        property.Add(nameof(CustomSample.hp), serializedObject.FindProperty(nameof(CustomSample.hp)));
+        property.Add(nameof(CustomSample.power), serializedObject.FindProperty(nameof(CustomSample.power)));
+    }
    
     public override void OnInspectorGUI()
     {
+        property[nameof(CustomSample.enemyName)].stringValue = EditorGUILayout.TextField("Enemy name", property[nameof(CustomSample.enemyName)].stringValue);
+        property[nameof(CustomSample.hp)].intValue = EditorGUILayout.IntField("HP", property[nameof(CustomSample.hp)].intValue);
+        EditorGUILayout.PropertyField(property[nameof(CustomSample.power)]);
     }
 }
 #endif

私はこっちの方がすっきりするから好き。

ファイルへ飛べるようにする

今のインスペクターを見て足りないのは…ファイルへ飛べないね。本体のスクリプトを修正しようと思ったときに飛べないのはなかなかつらい。だから、下のようにファイルへ飛べるようにするといいよ。

CustomInspectorSample.cs
 using System.Collections.Generic;
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 [CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {
     Dictionary<string, SerializedProperty> property = new Dictionary<string, SerializedProperty>();
	
     private void OnEnable()
     {
         property.Add(nameof(CustomSample.enemyName), serializedObject.FindProperty(nameof(CustomSample.enemyName)));
         property.Add(nameof(CustomSample.hp), serializedObject.FindProperty(nameof(CustomSample.hp)));
         property.Add(nameof(CustomSample.power), serializedObject.FindProperty(nameof(CustomSample.power)));
     }
	
     public override void OnInspectorGUI()
     {
+        using(new EditorGUI.DisabledGroupScope(true))
+        {
+            EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((MonoBehaviour)target), typeof(MonoScript), false);
+        }
         property[nameof(CustomSample.enemyName)].stringValue = EditorGUILayout.TextField("Enemy name", property[nameof(CustomSample.enemyName)].stringValue);
         property[nameof(CustomSample.hp)].intValue = EditorGUILayout.IntField("HP", property[nameof(CustomSample.hp)].intValue);
         EditorGUILayout.PropertyField(property[nameof(CustomSample.power)]);
     }
 }
 #endif

using(new EditorGUI.DisabledGroupScope(true)) のところで、この中かっこの中は操作できませんよとして、EditorGUILayout.ObjectField() でオブジェクトのところにスクリプトを入れている感じです。

保存できるようにする

ちょっとここでインスペクターで触ってみて気づくことないかな?

そう。保存ができない。だから、下のように OnInspectorGUI() の始まりに serializedObject.Update() を呼んで変数を最新の状態にして、終わりに serializedObject.ApplyModifiedProperties() を呼んで変数の変更を保存する必要があるの。

CustomInspectorSample.cs
 using System.Collections.Generic;
 using UnityEngine;

 #if UNITY_EDITOR
 using UnityEditor;
 #endif

 #if UNITY_EDITOR
 [CustomEditor(typeof(CustomSample))]
 public class CustomInspectorSample : Editor
 {
     Dictionary<string, SerializedProperty> property = new Dictionary<string, SerializedProperty>();
	
     private void OnEnable()
     {
         property.Add(nameof(CustomSample.enemyName), serializedObject.FindProperty(nameof(CustomSample.enemyName)));
         property.Add(nameof(CustomSample.hp), serializedObject.FindProperty(nameof(CustomSample.hp)));
         property.Add(nameof(CustomSample.power), serializedObject.FindProperty(nameof(CustomSample.power)));
     }
	
    public override void OnInspectorGUI()
    {
+        serializedObject.Update();
         using(new EditorGUI.DisabledGroupScope(true))
         {
             EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((MonoBehaviour)target), typeof(MonoScript), false);
         }
         property[nameof(CustomSample.enemyName)].stringValue = EditorGUILayout.TextField("Enemy name", property[nameof(CustomSample.enemyName)].stringValue);
         property[nameof(CustomSample.hp)].intValue = EditorGUILayout.IntField("HP", property[nameof(CustomSample.hp)].intValue);
         EditorGUILayout.PropertyField(property[nameof(CustomSample.power)]);
+        serializedObject.ApplyModifiedProperties();
    }
 }
 #endif

どう?ちゃんと動いた?これで、基本的なところは終了。あとは、チャプター 5 に書いてあるものを組み合わせて作ったり、公式のドキュメント(GUI 系のクラスを使うんだよ)を見て作ったりすると思った通りにいろんなものが作れるよ。

脚注
  1. 正確にはシリアライズされた変数。 ↩︎