【Unity】Missingになっているコンポーネントを一括削除する方法

Missingになっているコンポーネントを一括削除する方法です。

Unity2018.4.0

コンポーネントがMissingとは?

GameObjectにアタッチされたスクリプトが削除されると、実行すべきスクリプトが見つからずMissing表示になります。

f:id:halya_11:20190522200555p:plain

この状態になると実行時に次のような警告が出力されます。

f:id:halya_11:20190522200707p:plain

例えばこのスクリプトがアタッチされたPrefabが大量に生成されたら、
ログ出力が大量に発生してパフォーマンス低下・・ということもありそうです。

この記事ではこのようにMissingになっているスクリプトを一括でデタッチする方法を紹介します。

二つ方法があるけどスマートな解決策はない

さてこの現象の解決方法を調べると、大きく二つの方法が議論されています。

一つ目はSerializedObjectを編集してしまう方法です。

answers.unity.com

ただ記事にも書いてありますが、Sceneにこの方法を適用すると、正常にMissingは解消されるものの、Unityがクラッシュしてしまいます。
実際試してみたところ、どうやらこの方法で編集したSceneを閉じてから他のSceneを閉じるとクラッシュするようです。

二つ目の方法は、Missingなコンポーネントを洗い出してSelection.objectsに加える方法です。

mfyg.dk

対象のコンポーネントを選択中状態にし、歯車マークから一括でRemove Componentを行います。
この方法はクラッシュはしませんが、結局Remove Componentは手動で行う必要があります。
しかも複数のシーンを一括で処理したりしづらそうです。

Unity2019では専用のメソッドが用意されている

こんな感じでスマートな解決策がないので、Unity2019では専用のメソッドが用意されたみたいです。

docs.unity3d.com

その名もGameObjectUtility.RemoveMonoBehavioursWithMissingScript
実にスマートに実装できそうなメソッド名です。
しかしUnity2018では使えないので、今回は検証していません。

次節ではUnity2018で対応した方法を紹介します。

Unity2018で対応した方法

ベストな実装方法を色々と考えたのですが、結局以下の方針にしました。

  • SerializedObjectを編集する方法を使う
  • ただし編集したアセットを保存して他のシーンを開くと落ちる
  • そのためシーンはAdditiveモードで開いていって一連の処理中は閉じない
  • 全部の処理が終わった後にUnityを再起動することでクラッシュを防ぐ

かなり対症療法的ではありますが、Unityのクラッシュがどうにもならない以上仕方ないです。
ソースコードは下記の通りになりました。

using System.Diagnostics;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

public static class MissingComponentUtility
{
    /// <summary>
    ///    選択中のPrefabに置かれているGameObjectにアタッチされている
    ///    コンポーネントのうちMissingになっているものを削除する
    /// </summary>
    [MenuItem("Assets/Remove Missing Components On Prefabs")]
    public static void RemoveMissingComponentsOnPrefabs()
    {
        var prefabs = Selection
            .gameObjects
            .Where(x => AssetDatabase.GetAssetPath(x).EndsWith(".prefab"))
            .ToArray();
        var removedCount = 0;
        foreach (var prefab in prefabs) {
            removedCount += RemoveMissingComponentsInChildren(prefab);
        }
        if (removedCount >= 1) {
            // このままシーンを切り替えるとクラッシュするので再起動する
            var applicationPath  = EditorApplication.applicationPath;
            var args = "-projectPath " + Application.dataPath.Replace("/Assets", string.Empty);
            var startInfo = new ProcessStartInfo(applicationPath, args);
            Process.Start(startInfo);
            EditorApplication.Exit(0);
        }
    }

    /// <summary>
    ///    選択中のシーンに置かれているGameObjectにアタッチされている
    ///    コンポーネントのうちMissingになっているものを削除する
    /// </summary>
    [MenuItem("Assets/Remove Missing Components On Scenes")]
    public static void RemoveMissingComponentsOnScenes()
    {
        var scenePaths = Selection
            .objects
            .Select(x => AssetDatabase.GetAssetPath(x))
            .Where(x => x.EndsWith(".unity"))
            .ToArray();
        if (RemoveMissingComponentsInScenes(scenePaths) >= 1) {
            // このままシーンを切り替えるとクラッシュするので再起動する
            var applicationPath  = EditorApplication.applicationPath;
            var args = "-projectPath " + Application.dataPath.Replace("/Assets", string.Empty);
            var startInfo = new ProcessStartInfo(applicationPath, args);
            Process.Start(startInfo);
            EditorApplication.Exit(0);
        }
    }

    /// <summary>
    /// シーン上のMissingになっているコンポーネントをすべて削除する
    /// </summary>
    /// <returns>削除した数</returns>
    private static int RemoveMissingComponentsInScenes(string[] scenePaths)
    {
        var totalRemovedCount = 0;
        foreach (var scenePath in scenePaths) {
            // 編集したシーンを閉じて違うシーンを開くとクラッシュするので全てAdditiveで開く
            var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
            var removedCount = 0;
            foreach (var gameObject in scene.GetRootGameObjects()) {
                removedCount += RemoveMissingComponentsInChildren(gameObject);
            }
            totalRemovedCount += removedCount;

            if (removedCount >= 1) {
                EditorSceneManager.MarkSceneDirty(scene);
                EditorSceneManager.SaveScene(scene);
            }
        }

        return totalRemovedCount;
    }

    /// <summary>
    /// このPrefabにアタッチされているコンポーネントのうちMissingになっているものを削除する
    /// </summary>
    /// <returns>削除した数</returns>
    private static int RemoveMissingComponentsInChildren(GameObject prefab)
    {
        // インスタンス化
        var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
        // 全ての子オブジェクトを取得
        var gameObjects = instance
            .GetComponentsInChildren<Transform>(true)
            .Select(x => x.gameObject);

        var totalRemovedCount = 0;
        foreach (var gameObject in gameObjects) {
            var components = gameObject.GetComponents<Component>();
            var so = new SerializedObject(gameObject);
            so.Update();
            var prop = so.FindProperty("m_Component");

            int removedCount = 0;
            for(int i = 0; i < components.Length; i++)
            {
                if(components[i] == null)
                {
                    prop.DeleteArrayElementAtIndex(i - removedCount);
                    removedCount++;
                }
            }

            if (removedCount >= 1) {
                so.ApplyModifiedProperties();
            }
            so.Dispose();

            totalRemovedCount += removedCount;
        }
        // 変更を適用してインスタンスを破棄
        if (totalRemovedCount >= 1) {
            PrefabUtility.SaveAsPrefabAsset(instance, AssetDatabase.GetAssetPath(prefab));
        }
        GameObject.DestroyImmediate(instance);
        return totalRemovedCount;
    }
}

使い方

Prefabを選択した状態で右クリック > Remove Missing Components On Prefabs、
あるいはSceneを選択した状態で右クリック > Remove Missing Components On Scenesを選択することで
Missingなコンポーネントがあればデタッチされます。

f:id:halya_11:20190523220228p:plain

また、変更が加わった場合には最後に再起動処理が走ります。

参考

answers.unity.com mfyg.dk docs.unity3d.com