読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

【Unity】Missingがあるアセットを検索する

今回担当させていただく大橋です。
Unityで、Missing状態のプロパティを検索するエディタ拡張を作ってみました。
よろしくお願いします。

記事の最後に全コードを載せてますので、
とにかくコードを!という方はそちらをどうぞ。

動作確認したUnityのバージョンは5.0です。

なかなか気づかないMissing

あるアセットのプロパティに、別のアセットをセットしている場合、
そのセットしているアセット自体が削除されると、Missing状態になります。

MissingSearch_01_01

例えば、3Dオブジェクトにセットしていたマテリアルを削除すると、
その3Dオブジェクトは全身ピンク色になってしまいます。

MissingSearch_02

このこと自体は特にエラーにならないのですが、
逆にエラーが出ないので、実行してみて初めてピンク色になってることに気づいたり、
最悪、実行してもMissingに気づかないこともあり得ます。

ということで、Missing状態になってるものを検索してリスト表示するものをエディタ拡張で作ってみたいと思います。

まずはフォルダ作成

エディタ拡張のスクリプトを入れるEditorフォルダを作っておきます。
下の画像ではAssets直下に置いてますが、任意の場所にどうぞ。

MissingSearch_03

検索結果を表示するウィンドウを作る

とりあえず空のウィンドウを表示するようにしてみます。
先ほど作ったEditorフォルダ内にMissingListWindow.csを作成します。

そしてコードはこんな感じ。

using UnityEngine;  
using UnityEditor;  
using System.Collections;  
  
public class MissingListWindow : EditorWindow {  
  
    [MenuItem("Assets/MissingList")]  
    private static void ShowMissingList() {  
        // ウィンドウを表示  
        var window = GetWindow<MissingListWindow> ();  
        window.minSize = new Vector2 (900, 300);  
    }  
  
}  

UnityのAssetsメニューに「MissingList」が追加されたはずです。
選択するとウィンドウが表示されます。まだ中身は空です。

MissingSearch_04_01

MissingSearch_05

すべてのアセットを取得する

Missingを検索するには、まずプロジェクト内の全てのアセットを検索しないといけません。
これは、AssetDatabase.GetAllAssetPaths()を使ってできます。

ということで、MissingListWindowに、全てのアセットを検索する処理を追加します。

    [MenuItem("Assets/MissingList")]  
    private static void ShowMissingList() {  
        // 全てのアセットを検索  
        Search ();  
  
        // ウィンドウを表示  
        var window = GetWindow<MissingListWindow> ();  
        window.minSize = new Vector2 (900, 300);  
    }  
  
    private static void Search() {  
        // 全てのアセットのファイルパスを取得  
        string[] allPaths = AssetDatabase.GetAllAssetPaths ();  
        int length = allPaths.Length;  
  
        for (int i = 0; i < length; i++) {  
            // プログレスバーを表示  
            EditorUtility.DisplayProgressBar("Search Missing", string.Format("{0}/{1}", i+1, length), (float)i / length);  
  
            // ここでアセットにMissingが含まれてるかどうかチェックしたい  
            Debug.Log ("Asset:" + allPaths [i]);  
        }  
  
        // プログレスバーを消す  
        EditorUtility.ClearProgressBar ();  
    }  

Missing状態のプロパティを入れておくデータ

全アセットを検索できるようになったので、
次は、各アセットにMissingが含まれてないかチェックする処理を作りたいですが、
その前に、発見したMissing状態のプロパティをリストアップするために、
該当プロパティを覚えておくためのデータクラスを作っておきます。

Editorフォルダに、AssetParameterData.csを作成します。
Missingプロパティを発見したら、AssetParameterDataにプロパティの情報を入れて、
覚えておくことにします。

using UnityEngine;  
using UnityEditor;  
using System.Collections;  
  
public class AssetParameterData {  
    public UnityEngine.Object obj { get; set; }         //!< アセットのObject自体  
    public string path { get; set; }                    //!< アセットのパス  
    public SerializedProperty property { get; set; }    //!< プロパティ  
}  

Missing状態の検出

それでは、Missingを検索する部分をMissingListWindowに追加していきます。

まずは、必要なnamespaceの追加です。

