Missingになっているコンポーネントを一括削除する方法です。
Unity2018.4.0
- コンポーネントがMissingとは?
- 二つ方法があるけどスマートな解決策はない
- Unity2019では専用のメソッドが用意されている
- Unity2018で対応した方法
- 使い方
- ログ出力したいだけの場合
- 参考
コンポーネントがMissingとは?
GameObjectにアタッチされたスクリプトが削除されると、実行すべきスクリプトが見つからずMissing表示になります。
この状態になると実行時に次のような警告が出力されます。
例えばこのスクリプトがアタッチされたPrefabが大量に生成されたら、
ログ出力が大量に発生してパフォーマンス低下・・ということもありそうです。
この記事ではこのようにMissingになっているスクリプトを一括でデタッチする方法を紹介します。
二つ方法があるけどスマートな解決策はない
さてこの現象の解決方法を調べると、大きく二つの方法が議論されています。
一つ目はSerializedObjectを編集してしまう方法です。
ただ記事にも書いてありますが、Sceneにこの方法を適用すると、正常にMissingは解消されるものの、Unityがクラッシュしてしまいます。
実際試してみたところ、どうやらこの方法で編集したSceneを閉じてから他のSceneを閉じるとクラッシュするようです。
二つ目の方法は、Missingなコンポーネントを洗い出してSelection.objects
に加える方法です。
対象のコンポーネントを選択中状態にし、歯車マークから一括でRemove Componentを行います。
この方法はクラッシュはしませんが、結局Remove Componentは手動で行う必要があります。
しかも複数のシーンを一括で処理したりしづらそうです。
Unity2019では専用のメソッドが用意されている
こんな感じでスマートな解決策がないので、Unity2019では専用のメソッドが用意されたみたいです。
その名もGameObjectUtility.RemoveMonoBehavioursWithMissingScript
。
実にスマートに実装できそうなメソッド名です。
しかしUnity2018では使えないので、今回は検証していません。
2021.9.30追記: ↑のAPIについて改めて以下の記事で検証しました。
【Unity】【エディタ】Missingになっているコンポーネントを取得・削除する - LIGHT11
次節では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なコンポーネントがあればデタッチされます。
また、変更が加わった場合には最後に再起動処理が走ります。
ログ出力したいだけの場合
MissingしているコンポーネントがアタッチされているGameObjectをログ出力したいだけならこんな感じです。
using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using System.Collections.Generic; public static class MissingComponentUtility { [MenuItem("Assets/Log Missing Component Holders On Prefabs")] public static void LogMissingComponentHoldersOnPrefabs() { var prefabPaths = AssetDatabase .FindAssets("t:Prefab") .Select(x => AssetDatabase.GUIDToAssetPath(x)) .ToArray(); foreach (var prefabPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); var holders = GetMissingComponentHolders(prefab); foreach (var holder in holders) { Debug.Log($"{holder} (Prefab: {prefab.name})"); } } } [MenuItem("Assets/Log Missing Component Holders On Scenes")] public static void LogMissingComponentHoldersOnScenes() { var scenePaths = AssetDatabase .FindAssets("t:Scene") .Select(x => AssetDatabase.GUIDToAssetPath(x)) .ToArray(); foreach (var scenePath in scenePaths) { var scene = EditorSceneManager.OpenScene(scenePath); foreach (var gameObject in scene.GetRootGameObjects()) { var holders = GetMissingComponentHolders(gameObject); foreach (var holder in holders) { Debug.Log($"{holder} (Scene: {scenePath} / GameObject: {gameObject.name})"); } } } } private static List<string> GetMissingComponentHolders(GameObject go) { var result = new List<string>(); var gameObjects = go .GetComponentsInChildren<Transform>(true) .Select(x => x.gameObject); foreach (var gameObject in gameObjects) { if (HasMissingComponent(gameObject)) { result.Add(gameObject.name); } } return result; } private static bool HasMissingComponent(GameObject gameObject) { var components = gameObject.GetComponents<Component>(); for (int i = 0; i < components.Length; i++) { if (components[i] == null) { return true; } } return false; } }