PrefabUtilityとEditorGUILayoutでViewクラス自動生成&自動アタッチする

突然ですが、みなさん効率化ってできていますか?
アプリボットでは、ルーチンワーク(Routine)を効率化し、新しいチャレンジ(Challenge)に時間を割く、ということを強く打ち出しています。

この考え方の詳細は弊社代表のブログを読んでいただければと思います。
「ベンチャー流働き方改革」

 

 

 

 

 

 

 

 

では、実際に効率化とはどのようなものなのでしょうか?

プログラマーの3大美徳の1つに「怠惰または怠慢(Laziness)」というものがあります。

繰り返し同じような説明や作業をしたりするのは非常に面倒で、面白くなく、(自動化等してしまわないと)時間の無駄、なので、ドキュメントにまとめたり自動化するなりして、人が割く時間を減らす努力が必要です。怠惰や怠慢という言葉が表すように、面倒なことをしたくない、なまけたい、面白くないことに時間を使いたくない、という意識が強い人ほど、このような気質があるので、良いプログラマーになりやすい、ということです。

本ブログ(てっくぼっと)の記事の中でも

Spring REST Docs によるAPI仕様書自動化

スプレッドシートによるAPIドキュメントの管理とクラスの自動生成

などは効率化を行ったわかりやすい例です。

また、

AssetBundle設計のとある形

これも設計から運用までどのようにサボる…もとい効率よくできるかを考えてみた、という例になります。

あまり大掛かりな機能でなくても、すごい技術を使ってなくても、作業時間が短縮されれば、それは価値のあることになります。

と、長い前置きで効率化の大切さを説明しつつハードルを下げたところで、Unityで自動化・効率化といえばでおなじみの、エディタ拡張を利用した例をご紹介したいと思います。

 

■経緯
プロジェクト固有の事情からUnityエディタ上での操作はあまり推奨されていなかった。
UI(uGUI)は画面毎などの大きい単位でプレハブをつくっていたが、上記により スクリプトでの操作が主で、 GameObject名で探すような実装になっていた。

■問題点
検索するためのkey(GameObject名)を手動でコード上に書かなくてはいけない状況で、タイプミスなど、余計な手間暇がかかるし(そもそもタイプしなきゃいけない時点で手間)、コード上にkeyを書く(定義する)ので煩雑にもなる。
また、プレハブの構成はプレハブをつくってくれたデザイナさんがテキストベースで一覧をくれるが、結局プレハブ自体を目で見て探さないとよくわからないことが多い

■対応その1
とりあえずその時の状況的に、Unityエディタでの操作を制限するメリットも少なかったので、単純にプレハブに1つコンポーネント(Viewと呼んでいます)を追加し、そのコンポーネントにスクリプトで操作するUIをアタッチする、といったUnityではオーソドックスなやり方を提案。

結果、コードの見た目やばらつきはスッキリし、タイプミスの心配もなくなりました。ただ、プレハブの中身を探さないとよくわからない、という点は解決しないままです。また要素が多いと、Unityエディタで該当するものをアタッチするのもかなり面倒という状態に。

■対応その2
ということで、プレハブの中身を解析して、スクリプトでよく使うUI等を抽出し、
 ・Viewクラスのソースコードを自動生成
 ・生成したViewクラスをプレハブのコンポーネントに追加し、
         ワンクリックで必要な要素にアタッチする機能

があれば、楽になると思ったのでつくってみました。

仕上がりはこんな感じです。このギフトUIモドキを例に実装方法を紹介します。

■実装その1:PrefabUtility

まず、Prefabの中身を解析するのにPrefabUtilityを使います。

スクリプトリファレンスはこちら:

https://docs.unity3d.com/ja/current/ScriptReference/PrefabUtility.html

GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(/*path*/);

GameObject prefab = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;

・

・(GameObject名を抽出しソースコードを出力)

・

GameObject.DestroyImmediate(prefab);

これでプレハブのGameObjectを取得することができたので、GetComponentsInChildrenを利用して、Button,Text,Toggle,Animatorなど、必要なコンポーネントのGameObject名を抽出します。

 