using System.Collections.Generic;  
using System.Linq;  
using System.IO;  

検索対象の拡張子と、Missing状態のプロパティを覚えておくリストを定義します。

    private static string[] extensions = {".prefab", ".mat", ".controller", ".cs", ".shader", ".mask", ".asset"};  
    private static List<AssetParameterData> missingList = new List<AssetParameterData>();  

そして、Missingの検出関数です。

    private static void SearchMissing(string path) {  
        // 指定パスのアセットを全て取得  
        IEnumerable<unityengine .Object> assets = AssetDatabase.LoadAllAssetsAtPath (path);  
  
        // 各アセットについて、Missingのプロパティがあるかチェック  
        foreach (UnityEngine.Object obj in assets) {  
            if (obj == null) {  
                continue;  
            }  
            if (obj.name == "Deprecated EditorExtensionImpl") {  
                continue;  
            }  
  
            // SerializedObjectを通してアセットのプロパティを取得する  
            SerializedObject sobj = new SerializedObject (obj);  
            SerializedProperty property = sobj.GetIterator ();  
  
            while (property.Next (true)) {  
                // プロパティの種類がオブジェクト(アセット)への参照で、  
                // その参照がnullなのにもかかわらず、参照先インスタンスIDが0でないものはMissing状態!  
                if (property.propertyType == SerializedPropertyType.ObjectReference &&
                    property.objectReferenceValue == null &&
                    property.objectReferenceInstanceIDValue != 0) {  
  
                    // Missing状態のプロパティリストに追加する  
                    missingList.Add (new AssetParameterData () {  
                        obj = obj,  
                        path = path,  
                        property = property  
                    });  
                }  
            }  
        }  
    }  

この関数は、引数で指定されたパスのアセットにMissingが含まれているかチェックして、
Missingを発見したらリストに追加します。

アセットのプロパティへのアクセスは、SerializedObjectとSerializedPropertyを使います。
これで、全てのプロパティをチェックできます。

そしてMissing状態の判定ですが、次の条件を満たすプロパティがMissingなはずです。
・プロパティの種類がオブジェクト(アセット)への参照
・その参照がnull
・参照先インスタンスIDが0でない

どうやら、参照先がnullのとき、参照先インスタンスIDが0なら、それはまだセットされてないもので、
0じゃなければMissing状態のようです。

この関数は、先ほど作ったMissingListWindow.Search()内から呼ぶようにします。

            // ここでアセットにMissingが含まれてるかどうかチェックしたい  
            Debug.Log ("Asset:" + allPaths [i]);  

この部分を、次のように書き換えます。

            if (extensions.Contains (Path.GetExtension (allPaths [i]))) {  
                SearchMissing (allPaths [i]);  
            }  

発見したMissingリストの表示

では最後に、検索結果をウィンドウに表示するようにします。
MissingListWindow.OnGUI()を追加です。

    private Vector2 scrollPos;  
  
    private void OnGUI() {  
        // 列見出し  
        EditorGUILayout.BeginHorizontal ();  
        EditorGUILayout.LabelField ("Asset", GUILayout.Width (200));  
        EditorGUILayout.LabelField ("Property", GUILayout.Width (200));  
        EditorGUILayout.LabelField ("Path");  
        EditorGUILayout.EndHorizontal ();  
  
        // リスト表示  
        scrollPos = EditorGUILayout.BeginScrollView (scrollPos);  
  
        foreach (AssetParameterData data in missingList) {  
            EditorGUILayout.BeginHorizontal ();  
            EditorGUILayout.ObjectField (data.obj, data.obj.GetType (), true, GUILayout.Width (200));  
            EditorGUILayout.TextField (data.property.name, GUILayout.Width (200));  
            EditorGUILayout.TextField (data.path);  
            EditorGUILayout.EndHorizontal ();  
        }  
        EditorGUILayout.EndScrollView ();  
    }  

これで、検索結果が表示されます。完成!

MissingSearch_10

全コード

using UnityEngine;  
using UnityEditor;  
using System.Collections;  
using System.Collections.Generic;  
using System.Linq;  
using System.IO;  
  
public class MissingListWindow : EditorWindow {  
    private static string[] extensions = {".prefab", ".mat", ".controller", ".cs", ".shader", ".mask", ".asset"};  
  
