Help us understand the problem. What is going on with this article?

UnityのInspectorを拡張する。

More than 3 years have passed since last update.

概要

プランナーと一緒に作業をしていると「あの値ちょっと調整して欲しいんだけど。」ってこと結構ありますよね。
それを都度プログラマーが対応していると結構しんどいかと思います。

そこで、Inspector拡張の出番です!

プランナーが変更しても大丈夫な値はInspector(GUI)で触れるようにしてあげることで、
プログラマーの手が止まるリスクを減らすことができます。

今回はそんなInspector拡張について、軽く綴っていく回です。

拡張手法

2パターン紹介するよ。好きな手法を選んでね!
(個人的には手法2がオススメ!)

(手法1) Editorフォルダに新規でInspector拡張用のクラスを用意する

サンプル

処理コード

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

/**
 * キャラクタサンプルクラス
 */
public class Character : MonoBehaviour
{
    //! 体力
    [SerializeField]
    int                 m_hp_now        = 100;                          //!< 現在値
    [SerializeField]
    int                 m_hp_max        = 100;                          //!< 最大値

    [SerializeField]
    float               m_spd           = 6.4f;                         //!< 移動速度

    [SerializeField]
    string              m_name          = "星宮";                     //!< キャラ名

    [SerializeField]
    List<GameObject>    m_friends       = new List<GameObject>();       //!< 友達リスト


    //! 拡張クラスからアクセスするために getter/setter を用意してね! (他でも使うと思うけど)
    public int      hp          { get { return m_hp_now; } set { m_hp_now = value; } }
    public int      maxHp       { get { return m_hp_max; } set { m_hp_max = value; } }
    public float    speed       { get { return m_spd; } set { m_spd = value; } }
    public string   charaName   { get { return m_name; } set { m_name = value; } }
    public List<GameObject> friends { get { return m_friends; } set { m_friends = value; } }
}

拡張コード

CharacterEditor.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;                              //!< UnityEditorを入れてね!

/**
 * Inspector拡張クラス
 */
[CustomEditor(typeof(Character))]               //!< 拡張するときのお決まりとして書いてね
public class CharacterEditor : Editor           //!< Editorを継承するよ!
{
    bool folding = false;

    public override void OnInspectorGUI()
    {
        // target は処理コードのインスタンスだよ! 処理コードの型でキャストして使ってね!
        Character chara = target as Character;

        /* -- カスタム表示 -- */

        // -- 体力 --
        EditorGUILayout.LabelField( "体力(現在/最大)" );
        EditorGUILayout.BeginHorizontal();
        chara.hp        = EditorGUILayout.IntField( chara.hp, GUILayout.Width(48) );
        chara.maxHp     = EditorGUILayout.IntField( chara.maxHp, GUILayout.Width(48) );
        EditorGUILayout.EndHorizontal();

        // -- 速度 --
        chara.speed     = EditorGUILayout.FloatField( "速度", chara.speed );

        // -- 名前 --
        chara.charaName = EditorGUILayout.TextField( "名前", chara.charaName );

        // -- 友達 --
        List<GameObject> list = chara.friends;
        int i, len = list.Count;

        // 折りたたみ表示
        if( folding = EditorGUILayout.Foldout( folding, "友達" ) )
        {
            // リスト表示
            for( i = 0; i < len; ++i )
            {
                list[i] = EditorGUILayout.ObjectField( list[i], typeof(GameObject), true ) as GameObject;
            }

            GameObject go = EditorGUILayout.ObjectField( "追加", null, typeof(GameObject), true ) as GameObject;
            if( go != null )
                list.Add( go );
        }
    }
}

(拡張コードはEditorフォルダに入れてね!)

メリット

  • 処理コードと拡張コードが分離されるのでコードの見た目がスッキリする

デメリット

  • 拡張コードのためにgetter/setterを用意する必要がある(ないしはpublicで宣言するとか)

(手法2) 既存のクラスにInspector拡張処理を追記する

サンプル

Character.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;      //!< デプロイ時にEditorスクリプトが入るとエラーになるので UNITY_EDITOR で括ってね!
#endif

/**
 * キャラクタサンプルクラス
 */
public class Character : MonoBehaviour
{
    //! 体力
    [SerializeField]
    int                 m_hp_now        = 100;                          //!< 現在値
    [SerializeField]
    int                 m_hp_max        = 100;                          //!< 最大値

    [SerializeField]
    float               m_spd           = 6.4f;                         //!< 移動速度

    [SerializeField]
    string              m_name          = "星宮";                     //!< キャラ名

    [SerializeField]
    List<GameObject>    m_friends       = new List<GameObject>();       //!< 友達リスト


    //! 手法2の場合はプライベートメンバーにも直接アクセス可能なので、getter/setterは必須ではないよ!
    public int      hp          { get { return m_hp_now; } set { m_hp_now = value; } }
    public int      maxHp       { get { return m_hp_max; } set { m_hp_max = value; } }
    public float    speed       { get { return m_spd; } set { m_spd = value; } }
    public string   charaName   { get { return m_name; } set { m_name = value; } }
    public List<GameObject> friends { get { return m_friends; } set { m_friends = value; } }



/* ---- ここから拡張コード ---- */
#if UNITY_EDITOR
    /**
     * Inspector拡張クラス
     */
    [CustomEditor(typeof(Character))]               //!< 拡張するときのお決まりとして書いてね
    public class CharacterEditor : Editor           //!< Editorを継承するよ!
    {
        bool folding = false;