抽出したGameObject名をもとに以下のようなソースコードを出力します。

    public partial class PopupGiftView : MonoBehaviour
    {
        public Text TitleTxt { get { return titleTxt; } }
        public Text LBtnTxt { get { return lBtnTxt; } }
        public Text RBtnTxt { get { return rBtnTxt; } }
        public Text CenterBtnTxt { get { return centerBtnTxt; } }
        public Text SortBtnTxt { get { return sortBtnTxt; } }
        public Button CenterBtn { get { return centerBtn; } }
        public Button CloseBtn { get { return closeBtn; } }
        public Button SortBtn { get { return sortBtn; } }
        public Toggle LBtn { get { return lBtn; } }
        public Toggle RBtn { get { return rBtn; } }
        public Animator PopupGift { get { return popupGift; } }
        public Animator TabRoot { get { return tabRoot; } }


        [SerializeField]
        private Text titleTxt = null;
        [SerializeField]
        private Text lBtnTxt = null;
        [SerializeField]
        private Text rBtnTxt = null;
        [SerializeField]
        private Text centerBtnTxt = null;
        [SerializeField]
        private Text sortBtnTxt = null;
        [SerializeField]
        private Button centerBtn = null;
        [SerializeField]
        private Button closeBtn = null;
        [SerializeField]
        private Button sortBtn = null;
        [SerializeField]
        private Toggle lBtn = null;
        [SerializeField]
        private Toggle rBtn = null;
        [SerializeField]
        private Animator popupGift = null;
        [SerializeField]
        private Animator tabRoot = null;
    }

■実装その2:EditorGUILayout
viewクラスが生成されるようになったので、次にこのviewコンポーネントへのアタッチを自動化していきます。

CustomEditor属性をつけ、Editorクラスを継承したクラスをview毎につくります。
Editorのメンバであるtarget(先程生成したviewクラス)のプロパティにプレハブから取得したコンポーネントを設定します。

スクリプトリファレンスはこちら:

https://docs.unity3d.com/jp/current/ScriptReference/Editor.html

このクラスをviewクラスのサブクラスとしてソースコードを出力します。

#if UNITY_EDITOR
        [CanEditMultipleObjects]
        [CustomEditor(typeof(view.PopupGiftView))]
        public class PopupGiftViewInspector : Editor
        {
            public override void OnInspectorGUI()
            {
                serializedObject.Update();
                DrawProperties();
                serializedObject.ApplyModifiedProperties();
            }

            void DrawProperties()
            {
                EditorGUILayout.BeginVertical();

                if(GUILayout.Button("Reset", GUILayout.Width(50f)))
                {
                    view.PopupGiftView obj = target as view.PopupGiftView;
                    Undo.RecordObject(obj, "CustomSize Reset");

                    obj.titleTxt = GetComponent<Text>("title_txt");
                    obj.lBtnTxt = GetComponent<Text>("l_btn_txt");
                    obj.rBtnTxt = GetComponent<Text>("r_btn_txt");
                    obj.centerBtnTxt = GetComponent<Text>("center_btn_txt");
                    obj.sortBtnTxt = GetComponent<Text>("sort_btn_txt");
                    obj.centerBtn = GetComponent<Button>("center_btn");
                    obj.closeBtn = GetComponent<Button>("close_btn");
                    obj.sortBtn = GetComponent<Button>("sort_btn");
                    obj.lBtn = GetComponent<Toggle>("L_btn");
                    obj.rBtn = GetComponent<Toggle>("R_btn");
                    obj.popupGift = GetComponent<Animator>("popup_gift");
                    obj.tabRoot = GetComponent<Animator>("tab_root");

                    EditorUtility.SetDirty(target);
                }

                EditorGUILayout.PropertyField(serializedObject.FindProperty("titleTxt"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("lBtnTxt"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("rBtnTxt"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("centerBtnTxt"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("sortBtnTxt"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("centerBtn"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("closeBtn"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("sortBtn"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("lBtn"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("rBtn"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("popupGift"));
                EditorGUILayout.PropertyField(serializedObject.FindProperty("tabRoot"));

                EditorGUILayout.EndVertical();
            }

            T GetComponent<T>(string name)
            {
                Transform textObj = GameObject.Find(name).transform;
                return textObj.GetComponent<T>();
            }
        }
#endif

これで、図のようなボタン押下でコンポーネントがアタッチされるようになります。

 

インスペクタ上でアタッチされたコンポーネントをクリックすると、ヒエラルキー上でどの階層にあるのか教えてくれるので、目で探さないとわからない」という問題も解決できました。

■ルール

実装方法から察した方もいるかもしれませんが、この方法だとプレハブ内から抽出するGameObject名はユニークにしなければなりません。幸いにもユニークになっていないGameObjectはそもそも別プレハブにするべきものが多く、プレハブの作り直しに大きなコストはかかりませんでした。

このように、効率化は時にルール化とセットになります。窮屈にならない程度のルール決めもしっかり考えていきましょう。

■まとめ

この対応で、面倒だなと思う作業が大分なくなりました。

ここまでくると更に
・Button等のタッチできる要素を全て無効にする機能
とか拡張できる新しい要素も思いつくようになってきます。(まだやってないけど)

といった感じで小さな改善でも積み重ねていくと、大きな成果につながることも少なくありません。
こういった効率化は、ついつい後回しにしてしまうことも多いですが、日々の作業の中で当たり前のように考え、当たり前のように対応していけるエンジニアを目指したいですね。