WisdomSoft - for your serial experiences.

スクリプトによるエディタ制御

「第4回 渋谷Unity技術勉強会xコロプラ事例発表」で発表した資料と関連するソースコードの解説です。

開発工程の標準化と自動化のために

株式会社ハッチアップ主催、渋谷セルリアンタワーにあるGMOインターネット株式会社のオフィス内て2012年4月26日に開催された「第4回 渋谷Unity技術勉強会xコロプラ事例発表」にて、技術セッションのスピーカーを担当させていただきました。

今回は Unity によるゲーム開発から少し離れ、Unity エディタをカスタマイズすることを中心に、生産性を高めるための環境作りという視点でお話しさせていただきました。Unity エディタはスクリプトでカスタマイズでき、ゲーム制作を支援するためのプログラムを組み込むことができます。日々のタスクから反復的な作業を発見し、自動化するプログラムを作成すれば、より創造的な作業に割り当てる時間が増えるようになります。

以下に、勉強会資料を公開しています。

勉強会資料

Unity エディタのカスタマイズは非常に簡単で、Unity のプロジェクト内に UnityEditor 名前空間を使用するスクリプトを作成するだけです。Unity は UnityEditor 名前空間を使用するソースファイルをゲーム用のソースファイルとは別に、適切な参照設定が施されたエディタ用プロジェクトを作成し、自動的に振り分けてくれます。

コーディングも非常に簡単で UI に関する属性をクラスやメソッドに追加するだけです。面倒な設定ファイルの編集や、パッケージ化などは不要です。

メニューのカスタマイズ

任意のクラスメソッドに MenuItem 属性を追加することで、エディタ上にカスタムのメニュー項目を追加されます。メニュー項目が選択されると、MenuItem 属性に対応するメソッドが呼び出されます。MenuItem 属性を設定するメソッドは、パラメータを受け取ることはできません。戻り値を返すことはできますが無視されます。よって、通常はパラメータを受け取らず戻り値を返さないメソッドにします。

コード1
using UnityEngine;
using UnityEditor;

public class CustomMenuSample 
{
	private const string MENUNAME = "Thank you for coming to Shibuya Unity";
	
	[MenuItem(MENUNAME + "/Hello")]
	static void Hello()
	{
		Debug.Log("Hello Menu Test!!");
	}
	
	[MenuItem(MENUNAME + "/GameObjects Analyzer")]
	static void Analyze()
	{
		GameObject[] objects = (GameObject[])
			GameObject.FindSceneObjectsOfType(typeof(GameObject));
		foreach(GameObject obj in objects)
		{
			if (!char.IsUpper(obj.name[0]))
				Debug.LogWarning("Naming convention: \"" +
					obj.name + "\" first character is not upper.");
		}
	}
}
実行結果
コード1 実行結果

コード1は「Thank you for coming to Shibuya Unity」というメニューを追加し、その子メニュー項目として「Hello」メニュー項目と「GameObjects Analyzer」メニュー項目を表示します。それぞれ、コード1のメソッドに指定している属性パラメータからメニューの名前を指定しています。Unity エディタはコードを保存すると自動的にビルドし、生成されたアセンブリから属性情報を読み込み UI に反映させます。特別な設定は何もありません。

メニュー項目が選択されると、対応するメソッドが呼び出されます。例えば「Hello」メニュー項目が選択されると Hello() メソッドが実行されます。

独自のメニュー項目の例として、Analyze() メソッドのようなシーンの解析処理が考えられます。「GameObjects Analyzer」メニュー項目が選択されると Analyze() メソッドが実行されます。このメソッドはシーンに含まれているすべてのゲームオブジェクトを取得し、オブジェクト名の1文字目がアルファベットの大文字以外であれば「コンソール」ウィンドウに警告を出力します。これを応用すれば、会社やチームで決められた命名規則に従っているかどうかなどの分析やレポートを自動化できます。

「Preferences」のカスタマイズ

「Edit」メニューの「Preferences」メニュー項目を選択すると表示される「Unity Preferences」ウィンドウに、独自の UI を追加できます。これは、プロジェクト全体のグローバルなカスタム設定情報を管理するときに応用できます。

