Serialization of Scriptable Objects

Hi there guys,

I was wondering if you could point me in the right direction for the serialization of scriptable objects that hold references to other scriptable objects.

I have watched a range of tutorials and attempted to research the problem but after two days have yet to come up with a functional solution. Any help would be massively appreciated.

Here are some of the resources I have managed to learn from so far:

I have a player stats scriptable which holds references to other relevant player information. This is an exerpt of the example. PlayerAbilities, Class,Inventory and PlayerEquipment are all other scriptable objects.

public class PlayerStats : ScriptableObject
{
    public string playerName = "Joshua";
    public Sprite playerPortrait;

    public PlayerAbilities playerAbilities;
    public Class playerClass;
    public Inventory playerInventory;
    public PlayerEquipment playerEquipment;
...

This script below allows for the serialization of a list of scriptable objects, however upon loading the game, the references set within the scriptables are lost.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;


public class ScriptableObjectStateSave : MonoBehaviour
{

    [Header("Meta")]
    public string persisterName;

    [Header("Objects to Persist")]
    public List<ScriptableObject> objectsToPersist;

    //On Enable this loads the state of the scriptable object list.
    private void OnEnable()
    {
        for (int i = 0; i < objectsToPersist.Count; i++)
        {
            if (File.Exists(Application.persistentDataPath + string.Format("/{0}_{1}.save", persisterName, i))){

                BinaryFormatter binary = new BinaryFormatter();
                FileStream file = File.Open(Application.persistentDataPath + string.Format("/{0}_{1}.save", persisterName, i), FileMode.Open);
                JsonUtility.FromJsonOverwrite((string)binary.Deserialize(file), objectsToPersist[i]);
                file.Close();
            } else
            {

            }
        }
    }

    //On Disable this saves the state of the scriptable object list.
    private void OnDisable()
    {
        for (int i = 0; i < objectsToPersist.Count; i++)
        {
            BinaryFormatter binary = new BinaryFormatter();
            FileStream file = File.Create(Application.persistentDataPath + string.Format("/{0}_{1}.save", persisterName, i));
            var json = JsonUtility.ToJson(objectsToPersist[i]);
            binary.Serialize(file, json);
            file.Close();
        }
    }
}

Could anybody give me some advice on the best practices / setup of the serializer so that these references can be maintained?

Some of the Ideas I’ve had include combining those scriptable objects into one all encompasing player scriptable object.

Another being using a Player script to handle all interactions between the scriptable objects and not have scriptable objects contained inside each other.

What would be the best course of action for this?

All the best,
Josh

ScriptableObjects provide no benefit over plain old C# objects once you load them into the game and read them, since changes will not make it back to the Unity asset. I generally use ScriptableObjects only for static data. Like a player’s starting inventory. For the player’s actual inventory, I use a completely separate object that I have full control over the serialization properties of using a framework like Json.Net https://assetstore.unity.com/packages/tools/input-management/json-net-for-unity-11347. So the ScriptableObject is like a template whose data gets loaded into the real runtime type at the start of the game, and maybe is referenced when necessary. But at no point would I serialize ScriptableObject anywhere (although I sometimes serializes references to ScriptableObjects using Unity’s Addressable Assets system).

1 Like

Hi Praetor,

Thanks so much for advice on this, I really appreciate the feedback. I hope you don’t mind if I ask a few additional questions.

By C# Object are you just referring to a C# script / GameObject?

I’m not entirely sure if this is really bad practice but an example would be my players inventory and inventory items. The Inventory is a scriptable object which contains a list of inventory items which are also scriptable objects.

Would a better solution be to create an empty game object with a C# Inventory script, then referenced the Inventory Item scriptable objects, or would you advise to create both the items and inventory in C# rather than scriptables?

I suppose if it wasn’t referencing the List of scriptable objects within the inventory, I would have to create a database of gameobjects instead even if they wern’t instaniated in the players world?

I am definitly seeing the drawbacks of having so many references between them now it comes to the serialization at least and will look into Json.Net for Unity.

public class PlayerInventory : ScriptableObject
{
    public int keys; //Var holding key number
    public int gold; //Number of coins help by the player

    //Creates a list of the items held.
    public List<InventoryItem> myItems = new List<InventoryItem>();
    public void AddItem(InventoryItem newItem)
    {
        //increase number of keys
        if (newItem.itemType == ItemType.Key)
        {
            keys++;
        }

        //If the item isnt in the inventory then add a new item
        else
        {
            myItems.Add(newItem);
        }
    }

    public void RemoveItem(InventoryItem newItem)
    {
        if (myItems.Contains(newItem))
        {
            myItems.Remove(newItem);
        }
    }

    public void UseItem(InventoryItem newItem)
    {
        if (myItems.Contains(newItem))
        {
            //currentItem;
        }
    }