        public override void OnInspectorGUI()
        {
            // target は処理コードのインスタンスだよ! 処理コードの型でキャストして使ってね!
            Character chara = target as Character;

            /* -- カスタム表示 -- */

            // -- 体力 --
            EditorGUILayout.LabelField( "体力(現在/最大)" );
            EditorGUILayout.BeginHorizontal();
            chara.m_hp_now      = EditorGUILayout.IntField( chara.m_hp_now, GUILayout.Width(48) );
            chara.m_hp_max      = EditorGUILayout.IntField( chara.m_hp_max, GUILayout.Width(48) );
            EditorGUILayout.EndHorizontal();

            // -- 速度 --
            chara.m_spd     = EditorGUILayout.FloatField( "速度", chara.m_spd );

            // -- 名前 --
            chara.m_name    = EditorGUILayout.TextField( "名前", chara.m_name );

            // -- 友達 --
            List<GameObject> list = chara.m_friends;
            int i, len = list.Count;

            // 折りたたみ表示
            if( folding = EditorGUILayout.Foldout( folding, "友達" ) )
            {
                // リスト表示
                for( i = 0; i < len; ++i )
                {
                    list[i] = EditorGUILayout.ObjectField( list[i], typeof(GameObject), true ) as GameObject;
                }

                GameObject go = EditorGUILayout.ObjectField( "追加", null, typeof(GameObject), true ) as GameObject;
                if( go != null )
                    list.Add( go );
            }
        }
    }
#endif
}

メリット

  • コードが1つになるので、Inspector拡張時に処理コードと拡張コードを行ったり来たりする必要がない
  • 処理コードと同じクラス内に拡張コードを用意するので余計なgetter/setterを作らなくて済む

デメリット

  • 処理コードと拡張コードが混在する為、見た目が煩雑になる

実働イメージ

手法1でも手法2でも成果物は同じだよ!
ss_82.png

ステップアップサンプル

上のサンプル程度なら、拡張するまでもなかったのでは?って思われちゃうかも知れないので、もう一歩先の使い方を紹介しちゃいます。

入力値をチェックする

簡単なものなら[SerializeField]の属性で設定可能出来るけど、せっかくなので拡張コードで確認してみよう!

chara.maxHp = EditorGUILayout.IntField( "最大HP", chara.maxHp );
chara.hp = EditorGUILayout.IntField( "現在HP", chara.hp );

// 現在HPが最大HPを上回らないように補正
if( chara.hp > chara.maxHp )
{
    chara.hp = chara.maxHp;
}

ボタンを置いてみる

if( GUILayout.Button( "押すな" ) )
{
    // 押下時に実行したい処理
}

ボタンの配置には GUILayout.Button を使うよ。
ボタンを押すと true が返るから、if文に入ってくるよ。

CharacterEditorで実践してみるよ。

Character.cs
if( GUILayout.Button( "クリア" ) )
{
    list.Clear();
}

ボタンを押すと友達がいなくなります…。 (ToT)
ss_83.png

折り畳み表示

これは上のサンプルでも使っているね!

if( foldout = EditorGUILayout.Foldout( foldout, "折り畳み" ) )
{
    // 折り畳みされていない時の処理
}

Foldout の戻り値として、折り畳まれているかいないかがbool値で返ってくるよ!

複数のアイテムを一行に表示する

これも上のサンプルで使っているね!

EditorGUILayout.BeginHorizontal();      // 開始
name = EditorGUILayout.TextField( name );
age = EditorGUILayout.IntField( age );
EditorGUILayout.EndHorizontal();        // 終了

BeginHorizontal から EndHorizontal の間に書いた拡張UIは、同じ行に表示されるよ!

Enum値をドロップダウンメニューで選択

enum_value = (MY_ENUM)EditorGUILayout.EnumPopup( "選んでね", (MY_ENUM)enum_value );

とっても簡単ね!

任意の型のオブジェクトを選択させる

// Texture2Dを選ばせる例
tex = EditorGUILayout.ObjectField( "テクスチャ", tex, typeof(Texture2D), false ) as Texture2D;

// 自分で作ったクラスでもOK!
MyScripte ms = EditorGUILayout.ObjectField( "MyScriptを選んでね!", null, typeof(MyScript), true ) as MyScript;

第4引数のbool値は、Scene(Hierarchy)上のオブジェクトを指定可能かどうか、を指定するフラグだよ!

よく使う(?)拡張UIメソッド

メソッド 解説
IntField int値を取り扱うUIだよ!
FloatField float値を取り扱うUIだよ!
Toggle bool値を取り扱うUIだよ!
TextField string値を取り扱うUIだよ!
TextArea string値(複数行)を取り扱うUIだよ!
LabelField 文字列を表示するUIだよ!
Foldout 折り畳み矢印を表示するよ!
BeginHorizontal 一行表示の開始を宣言するよ。
EndHorizontalとセットで使うよ!
EndHorizontal 一行表示の終了を宣言するよ。
BeginHorizontalとセットで使うよ!
Space UI間の隙間(縦)を空けたい時に使うよ!
Horizontal内で使うと横の間隔が開くよ。

参考

おわりに

項目名とか日本語で書くの、意外と大事!(日本人相手なら)

プログラマー相手なら簡素な英単語で伝わるかも知れないけど、
他の畑の人には日本語じゃない表示は関係ないものとして心理フィルタリングされちゃったりします。

追記

Unity5.3以降、↑の実装では変更が保存されなくなってしまいました…。
良かったら↓もご覧ください。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away