Search Unity

Saving a list of scriptable objects to JSON

Discussion in 'Scripting' started by Dextozz, Mar 30, 2020.

  1. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    I'm working on a quest system of sorts. I have a base class QuestAction which inherits from ScriptableObject, like this:
    Code (CSharp):
    1. public abstract class QuestAction: ScriptableObject
    From there I have many classes that derive from QuestAction class. For example, DialogueAction, TravelAction etc.They all implement their own specific behaviors.

    Now, the idea is to have one list of QuestActions that will allow me to add new actions sequentially, like this:
    Code (CSharp):
    1. class Quest
    2. {
    3.    public List<QuestAction> questActions;
    4.  
    5.    public void Start()
    6.    {
    7.       questActions.Add(new TravelAction(_destination));
    8.       questActions.Add(new BeginDialogueAction(_speakers, _listeners);
    9.       questActions.Add(new DialogueAction(_character, _text);
    10.       questActions.Add(new DialogueAction(_character, _text);
    11.       questActions.Add(new DialogueAction(_character, _text);
    12.       questActions.Add(new EndDialogueAction();
    13.       questActions.Add(new PopupAction(_popupType));
    14.    }
    15. }
    As you can see, this is hardcoded and I plan to have hundreds of quests in this system in the future. I tried creating a custom inspector for this in order to serialize these actions in a JSON file. The deal is if I serialize just one QuestAction, everything works, JSON prints out really well. However, since JsonUtility doesn't support array serialization (as far as I'm aware), I have to create a wrapper class if I want to serialize an entire array, like this:
    Code (CSharp):
    1. class Quest
    2. {
    3.    public QuestActionCollection actionCollection;
    4.  
    5. }
    6.  
    7. [System.Serializable]
    8. public class QuestActionCollection
    9. {
    10.    public List<QuestAction> questActions;
    11. }
    Then I can serialize the actionCollection. However, JSON files don't contain any information anymore. This is all the file contains:
    Code (CSharp):
    1. {
    2.     "QuestActions": [
    3.         {
    4.             "instanceID": -4980
    5.         },
    6.         {
    7.             "instanceID": -5022
    8.         },
    9.         {
    10.             "instanceID": -5030
    11.         }
    12.     ]
    13. }
    Any idea how to fix this? I've done a fair bit of research and the most useful thing I came across is this:
    https://forum.unity.com/threads/solved-saving-scriptableobject-into-json.521193/
    The guy essentially says it's near impossible without using Odin. There's no chance I'll be getting any plugins to solve this issue.
     
  2. _met44

    _met44

    Joined:
    Jun 1, 2013
    Posts:
    633
    I felt I was wasting too much time trying to do it this way, ended giving up and wrote a little ID binder system.

    Each asset gets a unique uint ID and I bind it with a dictionary when the asset is enabled, and I serialize only the ID in the json file.

    I also have a proxy that serializes the ID but can return the instance and under the hood returns either a in cache reference if available or polls the binder.
     
  3. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @Dextozz

    "Any idea how to fix this?"

    There is nothing to fix unfortunately; this is how Unity built-in serialization works, it doesn't serialize ScriptableObjects or other asset references as data, but with id. I personally have tried to avoid using ScriptableObjects "as is" for anything that needs to be saved.

    Then again, if you use typical serializable C# classes for your data, you are not going to have your sub class data serialized, i.e. a List of QuestAction will be serialized as QuestAction, it won't have fields for sub class items of type TravelAction etc. despite of actual type of the item you have stored in your List.
     
  4. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    Thanks for the replies. I managed to figure it out after a ton of research. I've switched to Newtsonsoft.json. It allows for a great serialization of arrays.

    I simply replaced
    Code (CSharp):
    1. string dataAsJson = JsonUtility.ToJson(actionCollection, true);
    with
    Code (CSharp):
    1. string dataAsJson = JsonConvert.SerializeObject(actionCollection, Formatting.Indented);
    After that, I ran into an obvious issue. I was not able to deserialize the JSON file as expected. Why? I was serializing child classes, which worked fine, but when I deserialized them with
    Code (CSharp):
    1. actionCollection = JsonConvert.DeserializeObject<List<QuestAction>>(dataAsJson);
    I was actually casting child classes into their parent class, thus losing all the data that I was saving. This is where stuff got difficult since I needed a way to deserialize multiple classes from JSON. This article saved my life:
    https://blog.mbwarez.dk/deserializing-different-types-based-on-properties-with-newtonsoft-json/
    All it was missing was
    Code (CSharp):
    1. public override bool CanWrite
    2. {
    3.     get
    4.     {
    5.         return false;
    6.     }
    7. }
    Which enabled me to write to a JSON file without any issues and read multiple classes from it.
     
    SorraTheOrc likes this.
  5. arkilis

    arkilis

    Joined:
    Feb 22, 2018
    Posts:
    2

    Hey Dextozz,

    thanks for your post, what is the CanWrite? run into this same issue....
     
  6. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    If you open the last link I placed (the one leading to a blog) and scroll down to Vehicle Converter class, you'll see a similarly named variable - "CanConvert" (// Skipped WriteJson and CanConvert). When you created your class that derives from JsonConverter, you should have added the override for CanWrite as I mentioned above.
     
  7. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    64
    Hello @Dextozz I tried your code, and although now I get a correct JSON with all the data instead of the instanceID, but I couldn't deserialize the Json data to their scriptableobjects. I get this error: DataClass must be instantiated using the ScriptableObject.CreateInstance method instead of new DataClass.

    Could you post an example project using your code to understand it fully? Thanks
     
  8. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    Don't say DataClass data = new DataClass(), instead, use DataClass data = ScriptableObject.CreateInstance<DataClass>();

    I can't paste any examples since I'm not really allowed to. Sorry. If that didn't answer your question, please ask again, we'll figure it out.
     
  9. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    64
    I get this error from JsonConvert.DeserializeObject<DataClass>(json) being DataClass a ScriptableObject with a List of DataElement ScriptableObjects

    Newtsonsoft.json is asking to create a new DataClass and as much DataElements are included in it, but it's doing it the worng way. The possible solution could be to change the way Newtsonsoft.json deserialize the DataClass, but I'm not sure how to do it easily.
     
  10. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    Code (CSharp):
    1. // in your custom Converted class
    2. public class MyConverter : JsonConverter
    3. {
    4.     public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    5.     {
    6.         DataClass result = ScriptableObject.CreateInstance<DataClass>();
    7.  
    8.         if(reader.TokenType != JsonToken.Null)
    9.         {
    10.             JToken jObject = JToken.ReadFrom(reader);
    11.            
    12.             // Here's the part where your logic comes.
    13.             // Since you have a DataClass, my guess is that you have other classes deriving from it
    14.             // For example, PlayerData : DataClass, EnemyData : DataClass, whatever
    15.             //
    16.             // Somewhere, you have implemented a way to distinguish between them, perhaps an enum or a string
    17.             // Basically, your derived classes surely have some way of being distinguished from one another
    18.             // Maybe you have an enum DataType { Player, Enemy } and use it in your DataClass { protected virtual DataType thisClassType; }
    19.             //
    20.             // Once you figured it out, you want to handle the return of a value based on that distinguishable key
    21.             // Something like this:
    22.             //
    23.             // DataType dataType = jObject["dataType"].ToObject<DataType>();
    24.             // switch(dataType)
    25.             // {
    26.             //       case Player:
    27.             //          result = ScriptableObject.CreateInstance<PlayerData>();
    28.             //      case Enemy:
    29.             //          result = ScriptableObject.CreateInstance<EnemyData>();
    30.             // }
    31.            
    32.             serializer.Populate(jObject.CreateReader(), result);
    33.         }
    34.  
    35.         return result;
    36.     }
    37. }
    Now, once you have that (you already should have this if you followed the forum link I posted above), you can easily call DataClass myInstance = JsonConvert.DeserializeObject<DataClass>(json)
     
  11. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    64
    Hi @Dextozz, I manage to make it working, but I think I'm a bit confused about how scriptableobjects work. When I used JsonUtilitiy methods all the references were mantained whenever I loaded the data, but now using CreateInstance the data is recovered, but the references are not. Should I include the instanceIDs too? But in any case, that instanceID is not persistant, so I'm not sure if using ScriptableObjects should be the way to go...

    What I'm trying to do is having the whole configuration of the game in a file, so I can do minor changes or add more stuff just changing the file without rebuiling the whole game. Maybe Addressables is a better way to do all this, but I want to keep the benefits of ScriptableObjects... Addressable ScriptableObjects maybe?
     
  12. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    Hmmm, aren't game configs done through a big text file usually?
     
  13. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    64
    Well, I have events, images, data, all sort of things... Just a big text file could work for things like localization, but for all this information I'm using jsons (I can obfuscate them later and I don't need a database) Json is great for persistent data and for easy modification. I could even have an editor so anyone can mod things, create levels, etc... but I'm trying to escape from singletons and ScriptableObjects seem the way to go.

    Anyway, did you find a way to keep references? Saving InstanceIDs and resetting them in the new ScriptableObjects instances doesn't seem a good idea because they are not persistant, and they probably won't work in Build. Thanks for the support.
     
  14. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    From what I know, there's no easy way to do this. You can't simply store a reference to a file in your project in your JSON file and then have it back when you deserialize it. What you can do, is keep those images in the Resources folder (or wherever you keep them if you take the path of Addressables) and simply reference the path to them.

    So, instead of dragging and dropping your image in a field, you would have a string field that says "Images/MyImage" and then load it from resources using that location.
    Now, as you might imagine, that's quite dirty as you are not allowed to change the storage location of any file that is referenced in this way + it adds additional work making you type in its path every time you want to drag and drop a simple image.

    You can avoid the second part by using: https://docs.unity3d.com/ScriptReference/AssetDatabase.FindAssets.html where you would drag and drop your image as usual, but in the background, you would store the path to that file and then, when you open up your editor, use the stored path to load that exact image. That's cool, now you don't have to worry about that extra work for the simples of tasks. You are still stuck with that asset being in the same location forever or until you manually change it and all references to it.
     
  15. danbg

    danbg

    Joined:
    May 1, 2017
    Posts:
    64
    Your solution could work with vars too in an automatic way. If I could change the instanceID at runtime, all would be really easy, I could just load the data, check the the runtime generated instanceID (it doesn't matter now that it's not persistent) of the referenced ScriptableObject using a customID, create a new one with AssetDatabase.CreateAsset with the same customID and swap the instaceID... Using ScriptableObjects without using referencing in the editor seems losing a lot of potential.

    The only caveats I see are having 2 id fields for each ScriptableObject, slowing the loading with more processing to find references, and Unity doesn't allow to change the instanceID of a new created asset at runtime just by script (or at least I couldn't find a way to do it)