    private static List<AssetParameterData> missingList = new List<AssetParameterData>();  
    private Vector2 scrollPos;  
  
    /// <summary>  
    /// Missingがあるアセットを検索してそのリストを表示する  
    /// </summary>  
    [MenuItem("Assets/MissingList")]  
    private static void ShowMissingList() {  
        // Missingがあるアセットを検索  
        Search ();  
  
        // ウィンドウを表示  
        var window = GetWindow<MissingListWindow> ();  
        window.minSize = new Vector2 (900, 300);  
    }  
  
    /// <summary>  
    /// Missingがあるアセットを検索  
    /// </summary>  
    private static void Search() {  
        // 全てのアセットのファイルパスを取得  
        string[] allPaths = AssetDatabase.GetAllAssetPaths ();  
        int length = allPaths.Length;  
  
        for (int i = 0; i < length; i++) {  
            // プログレスバーを表示  
            EditorUtility.DisplayProgressBar("Search Missing", string.Format("{0}/{1}", i+1, length), (float)i / length);  
  
            // Missing状態のプロパティを検索  
            if (extensions.Contains (Path.GetExtension (allPaths [i]))) {  
                SearchMissing (allPaths [i]);  
            }  
        }  
  
        // プログレスバーを消す  
        EditorUtility.ClearProgressBar ();  
    }  
  
    /// <summary>  
    /// 指定アセットにMissingのプロパティがあれば、それをmissingListに追加する  
    /// </summary>  
    /// <param name="path">Path.</param>  
    private static void SearchMissing(string path) {  
        // 指定パスのアセットを全て取得  
        IEnumerable<UnityEngine.Object> assets = AssetDatabase.LoadAllAssetsAtPath (path);  
  
        // 各アセットについて、Missingのプロパティがあるかチェック  
        foreach (UnityEngine.Object obj in assets) {  
            if (obj == null) {  
                continue;  
            }  
            if (obj.name == "Deprecated EditorExtensionImpl") {  
                continue;  
            }  
  
            // SerializedObjectを通してアセットのプロパティを取得する  
            SerializedObject sobj = new SerializedObject (obj);  
            SerializedProperty property = sobj.GetIterator ();  
  
            while (property.Next (true)) {  
                // プロパティの種類がオブジェクト(アセット)への参照で、  
                // その参照がnullなのにもかかわらず、参照先インスタンスIDが0でないものはMissing状態!  
                if (property.propertyType == SerializedPropertyType.ObjectReference &&
                    property.objectReferenceValue == null &&
                    property.objectReferenceInstanceIDValue != 0) {  
  
                    // Missing状態のプロパティリストに追加する  
                    missingList.Add (new AssetParameterData () {  
                        obj = obj,  
                        path = path,  
                        property = property  
                    });  
                }  
            }  
        }  
    }  
          
    /// <summary>  
    /// Missingのリストを表示  
    /// </summary>  
    private void OnGUI() {  
        // 列見出し  
        EditorGUILayout.BeginHorizontal ();  
        EditorGUILayout.LabelField ("Asset", GUILayout.Width (200));  
        EditorGUILayout.LabelField ("Property", GUILayout.Width (200));  
        EditorGUILayout.LabelField ("Path");  
        EditorGUILayout.EndHorizontal ();  
  
        // リスト表示  
        scrollPos = EditorGUILayout.BeginScrollView (scrollPos);  
  
        foreach (AssetParameterData data in missingList) {  
            EditorGUILayout.BeginHorizontal ();  
            EditorGUILayout.ObjectField (data.obj, data.obj.GetType (), true, GUILayout.Width (200));  
            EditorGUILayout.TextField (data.property.name, GUILayout.Width (200));  
            EditorGUILayout.TextField (data.path);  
            EditorGUILayout.EndHorizontal ();  
        }  
        EditorGUILayout.EndScrollView ();  
    }  
}  
using UnityEngine;  
using UnityEditor;  
using System.Collections;  
  
public class AssetParameterData {  
    public UnityEngine.Object obj { get; set; }            //!< アセットのObject自体  
    public string path { get; set; }                    //!< アセットのパス  
    public SerializedProperty property { get; set; }    //!< プロパティ  
}