Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Clean way to save/load static scriptableobjects?

Discussion in 'Scripting' started by munkbusiness, Jun 21, 2023.

  1. munkbusiness

    munkbusiness

    Joined:
    Aug 22, 2017
    Posts:
    55
    Hi, I am trying to integrate a save system into our card game prototype. And I ran into an issue of not being able to use BinarryFormatter for serializing lists og Scriptable object references. I get this error:

    Code (CSharp):
    1. SerializationException: Type 'Encounter_SA' in Assembly 'Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
    • I am using SO the "intended way", so only as data containers with actionable scripts.
    • I am using scriptable objects as comparable for a lot of logic, for example to test if you own a card already.
    • I save lists of Scriptables for reference of which cards, events, and enemies that each have fuirther references to more scriptables.
    I have google around, and most solution suggest making clones of scriptables as serializeable classes, I would really like to avoid doing this and it muddies up the solution and breaks all references in the editor. I have seen solutons suggest saving either the GUID or name of the scriptables, and I am leaning to this solution, but I have a hard time figuring out how to intercept the formatter with some type convertion script.

    I have a very simple Binnarryformatter that I have always used for save/loading solutions in the past.

    Code (CSharp):
    1. public static void SaveGameData(SaveData data) {
    2.  
    3.  
    4.         //Create directory if it doens't exist
    5.         if (!Directory.Exists(dataPath + path)) {
    6.             //if it doesn't, create it
    7.             Directory.CreateDirectory(dataPath + path);
    8.  
    9.         }
    10.  
    11.         BinaryFormatter bf = new BinaryFormatter();
    12.         FileStream file = File.Create(dataPath + path + filename + ext);
    13.         bf.Serialize(file, data);
    14.         file.Close();
    15.     }
    16.  
    17.     public static SaveData LoadGameData(out bool wasEmpty) {
    18.  
    19.  
    20.         if (File.Exists(dataPath + path + filename + ext)) {
    21.             BinaryFormatter bf = new BinaryFormatter();
    22.             FileStream file = File.Open(dataPath + path + filename + ext, FileMode.Open);
    23.             SaveData data = bf.Deserialize(file) as SaveData;
    24.             file.Close();
    25.  
    26.             wasEmpty = false;
    27.             return data;
    28.         }
    29.  
    30.         //Create the savefile with everything reset
    31.         else {
    32.            
    33.             wasEmpty = true;
    34.             return new SaveData();
    35.         }
    36.     }
    I am using this to save a file that has tons of references inside, some of which are lists of scriptables.

    Is there anyway to add some logic that could automatically detect scriptables, save their path, id, guid or whatever, and then when loaded would automatically know to get the real scriptable with some code? I really want to keep the structure of my scriptables intact as much as possible.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Load/Save steps:

    https://forum.unity.com/threads/save-system-questions.930366/#post-6087384

    An excellent discussion of loading/saving in Unity3D by Xarbrough:

    https://forum.unity.com/threads/save-system.1232301/#post-7872586

    Loading/Saving ScriptableObjects by a proxy identifier such as name: <--- this is likely to be useful in your case. NOTE: you don't save the contents of the SO, you save that "I am using this-and-such pre-made SO."

    https://forum.unity.com/threads/use...lds-in-editor-and-build.1327059/#post-8394573

    When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls
    new
    to make one, it cannot make the native engine portion of the object.

    Instead you must first create the MonoBehaviour using AddComponent<T>() on a GameObject instance, or use ScriptableObject.CreateInstance<T>() to make your SO, then use the appropriate JSON "populate object" call to fill in its public fields.

    If you want to use PlayerPrefs to save your game, it's always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

    https://gist.github.com/kurtdekker/7db0500da01c3eb2a7ac8040198ce7f6

    Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

    https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide
     
    munkbusiness and Unifikation like this.
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    You can, but not by using the ancient binary formatter that you should immediately stop using.

    The Odin Serialiser has something called an external reference resolver that can be used to swap out references to Unity object references with replacement data (such as a UUID), and be able to restore those references on de-serialisation.
     
    munkbusiness likes this.
  4. munkbusiness

    munkbusiness

    Joined:
    Aug 22, 2017
    Posts:
    55
    I am happy that specifically you two responded, as I know you guys know what you are talking about, thanks a lot for your time, and sorry my noob brain take some time to figure this out.

    I have seen other discuss the Binary formatter, but does it actually matter for a single player experience? No amount of injection or modification to my code has any influence on anyone else except the users own local version of the game. Either way I dont really have any requirements for my serializer, so if Json Serializer can do the job then I swap. I just want whatever works with the least upkeep.

    I have read all of the links you sent me @Kurt-Dekker , I just have a hard time wrapping my head around how exactly I can use them.

    Just so we are using the same language, my save file uses the format "Save Game As Single Source Of Truth" one file, that has subclasses with subclasses going deep.

    I know how to do the "swap format" it is rather simple to get name, and get asset from path, or get id, get asset from a list. What I dont know is how to give a RULE to whatever serializer I use (binary, json, xml, ect.), to use that logic for specific types, eg. scriptable objects.

    My favorite solution would be to add one rule to whatever serializer so it automatically converts formats on load and save, and I dont have to change the structure of the data file itself.

    From your links it seems like this is more of the solution suggested:

    Code (CSharp):
    1. public List<Card_SA> draftableCards;
    2.     public List<Card_SA> draftableCardsBackup;
    3.     public List<Card_SA> tutorial1DraftCards;
    4.     public List<Card_SA> tutorial2DraftCards;
    5.  
    6.     public List<Talent_SA> gainableItems;
    7.  
    as an example would then become

    Code (CSharp):
    1.  
    2.     [NonSerialize]
    3.     public List<Card_SA> draftableCards;
    4.  
    5.     public List<Card_SA>draftableCardsSerialize {
    6.             get {
    7.                 // return list<string / GUID / int>
    8.             }
    9.             set {
    10.                 // draftableCards = LoopToGetListOfCard_SA(string/GUID/int);
    11.             }
    12.     }
    13.  
    14.     [NonSerialize]
    15.     public List<Card_SA> draftableCardsBackup;
    16.  
    17.     public List<Card_SA>draftableCardsBackupSerialize {
    18.             get {
    19.                 // return list<string / GUID / int>
    20.             }
    21.             set {
    22.                 // draftableCardsBackup = LoopToGetListOfCard_SA(string/GUID/int);
    23.             }
    24.     }
    25.  
    26.     //Repeat for EVERY SINGLE SCRIPTABLE REFERENCE
    27.  
    But then I would have to do this 20 + places in my data file.

    @spiney199 They look great, looks kinda like how I would approach "changing format", what I am unsure about is where I put these? I have also looked at https://learn.microsoft.com/en-us/d...gateselector?redirectedfrom=MSDN&view=net-7.0 but still same issue, I don't really get where to insert it concreately.
     
    Kurt-Dekker likes this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    It matters because it's a huge security risk you're opening up yourself and your players to. Microsoft tells you not to use it any more, so do not use it.

    And the binary formatter will probably be the most upkeep because of how old, out of date, and non-debuggable it is.

    You use it as part of the overall Odin serialiser package. It's not made for anything else.

    I have an interface called
    IAssetGUID
    that I implement in scriptable object types that need to express a unique ID. Then I can make an external string reference resolver like so:
    Code (CSharp):
    1. public sealed class AssetGuidReferenceResolver : IExternalStringReferenceResolver
    2. {
    3.     public IExternalStringReferenceResolver NextResolver { get; set; } = new AssetNameReferenceResolver();
    4.  
    5.     public bool CanReference(object value, out string id)
    6.     {
    7.         if (value is IAssetGUID assetGUID)
    8.         {
    9.             id = $"G:{assetGUID.AssetGUID}";
    10.             return true;
    11.         }
    12.  
    13.         id = null;
    14.         return false;
    15.     }
    16.  
    17.     public bool TryResolveReference(string id, out object value)
    18.     {
    19.         value = default;
    20.  
    21.         if (id.StartsWith("G:"))
    22.         {
    23.             CatalogueQueryResult result = Catalogues.QueryCatalogues(id[2..]);
    24.             value = result.QueryAsset;
    25.             return result.QueryResult;
    26.         }
    27.  
    28.         return false;
    29.     }
    30. }
    Catalogues is my internal database system I can share between projects, which is what I use to look up and restore object references on de-serialisation.

    Then, you just use the reference resolver when serialising the values:
    Code (CSharp):
    1. public static void SerializeSaveData(SaveData data, string path)
    2. {
    3.     SerializationContext context = new SerializationContext()
    4.     {
    5.         StringReferenceResolver = new AssetGuidReferenceResolver()
    6.     };
    7.  
    8.     byte[] bytes = SerializationUtility.SerializeValue<SaveData>(data, DataFormat.Binary, context);
    9.     File.WriteAllBytes(path, bytes);
    10. }
    Obviously just a basic example.
     
    munkbusiness likes this.
  6. munkbusiness

    munkbusiness

    Joined:
    Aug 22, 2017
    Posts:
    55
    I started working on a solution, but then I decided to google some of the keywords from your post and I stumbled into this solution, using the Odin Serializer: https://odininspector.com/community...g-references-to-scriptable-objects-at-runtime

    Which is a concrete solution of what you talked about @spiney199

    And it works perfectly. Thanks a lot for helping me get to this point.

    EDIT: I ran into a problem where the ScriptableObjectReferenceCache randomly got corrupted after having closed unity and opening it again, to solve this I had to change it from a ScriptableObject to a SerializedScriptableObject. Why? IDK, but it worked.
     
    Last edited: Jun 27, 2023