ヨイチカの冒険譚

日々の思い付きを日記のように書き留めていく

自動バリデーションの実装

シーン内のMissingをチェックする

f:id:yoitika:20201216102812p:plain

ソースコード

namespace EditorTest
{
    public class MissingOrNone : EditorWindow
    {
        //初期チェック
        private bool _ini = false;
        
        //スクロール位置
        private Vector2 _scrollPosition = Vector2.zero;
        
        //エディターウィンドウの作成
        [MenuItem("Tool/MissingOrNoneCheck")]
        public static void OpenWindow()
        {
            MissingOrNone window = (MissingOrNone)GetWindow(typeof(MissingOrNone));
            window.Show();
        }
        
        private void OnGUI()
        {
            GUI.contentColor = new Color(0.85f, 0.25f, 0.22f);
            
             GUILayout.Label("Missing None を直してください!!", GUILayout.Height(30));
             
             if (!_ini)
             {
                 _ini = true;
                 return;
             }

             //描画領域が足りないときにスクロールする
             _scrollPosition = GUILayout.BeginScrollView(_scrollPosition);
             
             //ミッシング None を表示する
             GameObject[] hierarchyObjects = HierarchyGetter.GetHierarchyObject();
                         
             foreach (var hierarchyObject in hierarchyObjects)
             {
                 MissingGetter.MissingButton(hierarchyObject);
             }
             
             //スクロールのエンド位置
             GUILayout.EndScrollView();
        }
    }


namespace EditorTest
{
    public class HierarchyGetter
    {
        
        /// <summary>
        /// ヒエラルキーのオブジェクトをすべて取得する
        /// Resourcesからオブジェクトを取得している
        /// 取得ができない場合はnullを返してエラーを吐く
        /// </summary>
        /// <returns></returns>
        public static GameObject[] GetHierarchyObject()
        {
            GameObject[] hierarchyObject = Resources.FindObjectsOfTypeAll<GameObject>();

            if (hierarchyObject == null)
            {
                Debug.LogError("ヒエラルキーのオブジェクトを取得できませんでした。");
                return null;
            }
            
            return hierarchyObject;
        }
    }
}

namespace EditorTest
{
    public static class MissingGetter
    {
        //Unityを開いたときに実行される
        [InitializeOnLoadMethod]
        public static void SceneSetting()
        {
            //シーン保存時にミッシングを検索してダイアログを出すようにする
            EditorSceneManager.sceneSaving += MissingGet;
        }
        
        
        /// <summary>
        /// シーン保存時に実行する処理
        /// ウィンドウを表示する
        /// </summary>
        /// <param name="scene"></param>
        /// <param name="path"></param>
        static void MissingGet(Scene scene,string path)
        {
            MissingOrNone.OpenWindow();
        }
        
        

