概要
プランナーと一緒に作業をしていると「あの値ちょっと調整して欲しいんだけど。」ってこと結構ありますよね。
それを都度プログラマーが対応していると結構しんどいかと思います。
そこで、Inspector拡張の出番です!
プランナーが変更しても大丈夫な値はInspector(GUI)で触れるようにしてあげることで、
プログラマーの手が止まるリスクを減らすことができます。
今回はそんなInspector拡張について、軽く綴っていく回です。
拡張手法
2パターン紹介するよ。好きな手法を選んでね!
(個人的には手法2がオススメ!)
(手法1) Editorフォルダに新規でInspector拡張用のクラスを用意する
サンプル
処理コード
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; } }
}
拡張コード
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拡張処理を追記する
サンプル
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を作らなくて済む
デメリット
- 処理コードと拡張コードが混在する為、見た目が煩雑になる
実働イメージ
ステップアップサンプル
上のサンプル程度なら、拡張するまでもなかったのでは?って思われちゃうかも知れないので、もう一歩先の使い方を紹介しちゃいます。
入力値をチェックする
簡単なものなら[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で実践してみるよ。
if( GUILayout.Button( "クリア" ) )
{
list.Clear();
}
折り畳み表示
これは上のサンプルでも使っているね!
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以降、↑の実装では変更が保存されなくなってしまいました…。
良かったら↓もご覧ください。