Search Unity

Feedback Assetdatabase: Missing Functionality To Remove "null" Sub-objects

Discussion in 'Asset Database' started by Peter77, Apr 13, 2019.

  1. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,610
    TL;DR: Please add functionality to remove "null" sub-objects from a ScriptableObject.

    The UnityEditor.AssetDatabase provides functionality to add an object to an existing asset via its AddObjectToAsset API. It also provides the counter-part, RemoveObjectFromAsset, to remove an earlier added object from an existing asset.

    However, it's currently missing functionality to remove an object from the asset, if that sub-object turned null. In this case RemoveObjectFromAsset does not work anymore.

    Having "null" objects in ScriptableObject assets occurs quite often here, due to copying ScriptableObject assets between projects.

    If one project does not contain a certain script, the sub-object in that ScriptableObject container appears as "null". This is fine though, because the missing script isn't required in the new project, therefore I just want to get rid of all those "null" objects in the ScriptableObject container now.

    Unity does not seem to provide functionality to remove those "null" sub-objects from a ScriptableObject, thus I use a text editor to remove it by hand. Depending on the complexity of the ScriptableObject, it can be quite a tedious task.

    Please add functionality to remove "null" sub-objects from a ScriptableObject.


    Reproduce
    1. Open attached project (2018.3)
    2. Right-click in Project window, select Create > Basket
    3. Select created Basket asset in Project window
    4. Click "Test > Add Banana to basket" from main menu
    5. Click "Test > Print basket objects" from main menu
    6. Observe it outputs "Banana"
    7. Rename Scripts/Banana to Scripts/_Banana
    8. Select created Basket asset in Project window
    9. Click "Test > Print basket objects" from main menu
    Observe it outputs "sub object at index 0 is null". There is no API to remove this null object now.
     

    Attached Files:

  2. mochu

    mochu

    Joined:
    Jan 30, 2020
    Posts:
    1
    Older question, I know, but for those (like me) who come over here: 3

    Code (CSharp):
    1. // Delete any old sub objects inside a main asset.
    2. UnityEngine.Object[] assets = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(TheMainAsset);
    3. foreach(UnityEngine.Object obj in assets)
    4. {
    5.   if ( obj == null ||
    6.      ( ! UnityEditor.AssetDatabase.IsMainAsset(obj) &&
    7.        ! (obj is GameObject) &&
    8.        ! (obj is Component) )
    9.      )
    10.     UnityEngine.Object.DestroyImmediate(obj, true);
    11. }
     
  3. alexanderlarsen

    alexanderlarsen

    Joined:
    Feb 26, 2015
    Posts:
    13
    Same problem here.

    @Peter77 Did you ever find a fix?

    @mochu Your method doesn't seem to work, for me at least. The null sub-object remains intact and undeleted.
     
    TobyWu likes this.
  4. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,610
    Only a workaround. I edited the file with a text editor and threw out the non-existing objects from there. But it's complicated :(
     
  5. AndyKorth

    AndyKorth

    Joined:
    Oct 19, 2009
    Posts:
    41
    I've definitely got this issue too. My current inclination is to re-create this object from scratch, and re-add all the non-null to a new replacement sub-asset. I have confirmed that DestroyImmediate doesn't help. (as in, following mochu's post up with a AssetDatabase.SaveAssets won't result in anything removed from the asset at that path.

    I'm in Unity 2019.4.28f1.
     
  6. AndyKorth

    AndyKorth

    Joined:
    Oct 19, 2009
    Posts:
    41
    Okay, an update. Rebuilding the asset from scratch is terrible. Don't take that route. It will get a new guid and disconnect it from everything in the project. Calling `AssetDatabase.DeleteAsset' also blasts away the current object (presumably with something like DestroyImmediately, so you can't create a new asset using that object.
     
  7. slippyfrog

    slippyfrog

    Joined:
    Jan 18, 2016
    Posts:
    42
    I'm having this issue as well. It seems like a pretty common use case that could result in some challenges on some bigger projects. Anytime we refactor code, or delete no-longer-used classes we run the risk of this issue presenting. We store identifiers and data-tuples as subasset. These are contained in many main-assets of our project. At the game logic layer of or game, many of these are sub-types of classes and they are quite volatile as especially as we iterate and prototype.

    Workaround:
    During production, these invalid subassets could exist in our project without much harm (other than increased load time, bloat and application/project size). We can work around by guarding against null when we iterate through the results of LoadAllAssetsAtPath. But before shipping we do not want to include this in our release binary so we need a step to clean them.

    I don' think we can fully automate a process that cleans these invalid subassets via parsing the .asset files. Lookin gonly at the asset file we cannot deduce if a script is a missing reference. We need to look in Unity first and then manaully edit the .asset files.

    Part A) From Unity, we would have to manually grep each subasset by name that is invalid. We cannot automate this as we are only returned 'null' from LoadAllAssetsAtPath. It is impossible to corollate a null value that is returned by LoadAllAssetsAtPath to the name of its associated sub asset. As such, we would need to click through each asset and see if it has an missing script.

    Part B) Next, we would have to edit each .asset file that contained these invalid sub assets, and manually remove entries with the corresponding names.

    In a big project, this is a bit of an issue as Part A would need to be done by manually by a human. Part B could be fed a list of .asset files names and sub-asset names.

    Also, muchu's solution mentioned above does not work.

    AssetDatabase API Improvement:
    Verified that calling RemoveObjectFromAsset and DestroyImmedately on the sub asset do not work -- I do not see these working as there would be no way to index the subasset to remove using 'null'.

    Similar we cannot call TryGetGUIDAndLocalFileIdentifier as they require the instance of the sub asset and not null.

    It really feels like the AssetDatabase API is missing functionality to handle this and also flawed in how subassets are accessed. The API assumes subassets are unique and not null. This allows subassets to be uniquely referenced within their main-asset. This breaks when we have null-subassets as null cannot uniquely reference the subassets they map to.

    Regarding the accessing of Subassets, Could there not be an int based indexing into AssetDatabase?
    AssetDatabase.GetSubAssetCount()
    AssetDatabase.GetSubAssetAtIndex(i)
    AssetDatabase.RemoveSubAssetAtIndex(i)


    If you Unity guys are not too busy, could some one from Unity advise?

    Thanks for any assistance in advance!! :)
     
  8. Mako-Infused

    Mako-Infused

    Joined:
    Nov 20, 2014
    Posts:
    2
    I see that no one has answered this question and this is still a problem as of Unity Version 2020.3.28f1. So I have created a solution, free for use, for whoever needs it:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class UnknownObject : ScriptableObject
    4. {
    5.     [System.NonSerialized]
    6.     private Object _Reference;
    7.     public Object Reference
    8.     {
    9.         get => _Reference;
    10.         protected set => _Reference = value;
    11.     }
    12.  
    13.     [System.NonSerialized]
    14.     private long _LocalIdentifier;
    15.     public long LocalIdentifier
    16.     {
    17.         get => _LocalIdentifier;
    18.         protected set => _LocalIdentifier = value;
    19.     }
    20.  
    21.     [System.NonSerialized]
    22.     private string _Data;
    23.     public string Data
    24.     {
    25.         get => _Data;
    26.         protected set => _Data = value;
    27.     }
    28.  
    29.     [System.NonSerialized]
    30.     private int _SubIndex;
    31.     public int SubIndex
    32.     {
    33.         get => _SubIndex;
    34.         protected set => _SubIndex = value;
    35.     }
    36.  
    37.     [System.NonSerialized]
    38.     private System.Action<int> _OnDelete;
    39.     public System.Action<int> OnDelete
    40.     {
    41.         get => _OnDelete;
    42.         protected set => _OnDelete = value;
    43.     }
    44.  
    45.  
    46.     private void OnDestroy()
    47.     {
    48.         OnDelete(SubIndex);
    49.     }
    50.  
    51.     public static UnknownObject CreateInstance(long localIdentifier, string data, Object reference, int subIndex, System.Action<int> onDelete)
    52.     {
    53.         var instance = CreateInstance<UnknownObject>();
    54.         instance.LocalIdentifier = localIdentifier;
    55.         instance.Data = data;
    56.         instance.Reference = reference;
    57.         instance.SubIndex = subIndex;
    58.         instance.OnDelete = onDelete;
    59.         return instance;
    60.     }
    61. }
    62.  
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Linq;
    4. using System.Text;
    5. using System.IO;
    6. using System.Collections.Generic;
    7. using System.Text.RegularExpressions;
    8.  
    9. public class AssetDatabaseHelper
    10. {
    11.     public readonly static Regex LocalIdPattern = new Regex(@"---\s!.!\d+\s&(.*)");
    12.  
    13.     private static (long, string)[] ParseSubAssetsAtIndex(Object mainAsset, int[] indices, bool keepMain = false)
    14.     {
    15.         var ownerPath = AssetDatabase.GetAssetPath(mainAsset);
    16.         _ = AssetDatabase.TryGetGUIDAndLocalFileIdentifier(mainAsset, out _, out long mainAssetLocalId);
    17.  
    18.         var yaml = new List<(long, StringBuilder)>();
    19.         var index = indices?[0];
    20.  
    21.         using (var sr = new StreamReader(ownerPath))
    22.         {
    23.             var line = string.Empty;
    24.             var delete = false;
    25.             var subAssetIndex = 0;
    26.             var localId = 0L;
    27.  
    28.             while ((line = sr.ReadLine()) != null)
    29.             {
    30.                 var match = LocalIdPattern.Match(line);
    31.                 if (match.Success)
    32.                 {
    33.                     localId = long.Parse(match.Groups[1].Value);
    34.                     var isSubAsset = localId != mainAssetLocalId;
    35.                     delete = (isSubAsset && (index < 0 || indices.Contains(subAssetIndex)) == keepMain) || (!isSubAsset && !keepMain);
    36.                     if (isSubAsset) subAssetIndex++;
    37.                 }
    38.                 if (!delete)
    39.                 {
    40.                     if (subAssetIndex >= yaml.Count) yaml.Add((localId, new StringBuilder()));
    41.                     yaml[subAssetIndex].Item2.Append(line + System.Environment.NewLine);
    42.                 }
    43.             }
    44.         }
    45.  
    46.         return yaml.Select(x => (x.Item1, x.Item2.ToString())).ToArray();
    47.     }
    48.  
    49.     public static Object GetSubAsset(Object subAsset, Object mainAsset)
    50.     {
    51.         try
    52.         {
    53.             return AssetDatabase.IsSubAsset(subAsset) ? subAsset : null;
    54.         }
    55.         catch (System.Exception)
    56.         {
    57.             var allSubAssets = GetNonSceneSubAssets(mainAsset);
    58.             var subIndex = allSubAssets
    59.                 .Select((subAsset, index) => (subAsset, index))
    60.                 .First(x => x.subAsset == subAsset)
    61.                 .index;
    62.  
    63.             return GetSubAssetsAtIndicesInternal(mainAsset, allSubAssets, subIndex).FirstOrDefault();
    64.         }
    65.     }
    66.  
    67.     public static Object[] GetNonSceneSubAssets(Object mainAsset)
    68.     {
    69.         var assetPath = AssetDatabase.GetAssetPath(mainAsset);
    70.         return assetPath.Contains(".unity")
    71.             ? new Object[] { }
    72.             : AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(x => x != mainAsset).ToArray();
    73.     }
    74.  
    75.     public static int GetSubAssetCount(string path)
    76.     {
    77.         return GetSubAssetCount(AssetDatabase.LoadMainAssetAtPath(path));
    78.     }
    79.  
    80.     public static int GetSubAssetCount(Object mainAsset)
    81.     {
    82.         return GetNonSceneSubAssets(mainAsset).Length;
    83.     }
    84.  
    85.     public static Object[] GetAllSubAssets(Object mainAsset)
    86.     {
    87.         return GetSubAssetsAtIndices(mainAsset, new int[] { -1 });
    88.     }
    89.  
    90.     public static Object GetSubAssetAtIndex(Object mainAsset, int index)
    91.     {
    92.         return GetSubAssetsAtIndices(mainAsset, new int[] { index }).FirstOrDefault();
    93.     }
    94.  
    95.     public static Object[] GetSubAssetsAtIndices(Object mainAsset, params int[] indices)
    96.     {
    97.         return GetSubAssetsAtIndicesInternal(mainAsset, null, indices);
    98.     }
    99.  
    100.     private static Object[] GetSubAssetsAtIndicesInternal(Object mainAsset, Object[] allSubAssets = null, params int[] indices)
    101.     {
    102.         var yamlIdentifiers = ParseSubAssetsAtIndex(mainAsset, indices);
    103.         var yamlAssets = yamlIdentifiers.Skip(1).ToArray();
    104.  
    105.         if (allSubAssets == null) allSubAssets = GetNonSceneSubAssets(mainAsset);
    106.         var subAssets = new List<Object>();
    107.  
    108.         var subIndex = 0;
    109.         foreach (var yamlAsset in yamlAssets)
    110.         {
    111.             try
    112.             {
    113.                 var subAsset = allSubAssets[subIndex];
    114.                 if (AssetDatabase.IsSubAsset(subAsset)) subAssets.Add(subAsset);
    115.             }
    116.             catch (System.Exception)
    117.             {
    118.                 var subAsset = UnknownObject.CreateInstance(yamlAsset.Item1, yamlAsset.Item2, allSubAssets[subIndex], subIndex, (subIndex) =>
    119.                 {
    120.                     RemoveSubAssetAtIndex(mainAsset, subIndex);
    121.                 });
    122.                 subAssets.Add(subAsset);
    123.             }
    124.             subIndex++;
    125.         }
    126.  
    127.         return subAssets.ToArray();
    128.     }
    129.  
    130.     public static void RemoveAllSubAssets(Object mainAsset)
    131.     {
    132.         RemoveSubAssetAtIndex(mainAsset, -1);
    133.     }
    134.  
    135.     public static void RemoveSubAssetAtIndex(Object mainAsset, int index)
    136.     {
    137.         RemoveSubAssetsAtIndices(mainAsset, new int[] { index });
    138.     }
    139.  
    140.     public static void RemoveSubAssetsAtIndices(Object mainAsset, int[] indices)
    141.     {
    142.         EditorApplication.delayCall += () =>
    143.         {
    144.             var ownerPath = AssetDatabase.GetAssetPath(mainAsset);
    145.  
    146.             string tempFile = Path.GetTempFileName();
    147.  
    148.             using (var sw = new StreamWriter(tempFile))
    149.             {
    150.                 var yaml = string.Join("", ParseSubAssetsAtIndex(mainAsset, indices, true).Select(x => x.Item2).ToArray());
    151.                 sw.Write(yaml);
    152.             }
    153.  
    154.             File.Delete(ownerPath);
    155.             File.Move(tempFile, ownerPath);
    156.  
    157.             AssetDatabase.ImportAsset(ownerPath);
    158.         };
    159.     }
    160. }
    161.  
    It provides some helpful API, such as suggested by @slippyfrog (above):
    AssetDatabaseHelper.GetSubAssetCount()
    AssetDatabaseHelper.GetSubAssetAtIndex(o, i)
    AssetDatabaseHelper.RemoveSubAssetAtIndex(o, i)

    Additionally it has a few more features for working with what I call "UnknownObjects".

    I hope this helps whoever is still searching the internet for this answer! :)
     
    Last edited: Mar 9, 2022
    Tyrant117, slippyfrog and SSSekhon like this.