        /// <summary>
        /// ヒエラルキーからMissingとNoneを見つけてリスト化して表示する
        /// ボタンを生成してMissingやNoneを選択できるようになっている
        /// </summary>
        /// <param name="hierarchyObject"></param>
        public static void MissingButton(GameObject hierarchyObject)
        {
           var components = hierarchyObject.GetComponents<Component>().Where(c => c != null);
           
           foreach (var component in components)
           {
               var so = new SerializedObject(component);
               var sp = so.GetIterator();

               while (sp.NextVisible(true))
               {
                   //エラーチェック
                   if(sp.propertyType != SerializedPropertyType.ObjectReference) continue;
                   if(sp.objectReferenceValue != null) continue;
                   if(!sp.hasChildren) continue;
                   
                   
                   var fileId = sp.FindPropertyRelative("m_FileID");
                   if(fileId == null) continue;
                   
                   //fileIDが0の場合はNoneのためNoneをウィンドウに表示する
                   if (fileId.intValue == 0)
                   {
                       continue;
                   }
                   
                   
                   
                   //一つ一つをボックスとして扱う
                   GUILayout.BeginHorizontal(GUI.skin.box);
                   
                   
                   
                     //色を変える  背景を青に近い色にして見やすく
                     //文字を白くして読みやすくしています
                    GUI.backgroundColor = new Color(0.38f, 0.54f, 1f);
                    GUI.contentColor = Color.white;

                    
                    
                   //fileIDがあり、中身がない場合MissingになっているためMissingをウィンドウに表示する
                   GUILayout.Label($"{component.gameObject.name}のMissing");
                   
                   
                   if (GUILayout.Button($"選択",GUILayout.Width(100)))
                   {
                       Selection.activeObject = component.gameObject;
                   }
                   
                   
                   GUILayout.EndHorizontal();
               }
           } 
        }
    }
}


自動バリデーションとは

バリデーションとは簡単に言えば入力値のチェックのことを言います。
そして自動バリデーションは入力値のチェックを自動で行ってくれる仕組みのことを言います。
今回の実装ではシーン保存時に自動的にヒエラルキー内のオブジェクトを探索してMissingObjectがあった場合に専用windowを開いてMissingを選択できるようにしています。


シーン保存時に処理を実行する

シーン保存時に処理を実行したい場合はUnityが用意しているイベントを使うことで簡単にできます。
        public static void SceneSetting()
        {
            //シーン保存時にミッシングを検索してダイアログを出すようにする
            EditorSceneManager.sceneSaving += MissingGet;
        }
[Unityが用意してくれているイベント](https://docs.unity3d.com/ja/2018.4/ScriptReference/SceneManagement.EditorSceneManager.html)
これで登録するためのメソッドは用意できます。ですが、これを実行時に追加しても意味がありません。そのためUnity起動時に追加するようにします。
Unity起動時に処理を実行するためにはメソッドに以下の属性をつけてあげます。
`[InitializeOnLoadMethod]`
イベント登録の全体コード
 //Unityを開いたときに実行される
        [InitializeOnLoadMethod]
        public static void SceneSetting()
        {
            //シーン保存時にミッシングを検索してダイアログを出すようにする
            EditorSceneManager.sceneSaving += MissingGet;
        }
        
        
        /// <summary>
        /// シーン保存時に実行する処理
        /// ウィンドウを表示する
        /// </summary>
        /// <param name="scene"></param>
        /// <param name="path"></param>
        static void MissingGet(Scene scene,string path)
        {
            MissingOrNone.OpenWindow();
        }


Missingを見つけてウィンドウを表示する

メインの処理であるMissingを見つけてウィンドウを表示する処理です。
そもそもですが、Missingとはオブジェクトがアタッチされていたものが消えた際に発生します。


ヒエラルキー内のオブジェクトを探索する
今回はヒエラルキー内のオブジェクトのアクティブ、非アクティブ関係なく探索したいため Resources.FindObjectsOfTypeAll<>()を使い実装しました。
処理内容はヒエラルキー内のゲームオブジェクトを配列に格納して渡すようにしています。
メソッド
public static GameObject[] GetHierarchyObject()
        {
            GameObject[] hierarchyObject = Resources.FindObjectsOfTypeAll<GameObject>();

        if (hierarchyObject == null)
        {
            Debug.LogError(&#34;ヒエラルキーのオブジェクトを取得できませんでした。&#34;);
            return null;
        }

        return hierarchyObject;
    }</pre>


Missingを表示する
ヒエラルキーから得られたオブジェクトに対してMissingがあるかを見てあった場合はwindowに追加するようにしています。
メソッド
  public static void MissingButton(GameObject hierarchyObject)
        {
           var components = hierarchyObject.GetComponents<Component>().Where(c => c != null);

       foreach (var component in components)
       {
           var so = new SerializedObject(component);
           var sp = so.GetIterator();

           while (sp.NextVisible(true))
           {
               //エラーチェック
               if(sp.propertyType != SerializedPropertyType.ObjectReference) continue;
               if(sp.objectReferenceValue != null) continue;
               if(!sp.hasChildren) continue;


               var fileId = sp.FindPropertyRelative(&#34;m_FileID&#34;);
               if(fileId == null) continue;

               //fileIDが0の場合はNoneのためNoneをウィンドウに表示する
               if (fileId.intValue == 0)
               {
                   continue;
               }



               //一つ一つをボックスとして扱う
               GUILayout.BeginHorizontal(GUI.skin.box);



                 //色を変える  背景を青に近い色にして見やすく
                 //文字を白くして読みやすくしています
                GUI.backgroundColor = new Color(0.38f, 0.54f, 1f);
                GUI.contentColor = Color.white;



               //fileIDがあり、中身がない場合MissingになっているためMissingをウィンドウに表示する
               GUILayout.Label($&#34;{component.gameObject.name}のMissing&#34;);


               if (GUILayout.Button($&#34;選択&#34;,GUILayout.Width(100)))
               {
                   Selection.activeObject = component.gameObject;
               }


               GUILayout.EndHorizontal();
           }
       } 
    }</pre>


windowを表示する
ここはおまけみたいなものですがエディタ拡張でMissingを表示するためのwindowを表示します。
windowを表示
 public class MissingWindow : EditorWindow
    {
        //初期チェック
        private bool _ini = false;

    //スクロール位置
    private Vector2 _scrollPosition = Vector2.zero;

    //エディターウィンドウの作成
    [MenuItem(&#34;Tool/MissingOrNoneCheck&#34;)]
    public static void OpenWindow()
    {
        MissingWindow window = (MissingWindow)GetWindow(typeof(MissingWindow));
        window.Show();
    }

    private void OnGUI()
    {
        GUI.contentColor = new Color(0.85f, 0.25f, 0.22f);

         GUILayout.Label(&#34;Missing None を直してください!!&#34;, GUILayout.Height(30));

         if (!_ini)
         {
             _ini = true;
             return;
         }

         //描画領域が足りないときにスクロールする
         _scrollPosition = GUILayout.BeginScrollView(_scrollPosition);

         //ミッシングを表示する
         GameObject[] hierarchyObjects = HierarchyGetter.GetHierarchyObject();

         foreach (var hierarchyObject in hierarchyObjects)
         {
             MissingGetter.MissingButton(hierarchyObject);
         }

         //スクロールのエンド位置
         GUILayout.EndScrollView();
    }
}</pre>

小技ですが、コードにある_iniの役割はメインの処理が長くなる時全体の表示も遅くなってしまうので一度描画してから処理が終わり次第表示するようにするためのものです。