Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Question Script generating ScriptableObjects not saving Editor data persistently through Unity restarts

Discussion in 'Asset Database' started by tsutter, Feb 8, 2024.

  1. tsutter

    tsutter

    Joined:
    Jun 6, 2018
    Posts:
    1
    Hi all -

    I currently have a script that reads the raw data from a CSV and parses through it in order to generate a big pile of ScriptableObjects. This all works great; however, some List<T> data that's attached to the SO is lost for various reasons beyond my comprehension, restarting the Unity editor being the main one.

    The entire workflow here would be: CSV is parsed & data is pulled from it, that data is supplied to a ScriptableObject, and that ScriptableObject is serialized with the below code. The SO has a List<T> class component that loses some (but not all) of it's data after a Unity restart. After the script is ran initially it all works fine. This also doesn't happen to *every* ScriptableObject that gets generated, either. The majority of the data persists, but not enough to where it wouldn't be a massive PITA to fix by hand afterwards.

    Here's the codeblock where assets are being generated, which I would assume is where the problem lies:

    Code (CSharp):
    1.         private static void LetThereBeAssets()
    2.         {
    3.             Debug.Log("creating & saving dialogue scriptable objects...");
    4.             var i = 0;
    5.             foreach (DialogueData dData in stagedDialogueData)
    6.             {
    7.                 EditorUtility.SetDirty(dData);
    8.                 string fileName = "/" + dData.speakerName + "_" + i + ".asset";
    9.                 var _pathStr = $"Assets/Disco/DialogueData/Dialogue Objects/{dData.folderPath}";
    10.                 _pathStr = Regex.Replace(_pathStr, @"\s", string.Empty);
    11.                 if (!System.IO.Directory.Exists(_pathStr))
    12.                 {
    13.                     System.IO.Directory.CreateDirectory(_pathStr);
    14.                     AssetDatabase.Refresh();
    15.                 }
    16.                 AssetDatabase.CreateAsset(dData, _pathStr + fileName);
    17.                 EditorUtility.SetDirty(dData);  // double tap
    18.                 AssetDatabase.SaveAssets();
    19.                 i++;
    20.             }
    21.         }
    I can supply more information if necessary. I feel like maybe there is some AssetDatabase call that isn't being made that is causing the serialization to get lost in the mix somewhere, but poking through the docs I couldn't find much. I've a feeling there's some weird Unity garbage collecting happening that is catching this stuff in the crossfire, but I'm not sure why it would be happening to some and not others.
     
    Last edited: Feb 8, 2024
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,161
    I am 100000% certain that you do NOT have an issue with the AssetDatabase. ;)

    The guy who redesigned the entire AssetDatabase can spot from a mile away that this script is inherently broken. That would be me btw. ;)

    I fixed the script and provide explanations because incorrect AssetDatabase usage and this perpetuating myth that the AssetDatabase is somehow very unreliable demands a thorough, rational analysis. You may skip this to the next dashed lines if you want to focus on analyzing the issue.

    -------------------------------------------------------
    First: It's not your fault because there are a TON of inherently broken AssetDatabase scripts and examples out there and whenever we need to do something out of the ordinary like working with the AssetDatabase then we just tend to copy that from other's code snippets. It's just crap that spread over the years and it is aggravating me every time. ;)

    There's a >50% chance when you google that you get to see broken or overkill AssetDatabase code snippets like these:


    Here's your script but corrected so it does EXACTLY what is required and no more:
    Code (CSharp):
    1. private static void LetThereBeAssets()
    2. {
    3.     Debug.Log("creating & saving dialogue scriptable objects...");
    4.     var i = 0;
    5.     foreach (DialogueData dData in stagedDialogueData)
    6.     {
    7.         string fileName = "/" + dData.speakerName + "_" + i + ".asset";
    8.         var _pathStr = $"Assets/Disco/DialogueData/Dialogue Objects/{dData.folderPath}";
    9.         _pathStr = Regex.Replace(_pathStr, @"\s", string.Empty);
    10.         if (!System.IO.Directory.Exists(_pathStr))
    11.         {
    12.             System.IO.Directory.CreateDirectory(_pathStr);
    13.         }
    14.         AssetDatabase.CreateAsset(dData, _pathStr + fileName);
    15.         i++;
    16.     }
    17. }
    18.  
    :eek::eek::eek::eek::eek::eek:

    This may seem eerily horrific to have merely a single AssetDatabase call there!
    There HAS TO BE a Refresh and a SaveAssets and SetDirty and ForceReserializeEverythingAndTheWholeUniverse otherwise it cannot possibly work, right? :cool:

    But no, I wrote a testcase to be certain that creating an asset in a newly created folder must, by necessity, also import the newly created folder (Refresh == ImportAllAssets). But yes, that's exactly what happens. You cannot possibly have a non-imported folder with imported assets in it.

    Refresh() is almost every time entirely unnecessary. Where you would need it after "externally" creating a file or dir, you can and should always use a targeted ImportAsset(path) instead. Yes, importing folders is a thing because folders are assets too.

    The whole "dirty" deeds are completely unnecessary when the asset doesn't exist yet. When you create an asset from an instance, the instance will be written to disk regardless. CreateAsset behaves equivalent to File.WriteAllText() in that regard. Imagine what would happen if CreateAsset only created "dirty" objects as assets but would not write the "clean" ones.

    Saving (all!) assets is not necessary when you create an asset. Create the asset, then save the asset. What is the state of the asset after creating and before saving? ;)
    A created asset by nature is already saved. Also notable: Sometimes I see devs create an asset, then modify, dirty and save the asset object and ... sure that works, but why wasn't the change made BEFORE creating the asset in the first place?

    Lastly: If you can have more than 10 scripts it would of course be helpful to wrap these in try {StartAssetEditing} finally{StopAssetEditing} to speed things up tremendously. And I mean it! In my CodeSmile AssetDatabase sample I duplicate 32 assets with and without batching. Without batching even on my high-end system I have to wait 3-5 seconds, while batched it is almost instant.

    ----------------------------------------------------------------------

    Here's what I would do to analyze the issue:
    • Confirm that there is no obvious data missing from the lists in the SO. It could be that the CSV is broken to begin with. Not unlikely because most CSV readers are broken - if yours is using Regex: kill it WITH FIRE! This will ALWAYS fail at the worst of times. You cannot rely on Regex reading CSV correctly. CSV needs to be parsed!
    • Run the above asset creation script. Close the editor. Reopen the project. Locate an SO that has data missing from the list. Open that SO's .asset file in a text editor to see whether the missing data is in the file or not. Depending on that you have one of these issues:
      • If the missing data is in the asset: It clearly got lost during or after deserialization. This could have a number of reasons, such as modifying the list contents after the SO was instantiated or a broken ISerializationCallbackReceiver implementation.
      • If the missing data is absent from the asset: the data was not serialized. This indicates that you have data in the list that isn't serializable (dictionary, ..) or not marked as serializable (custom class, non-public field, interface or base class reference without SerializeReference attribute, generics). Google will help you find solutions or workarounds.
    PS: I'd appreciate it if you could detail the result/fix because I'm curious. I place my bet on a non-serializable T or something within T that is not serializable.
     
    Last edited: Feb 26, 2024