Search Unity

Bug Remove all missing components in prefabs

Discussion in 'Prefabs' started by Camarent, May 25, 2020.

  1. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    Hi!
    I am trying to figuring out how to automatically remove all missing components in prefabs.
    I found out script here: https://answers.unity.com/questions...o-scr.html?childToView=1614734#answer-1614734

    Here is the problem when I use it It is actually delete missing components but it produce external object ids in prefabs. So when scriptable build pipeline gather all reference for building by using AssetDatabase.
    LoadAllAssetRepresentationsAtPath it return this components as null subasset.
    I cannot find a way to delete it except manually parse yaml prefab asset and remove components. As I understand it is happening beacuse of using GameObjectUtility.RemoveMonoBehavioursWithMissingScript.

    Few screenshots to see the problem.

    1. Prefab after using script.
    upload_2020-5-25_14-47-18.png

    2. External ids appeared
    upload_2020-5-25_14-47-40.png

    3.Here my missing component that was added after removing by script. If I delete it will fix the problem.
    upload_2020-5-25_14-48-5.png

    4. Part of diff after using script.
    upload_2020-5-25_14-48-37.png
     
  2. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    So tried to use this script for clean null subassets afterwards: https://forum.unity.com/threads/deleting-null-sub-assets.466430/
    I modified to work with prefabs. It can delete null subassets but it cause all guids and ids inside prefab to regenerate because it use magic workaround. So I am not usre how stable it will work with nested and variants. I also see that some other objects have lost links to my prefabs.

    Code (CSharp):
    1.  
    2.         private static void FixMissingScript(Object toDelete)
    3.         {
    4.             //Create a new instance of the object to delete
    5.             var type = toDelete.GetType();
    6.             Object newInstance;
    7.             if(type == typeof(ScriptableObject))
    8.                 newInstance = CreateInstance(toDelete.GetType());
    9.             else if (type == typeof(GameObject) && PrefabUtility.IsPartOfPrefabAsset(toDelete))
    10.             {
    11.                 newInstance = Instantiate(toDelete);
    12.             }
    13.             else
    14.             {
    15.                 return;
    16.             }
    17.  
    18.             //Copy the original content to the new instance
    19.             EditorUtility.CopySerialized(toDelete, newInstance);
    20.             newInstance.name = toDelete.name;
    21.  
    22.             string toDeletePath = AssetDatabase.GetAssetPath(toDelete);
    23.             string clonePath = "";
    24.          
    25.             if(type == typeof(ScriptableObject))
    26.                 clonePath = toDeletePath.Replace(".asset", "CLONE.asset");
    27.             else if (type == typeof(GameObject) && toDeletePath.Contains(".prefab") && PrefabUtility.IsPartOfPrefabAsset(toDelete))
    28.             {
    29.                 clonePath = toDeletePath.Replace(".prefab", "CLONE.prefab");
    30.             }
    31.             else
    32.                 return;
    33.  
    34.             //Create the new asset on the project files
    35.             if (type == typeof(ScriptableObject))
    36.             {
    37.                 AssetDatabase.CreateAsset(newInstance, clonePath);
    38.             }
    39.             else if (type == typeof(GameObject) &&PrefabUtility.IsPartOfPrefabAsset(toDelete))
    40.             {
    41.                 PrefabUtility.SaveAsPrefabAsset(newInstance as GameObject, clonePath, out var success);
    42.                 if(!success)
    43.                     Debug.LogError($"cannot fix {newInstance.name}");
    44.             }
    45.  
    46.             AssetDatabase.ImportAsset(clonePath);
    47.  
    48.             //Unhide sub-assets
    49.             var subAssets = AssetDatabase.LoadAllAssetsAtPath(toDeletePath);
    50.             HideFlags[] flags = new HideFlags[subAssets.Length];
    51.             for (int i = 0; i < subAssets.Length; i++)
    52.             {
    53.                 //Ignore the "corrupt" one
    54.                 if (subAssets[i] == null)
    55.                     continue;
    56.  
    57.                 //Store the previous hide flag
    58.                 flags[i] = subAssets[i].hideFlags;
    59.                 subAssets[i].hideFlags = HideFlags.None;
    60.                 EditorUtility.SetDirty(subAssets[i]);
    61.             }
    62.  
    63.             EditorUtility.SetDirty(toDelete);
    64.             AssetDatabase.SaveAssets();
    65.  
    66.             //Reparent the subAssets to the new instance
    67.             foreach (var subAsset in AssetDatabase.LoadAllAssetRepresentationsAtPath(toDeletePath))
    68.             {
    69.                 //Ignore the "corrupt" one
    70.                 if (subAsset == null)
    71.                     continue;
    72.  
    73.                 //We need to remove the parent before setting a new one
    74.                 AssetDatabase.RemoveObjectFromAsset(subAsset);
    75.                 AssetDatabase.AddObjectToAsset(subAsset, newInstance);
    76.             }
    77.  
    78.             //Import both assets back to unity
    79.             AssetDatabase.ImportAsset(toDeletePath);
    80.             AssetDatabase.ImportAsset(clonePath);
    81.  
    82.             //Reset sub-asset flags
    83.             for (int i = 0; i < subAssets.Length; i++)
    84.             {
    85.                 //Ignore the "corrupt" one
    86.                 if (subAssets[i] == null)
    87.                     continue;
    88.  
    89.                 subAssets[i].hideFlags = flags[i];
    90.                 EditorUtility.SetDirty(subAssets[i]);
    91.             }
    92.  
    93.             EditorUtility.SetDirty(newInstance);
    94.             AssetDatabase.SaveAssets();
    95.  
    96.             //Here's the magic. First, we need the system path of the assets
    97.             string globalToDeletePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Application.dataPath), toDeletePath);
    98.             string globalClonePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Application.dataPath), clonePath);
    99.  
    100.             //We need to delete the original file (the one with the missing script asset)
    101.             //Rename the clone to the original file and finally
    102.             //Delete the meta file from the clone since it no longer exists
    103.  
    104.             System.IO.File.Delete(globalToDeletePath);
    105.             System.IO.File.Delete(globalClonePath + ".meta");
    106.             System.IO.File.Move(globalClonePath, globalToDeletePath);
    107.  
    108.             AssetDatabase.Refresh();
    109.         }
     
  3. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    For now I cannot find a way to do it except manually delete missing components in prefab yaml after using missing script cleaner.
     
    phobos2077 likes this.
  4. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    I think it may be a bug so I changed title to it.
     
    phobos2077 likes this.
  5. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    Poping up a question
     
  6. SteenLund

    SteenLund

    Unity Technologies

    Joined:
    Jan 20, 2011
    Posts:
    639
    Looking at the file you posted above, this is not about a GameObject component, this is a ScriptableObject embedded in the prefab asset.
    I am not sure how to get rid of those if the script is missing. I will have to do some investigation.
     
    phobos2077 likes this.
  7. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    Ok, but why it was there? I did not have any embedded ScriptableObject in this prefab. It appeared when I used this method.

    Few screenshots of diff after script usage:
    upload_2020-6-4_11-52-15.png
    upload_2020-6-4_11-51-57.png
     

    Attached Files:

  8. SteenLund

    SteenLund

    Unity Technologies

    Joined:
    Jan 20, 2011
    Posts:
    639
    Ohhh I see now, so you used RemoveMonoBehaviour..... which did remove the MB from the GameObject but it didn't actually destroy the object and it became detached.

    I will have a look at this.
     
    phobos2077 and Camarent like this.
  9. Camarent

    Camarent

    Joined:
    Feb 19, 2014
    Posts:
    168
    Yes, thank you!
     
  10. huang9012

    huang9012

    Joined:
    Sep 24, 2015
    Posts:
    1
    @SteenLund Is this bug fixed in the latest version?
     
  11. SteenLund

    SteenLund

    Unity Technologies

    Joined:
    Jan 20, 2011
    Posts:
    639
    phobos2077, codestage and Camarent like this.
  12. loganstalwart

    loganstalwart

    Joined:
    Feb 28, 2018
    Posts:
    3
    I hit this too. As a work around until RemoveMonoBehavioursWithMissingScript gets fixed, if you just instantiate a prefab in the scene (PrefabUtility.InstantiatePrefab) then remove the missing scripts from the instance, then save over the previous prefab (PrefabUtility.SaveAsPrefabAsset) it seems to remove the missing components from the prefab file without the extra null sub-assets that appear when calling RemoveMonoBehavioursWithMissingScript directly on the prefab.
     
  13. amynox

    amynox

    Joined:
    Oct 23, 2016
    Posts:
    178
    Any news on this ?
     
  14. SteenLund

    SteenLund

    Unity Technologies

    Joined:
    Jan 20, 2011
    Posts:
    639
    @amynox

    The fix landed in the beginning of August and thus is included in 2020.2 beta
     
    amynox likes this.
  15. Brightori

    Brightori

    Joined:
    Sep 15, 2017
    Posts:
    64
    what kind of fix it was? I still cant delete null sub assets from scriptable object
     
    phobos2077 likes this.
  16. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    My guess is they fixed the fact that RemoveMonoBehavioursWithMissingScript produces "dangling" objects inside prefabs that can't be deleted with code. But if you already have them, there's no way to do it. Here's the best way I found:
    - Manually identify missing script guid by opening one of the .prefab files
    - Create a new empty ScriptableObject and modify it's .meta file to have the same guid
    - Write a script that searches all assets and removes sub-objects of this new type
     
    BigRookGames likes this.
  17. slippyfrog

    slippyfrog

    Joined:
    Jan 18, 2016
    Posts:
    42
    I'm encountering the same issue in unity 2021.2 , sans prefabs, with assets derived from scriptable objects containing subassets that are instances derived from ScriptableObjects.

    Steps:
    1. From C# , Declare a new class called TestScriptableObject that extends from ScriptableObject
    2. From Unity or via c#, Create a new ScriptableObject instance (A) and save it to the AssetDatabase (it doesnt matter the Type of A is)
    3. From Unity, Create a new ScriptabObject (B) of instance TestScriptabeObject and set it as a subasset of (A).
    4. In the C# environment, Rename the class TestScriptableObject or delete the definition of the class.
    5. From Unity, select the asset (A) and view its subassets.

    Observed Notice (A) now contains null subassets (Script Is Missing).

    These null subassets cannot be removed via API calls to the AssetDatabase via DestroyImmediatey, or RemoveObjectFromAsset. Furthermore, GameObjectUil::RemoveMonoBehavioursWithMissingScript cannot be used as the asset (A) is not a prefab in this case.


    The AssetDatabase does seem like it is missing proper functionality to handle this. The use case does comes up quite frequently. Consider refactoring/deleting code that could result in missing scripts.

    @SteenLund Do you know if the fix accounted for this use case (and not just the prefab asset use case mentioned initially)?


    Please pardon the message if it is not relevant -- I do feel like this is the relevant place to to post this as the issue presented is a generalization of the prefab asset issue mentioned.

    Thanks in advance and have a nice day:)
     
    Last edited: Feb 18, 2022
  18. SteenLund

    SteenLund

    Unity Technologies

    Joined:
    Jan 20, 2011
    Posts:
    639
    @slippyfrog I am fairly certain that RemoveMonoBehaviourWithMissingScript does not work for this case. I'll check with the Asset Database team what the proper way is to handle this.
     
  19. PassivePicasso

    PassivePicasso

    Joined:
    Sep 17, 2012
    Posts:
    100
    I run into this same issue refrequently, I've build a few system which have ScriptableObjects with component based behavior, setup as an array pointing to sub-assets which the ScriptableObject host adds to itself.
    This situation naturally needs a custom inspector, and in these cases I've had to add code to handle when some Sub-Asset has become "corrupted" and render a "Missing Component" Editor in those cases.

    Unfortunately I see no way to fix these Assets nor remove them using built in APIs, the only idea I've had is to build code to directly modify the asset file text. Obviously this wouldn't be an idea solution and so its one I've not pursued.

    The ability to repair broken assets by trying to re-assign scripts to them would be a great feature to have along side the ability to remove broken sub-assets.
     
  20. snoche

    snoche

    Joined:
    Feb 22, 2010
    Posts:
    82
    I had a tool that use to work using RemoveMonoBehaviourWithMissingScript but now in 2021.2 doesn't work.
    I have a scene with some gameObjects that has missing scripts, RemoveMonoBehaviourWithMissingScript return always 0.
    Is this a known bug?
     
  21. Molder

    Molder

    Joined:
    Aug 28, 2013
    Posts:
    5
    ok, i found the solution, basically the secret is to never get to the situation where the subPrefab/subScriptableObject has lost the reference to its script.
    if this happens you have to open the .asset file and clean it by hand.
    but it will never be necessary it can be prevented.

    in the script that I attach as an example,
    OnWillDeleteAsset is called by the unity editor before deleting a MonoScript type asset, and at this point we can check if this MonoScript asset is used by other assets like Node (my class derived from ScriptableObject), if yes.
    I take the assets that can contain a subAsset derived from the Node class, I check its presence, and if one exists I eliminate it from the subAssets.
    then finally, when all the subAssets are deleted, I save the container asset and let the .cs script get deleted by unity editor


    // returns the container asset (you can search for it in the AssetDatabase)
    var assetGraph = AssetDatabaseExtension.GetAsset(typeof(GraphProcess)) as GraphProcess;



    // it's a list of Nodes, I like to have a copy of it, for convenience, (not necessary)
    assetGraph.nodes



    // remove Node from the list (not needed)
    assetGraph.DeleteNode(node);




    Code (CSharp):
    1.  
    2. using System;
    3. using InLogic.Core;
    4. using InLogic.Modules.Process;
    5. using UnityEditor;
    6.  
    7. public class UnityEditorWillDeleteAsset : AssetModificationProcessor
    8. {
    9.     /// <summary>
    10.     /// This is called by Unity Editor when it is about to delete an asset from disk.
    11.     /// </summary>
    12.     /// <param name="assetPath"></param>
    13.     /// <param name="options"></param>
    14.     /// <returns></returns>
    15.     private static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions options)
    16.     {
    17.         var assetScript = AssetDatabase.LoadAssetAtPath<MonoScript>(assetPath);
    18.        
    19.         if (assetScript == null || assetScript.GetClass().IsAbstract) return AssetDeleteResult.DidNotDelete;
    20.         if (!TypeCompare(assetScript.GetClass(), typeof(Node))) return AssetDeleteResult.DidNotDelete;
    21.  
    22.         var assetGraph = AssetDatabaseExtension.GetAsset(typeof(GraphProcess)) as GraphProcess;
    23.         if (assetGraph == null) return AssetDeleteResult.DidNotDelete;
    24.        
    25.         var toDelete = assetGraph.nodes.FindAll(node => node.GetType() == assetScript.GetClass());
    26.         toDelete.ForEach(node =>
    27.         {
    28.             assetGraph.DeleteNode(node);
    29.             AssetDatabase.RemoveObjectFromAsset(node);
    30.                    
    31.         });
    32.                    
    33.         assetGraph.SaveToDisk();
    34.  
    35.         return AssetDeleteResult.DidNotDelete;
    36.     }
    37.    
    38.     /// <summary>
    39.     /// Determines whether the a Type derives from the b Type, or vice versa.
    40.     /// </summary>
    41.     /// <param name="a">element to compare</param>
    42.     /// <param name="b">element to compare</param>
    43.     /// <returns>returns true when the competition is positive, otherwise false</returns>
    44.     private static bool TypeCompare(Type a, Type b)
    45.     {
    46.         if(a == b) return true; // Both are null or they are the same type
    47.         if(a == null || b == null) return false;
    48.         if(a.IsSubclassOf(b) || b.IsSubclassOf(a)) return true; // One inherits from the other
    49.  
    50.         return a.BaseType == b.BaseType; // They have the same immediate parent
    51.     }
    52. }
    53.