Unity Editor の拡張機能には,ドロップダウンメニューの作成方法が複数用意されています。
その中には,メニューの内容を階層構造に出来る物があります。
本稿では,その使い方について御説明いたします。
(2019/08/25)不備のあったスクリプトと記事内容を修正しました。
(Unity 2018.3.0f2)
EditorGUILayout.Popup
今回は,こちらの関数で確認します。
文字列配列string[]
から項目を選択し,配列のインデックス番号int
を返します。
(もちろん,ToString()
関数によって文字列string
を取得する事も出来ます。)
階層化自体は簡単だが……
階層化は実に単純で,リストの要素に半角スラッシュ/
を加えるだけです。
ただしリストの内容によっては不具合が生じるため,対策が必要となります。
まずは,特に対策を取らなかった場合のスクリプトを御覧ください。
using UnityEngine; // ここからエディター上でのみ有効 #if UNITY_EDITOR using UnityEditor; // エディター拡張クラス [CustomEditor(typeof(Extend))] public class ExtendedEditor : Editor {// Editor クラスを継承 // Extend クラスの変数を扱うために宣言 Extend extend; void OnEnable() {// 最初に実行 // Extend クラスに target を代入 extend = (Extend)target; } public override void OnInspectorGUI() {// Inspector に表示 // これ以降の要素に関してエディタによる変更を記録 EditorGUI.BeginChangeCheck(); // ラベルの作成 var label = "List"; // 初期値として表示する項目のインデックス番号 var selectedIndex = extend.index; // プルダウンメニューに登録する文字列配列 var displayOptions = Extend.list; // プルダウンメニューの作成 var index = EditorGUILayout.Popup(label, selectedIndex, displayOptions); if (EditorGUI.EndChangeCheck()) {// 操作を Undo に登録 // Extend クラスの変更を記録 var objectToUndo = extend; // Undo メニューに表示する項目名 var name = "Extend"; // 記録準備 Undo.RecordObject(objectToUndo, name); // Undo に記録したい変数を登録 extend.index = index; } } } // ここまでエディター上でのみ有効 #endif public class Extend : MonoBehaviour {// エディター拡張の中身 // リスト public static readonly string[] list = { "a/a", "a/ /a", "a/", "a", "b", "b/a" }; // 初期値は“a/ /a” public int index = 1; }
上記のスクリプトによって,次のようなドロップダウンメニューが生成されます。
問題点
- 選択してもチェックが付かずエラーになる物がある(a/ /a)
- 半角スラッシュ
/
以降に文字がなかったり半角スペースのみだったりした場合に区切り線として扱われてしまう(a/) - 階層化した要素の上層と同じ文字列の要素がリスト後方にあっても後者は表示されない(a)
改善策
これらの問題を解決した物が,以下のコードになります。
using UnityEngine; // Regex の使用 using System.Text.RegularExpressions; // List の使用 using System.Collections.Generic; // LINQ の使用 using System.Linq; // ここからエディター上でのみ有効 #if UNITY_EDITOR using UnityEditor; // エディター拡張クラス [CustomEditor(typeof(Extend))] public class ExtendedEditor : Editor {// Editor クラスを継承 // Extend クラスの変数を扱うために宣言 Extend extend; void OnEnable() {// 最初に実行 // Extend クラスに target を代入 extend = (Extend)target; } /// <summary> /// 区切り線と重複要素の対策 /// </summary> /// <param name="list">改変対象の配列</param> void CustomizeList(List<string> list) { // 配列の複製 list.AddRange(extend.list); for (var i = 0; i < list.Count; i++) {// 表記変更 if (list[i] == null) {// null list[i] = "\"Null\""; } if (list[i] == string.Empty) {// 空文字 list[i] = "\"\""; } if (Regex.IsMatch(list[i], "^ +$")) {// 半角スペースのみ list[i] = list[i].Replace(list[i], $"\"{list[i]}\""); } if (Regex.IsMatch(list[i], "^/")) {// 先頭に半角スラッシュ list[i] = Regex.Replace(list[i], "^", "\"\""); } if (Regex.IsMatch(list[i], "^ +/")) {// 半角スペースと半角スラッシュ list[i] = Regex.Replace(list[i], "^ +", "\"$0\""); } if (Regex.IsMatch(list[i], "//")) {// 連続半角スラッシュ list[i] = Regex.Replace(list[i], "//", "/\"\"/"); } if (Regex.IsMatch(list[i], "/ +/")) {// 半角スペースの間に半角スペースのみ list[i] = Regex.Replace(list[i], "/( +)/", "/\"$1\"/"); } if (Regex.IsMatch(list[i], "/$")) {// 末尾に半角スラッシュ list[i] = Regex.Replace(list[i], "$", "\"\""); } if (Regex.IsMatch(list[i], "/ +$")) {// 半角スラッシュと半角スペース list[i] = Regex.Replace(list[i], " +$", "\"$0\""); } } // 文字列パターンリスト var pattern = new List<string>(); for (var i = 0; i < list.Count; i++) {// 全要素チェック // 連番初期化 var count = 0; for (var j = i + 1; j < list.Count; j++) {// 重複チェック if (list[i] == list[j]) {// 完全一致 // 連番指定 count = pattern.Count((string s) => Regex.IsMatch(s, $"{list[i]}/*$")) + 1; // パターン記録 pattern.Add(list[i]); // 連番付与 list[j] = list[j].Replace(list[j], $"{list[j]}({count})"); } else if (Regex.IsMatch(list[i], $"^{list[j]}/")) {// 半角スラッシュ前の部分一致 count = pattern.Count((string s) => s == list[j]) + 1; pattern.Add(list[j]); list[j] = list[j].Replace(list[j], $"{list[j]}({count})"); } else if (Regex.IsMatch(list[j], $"^{list[i]}/")) {// 半角スラッシュがあれば部分一致 count = pattern.Count((string s) => s == list[j]) + 1; pattern.Add(list[i] + "/"); list[j] = list[j].Replace(list[i], $"{list[i]}({count})"); } } } } public override void OnInspectorGUI() {// Inspector に表示 // これ以降の要素に関してエディタによる変更を記録 EditorGUI.BeginChangeCheck(); // SerializedObject の内容を更新 serializedObject.Update(); // Inspector のコンポーネントに表示する項目名 var text = "List"; // ツールチップのテキスト var tooltip = "Popup list \"Item\" will be made out of this."; // ツールチップ入りラベルの作成 var label = new GUIContent(text, tooltip); // 配列の中身も表示する var includeChildren = true; // リストを取得 var property = serializedObject.FindProperty(nameof(extend.list)); // 配列フィールドの作成 EditorGUILayout.PropertyField(property, label, includeChildren); // SerializedObject の変更を適用 serializedObject.ApplyModifiedProperties(); // 複製先の配列 var list = new List<string>(); // 配列の複製と変更 CustomizeList(list); // 仕切り var divider = string.Empty; // 未選択状態 var unselected = "(Unselected)"; // リストに仕切りを追加 list.Add(divider); // 未選択状態を追加 list.Add(unselected); // Inspector のコンポーネントに表示する項目名 text = "Item"; // ツールチップのテキスト tooltip = "Select some items."; // ツールチップ入りラベルの作成 label = new GUIContent(text, tooltip); // 初期値として表示する項目のインデックス番号 var selectedIndex = extend.list.Length == 0 ? -1 : extend.index < 0 ? extend.list.Length + 1 : extend.index; // プルダウンメニューに登録する文字列配列 var displayOptions = list.ToArray(); // プルダウンメニューの作成 var index = extend.list.Length > 0 ? EditorGUILayout.Popup(label, selectedIndex, displayOptions) : -1; if (EditorGUI.EndChangeCheck()) {// 操作を Undo に登録 // Extend クラスの変更を記録 var objectToUndo = extend; // Undo メニューに表示する項目名 var name = "Extend"; // 記録準備 Undo.RecordObject(objectToUndo, name); // Undo に記録したい変数を登録 extend.index = index; } // 未選択状態のインデックス番号を -1 とする extend.index = index > extend.list.Length ? -1 : index; if ((extend.index != selectedIndex) && (extend.index >= 0)) {// 選択した項目のインデックス番号と項目名をログに出力 Debug.Log(extend.index); Debug.Log(extend.list[extend.index]); } } } // ここまでエディター上でのみ有効 #endif // 同一オブジェクトへの複数追加を禁止 [DisallowMultipleComponent] public class Extend : MonoBehaviour {// エディター拡張の中身 // リスト public string[] list = { }; // 初期値は“未選択” public int index = -1; }
上記のスクリプトによって生成されたドロップダウンメニューは,次のようになります。
重複要素の消失や,区切り線の問題が解消されています。
ラベルのツールチップ・未選択状態の追加・動的リスト等は,個人的な好みとして追加しております。
SerializedObject の代入に関しては,nameof
演算子を使用しています。
C# のバージョンが 5 以下であれば,次のように記述してください。
// リストを取得 var property = serializedObject.FindProperty("list");
連番を付与する際に,文字列補間機能を使用しています。
C# のバージョンが 5 以下であれば,次のように記述してください。
// 連番付与 list[j] = list[j].Replace(list[j], list[j] + "(" + count + ")");
また今回は文字列のパターン一致を判定して置換するため,Regex.Ismatch()
とRegex.Replace()
を使用しております。
なぜ連番に不等号を使うのか
今回の方法では,連番を半角不等号<>
で囲っています。
その理由は,半角丸括弧()
・半角大括弧[]
・半角中括弧{}
等では不具合が生じるためです。
全角括弧類で囲ったり,半角井桁#
や半角アンダースコア_
で区切ったりするのも良いでしょう。
(2019/08/25)半角丸括弧()
を使用できる事が分かったため,上記の文章を取り消しました。
以上,択一選択式プルダウンメニューを階層化する方法でした。
複数選択式プルダウンメニューの場合は,こちらの記事をお読みください。