「Unity Preferences」ウィンドウに独自の UI を追加するには PreferenceItem 属性を任意のクラスメソッドに追加します。メソッドの戻り値は void にしなければなりません。パラメータを受け取ることはできますが、パラメータには既定の値(null や 0 など)が格納されます。よって、通常はパラメータを受け取らず戻り値を返さないメソッドにします。

コード2
using UnityEngine;
using UnityEditor;
using System.Collections;

public class CustomPreference
{
	private const string KEY = "Message";
	private static string message;
	
	[PreferenceItem("Sample")]
	public static void ShowPreference()
	{
		if (message == null)
		{
			message = EditorPrefs.GetString(KEY, "");
		}
		
		message = EditorGUILayout.TextField(KEY, message);
		
		if (GUI.changed)
			EditorPrefs.SetString(KEY, message);
	}
}
実行結果
コード2 実行結果

コード2は PreferenceItem 属性を ShowPreference() メソッドに追加しています。PreferenceItem 属性のパラメータには、メソッドが表示する設定パネルの名前を指定します。「Unity Preferences」ウィンドウを表示すると、新しいタブが追加されていることが確認できます。コード2では「Sample」タブを追加しています。

タブが選択されると PreferenceItem 属性に対応する ShowPreference() メソッドが実行されるので、内部で EditorGUILayout クラスを使ってテキストボックスを描画しています。

「Inspector」のカスタマイズ

コンポーネントの状態を個別に設定するというのは、かなり手間のかかることなので、個人的に一番お勧めしたいのは「Inspector」ウィンドウのカスタマイズです。任意のコンポーネントに対して、独自の UI を提供できるため、外部ファイルとのバインディングや入力値の制約などが可能になります。

「Inspector」ウィンドウのカスタマイズを行う前に、カスタマイズ対象となるスクリプトを用意しましょう。以下のコードはゲームオブジェクトを Speed プロパティの値で Y 軸に回転させるだけのスクリプトです。

コード3
using UnityEngine;
using System.Collections;

public class CustomInspectorTarget : MonoBehaviour 
{
	[SerializeField]
	private float speed;
	
	public float Speed
	{
		get { return speedField; }
		set { speedField = value; }
	}
	
	void Update ()
	{
		transform.Rotate(0, Speed, 0);
	}
}
実行結果
コード3 実行結果

コード3のスクリプトを任意のゲームオブジェクトに追加して「Inspector」ウィンドウを見ると、上記のように speed フィールドに対応する入力項目が表示されます。Unity 組み込みの UI では、数値や文字列型のフィールドに対してテキストボックスが表示されます。ここまでは、カスタマイズをしていない素の Unity の振る舞いです。

無制限に値を入れられることは少なく、仕様で定められた最小値と最大値の範囲で設定しなければなりません。ところが、上記の方法では無制限に値を設定できてしまいます。そこで、テキストボックスの代わりにスライダーを表示する方法が考えられます。

「Inspector」ウィンドウをカスタマイズするには Editor クラスを継承する独自のクラスを用意し、これに CustomEditor 属性を追加します。このとき、属性のパラメータにカスタマイズの対象となるコンポーネントの型を指定します。

次に Editor クラスを継承したクラスで OnInspectorGUI() メソッドをオーバーライドします。属性パラメータで指定したコンポーネントを持つゲームオブジェクトが選択されたとき、OnInspectorGUI() メソッドが呼び出されるので、内部で独自の UI を表示するコードを記述します。

コード4
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(CustomInspectorTarget))]
public class CustomInspectorSample : Editor 
{
 	public override void OnInspectorGUI()
	{
		CustomInspectorTarget script = target as CustomInspectorTarget;
				
		EditorGUILayout.BeginHorizontal();
		EditorGUILayout.PrefixLabel("Rotate Speed");
		script.Speed = EditorGUILayout.Slider(script.Speed, 0, 30);
		EditorGUILayout.EndHorizontal();
	}
}
実行結果
コード4 実行結果

コード4は 0 から 30 までの範囲で値を入力できるスライダーを表示するプログラムです。taregt プロパティには選択されているゲームオブジェクトが持つ、CustomEditor 属性で指定した型のオブジェクトが格納されています。コード4では CustomInspectorTarget オブジェクトが格納されているので、スライダーの値と Speed プロパティの値が同期するように処理しています。

関連文書