    public bool IsItemInInventory(InventoryItem newItem)
    {
        return myItems.Contains(newItem);
    }
}

This is the basis of the Inventory system, would it be better to have this inherit from MonoBehaviour and sit in an empty game object on the player / In a system prefab on each scene? If so should I replace the ‘InventoryItem’ scriptable reference with a list of GameObjects and rather than use scriptables, use gameobjects for the items within the game?

Either way i’m going to do some research into class based inventory systems, I think I got a bit overzealous with the scriptableObjects.

Thanks again :slight_smile:

I’m just referring to an instance of a plain old C# class. Not a MonoBehaviour, not related to GameObjects, or any Unity concept whatsoever. If you are using a framework like Json.NET you can then decorate these C# classes as needed to make them easy to serialize.

If your items are just static data containers, then it’s fine for the item to completely be described by a ScriptableObject. For example maybe you have a “bronze sword”. The bronze sword does 5 damage and weighs 10 pounds. It ALWAYS does 5 damage and ALWAYS weighs 10 pounds. In that case a ScriptableObject is perfect for defining that sword.

Then, as you suggested, you can have an inventory. Now the inventory just needs to be a list of those ScriptableObjects. When you serialize your inventory, you just need to serialize a list of references to those ScriptableObjects, and not the whole SO itself (because the actual data for the SO is a static part of your game). The easiest references to use would be either the name of the SO asset for use with Resources.Load, or it can be an Address as per Unity’s Addressable Asset System: https://docs.unity3d.com/Packages/com.unity.addressables@1.4/manual/index.html Either of those will let you load your item ScriptableObjects back into memory when you load your game, based on the list of references that you serialized from your inventory.

It looks like you’re serializing the class content, and not serializing a reference to the SO. In that case, when bringing it back it will have no way to reattach to the original asset.

You need to give it some GUID information or a path to Resources.Load or another API point to fetch items by name (and just serialize their name and stack count, for instance).

In other words, try serializing data you can use to find the correct reference and when deserializing you can use that data to lookup and assign the data that should be in that field.

I’m going to do a little research into a non-scriptable based inventory system then and see how I get on, I did start to suspect that I wasn’t programming in the most efficient way. I havn’t actually come across Resources.Load yet, so that will be a good starting point too. It would also be good to get some experience programming straight C# classes.

The problem with the ScriptableObject inventory was that upon loading, all references would be gone however it did make it easy for them to talk to each other. It might be quite a bit of work yet but I appreciate the support.

Hi LaneFox, thanks for your advice too!

Resources.Load was mentioned above, so with two experienced people suggesting this I will defo look into and post my solution here for other people in the future.

With the Resources.Load reference, would that be something that is coded into the serializer or perhaps in the inventory itself? I can imagine the logic that when deserializing, the inventory immediatly knows where the connection is rather than manully setting the connection in the editor.

You’ve both been a great help and I think once I get my head around this, I’ll be a better programmer for it.

Thanks again guys! :slight_smile:

The idea is that you serialize enough information for your deserializer to rebuild or reconnect things. Resources.Load is one way of loading fixed data points, so by serializing the path to that asset you can deserialize by pulling that path out of json and using Resources.Load to reconnect it.

Resources.Load is not the greatest. It has caveats and generally isn’t recommended at scale. There are better ways but if your game is pretty simple then it’s not always a bad idea to use it. Not everyone has to conform to some big corporate standards of distribution so it makes sense to do some less than ideal things sometimes.

Your serialized inventory might look something like a 2d array - first column with something to lookup the datapoint via an api you have or with Resources.Load. Maybe “SuperSword3000” or “Assets/Weapons/SuperSword3000Data”. The second column could be a stack count. With this, you can rebuild the inventory from scratch by finding the relevant data SO’s and putting them into the new inventory with the saved stack count.

Conceptually it’s pretty straightforward. Ideally you would not use Resources.Load but then again don’t rule it out just because we said so. Do your own research as well.

ScriptableObjects are interesting, because they preserve states and are a shared reference, rather than strictly a value type.
ScriptableObjects are not only reference types, but are also serialized, meaning the state is saved even after termination of the game.

For example, if you have a public float health = 10, you take two hits, and set it to health 8, that health variable is saved as health 8 when program terminates.
If you run the game again, and take two more hits, you’ll be at 6 health.

In order to not preserve state, you can mark a field as either static, or [NonSerialized].
NonSerialized fields must be given a default value - same as static variables, otherwise ScriptableObjects left in a disposed state will throw an error when re initialized containing a null value.

No, they are not saved after termination of the game. ScriptableObjects only serialized inside the Editor (that’s why you see the values you have changed during play mode saved, like any other assets).

Nothing happens in the built game. You change the values, restart the game and you will have the original values.

The NonSerialized will prevent to write the values in the editor.

2 Likes

The word “serialized” is misinterpreted for unity objects by new people, to mean what it usually means for non-unity objects. No, they can’t be used for a save game like regular C# objects can, and in fact, you can’t even serialize/deserialize scriptableobjects or monobehaviours with a regular serializer.

2 Likes