How to Save and Load a list of Scriptable Objects from a File?

I’d like to be able to load and save a list Scriptable Objects. For example, an players inventory which is a C# List of Scriptable object type Items that needs to be saved to a file and loaded from said file when the player starts the game again.

I’d preferably like to not have to store this data on a server but instead keep it local where the game lives. But at this point if there is an simpler way to load SO, I’d be open to hearing it.

I’ve looked into addressables and saving to json files, but have ran into dead ends with both (I can’t figure out a way to load a SO from addressables without using Addressables.InstantiateAsync(“string/to/path”); and json files are unable to save the SO since its id changes every time I reload the game). I won’t rule out either of these options, but if someone could provide a simple example of using one of these methods or a new method or even just talk through a possible workaround that would be great!

Why overcomplicate this and not just use AssetDatabase? Using AssetDatabase.CreateAsset() you can save a scriptableobject to an asset file. At editor-time you can load it via AssetDatabase.LoadAssetAtPath() and you can also add it to any form of asset retrieval method (assetbundles, addressables …) for loading at runtime. As your ScrptableObjects are not supposed to be altered at runtime, this should be the easiest way.

Saving and loading data can be done by writing and reading variables to and from a file. I will get to how you can do that later, but just a quick explanation on files.


Files can store data that contain text. This text could be a string, a number, a bool or byte etc. It’s important to note that not all data types can be converted to a string perfectly. Therefore, we need to save a string that represents that data type, and when we read the file, we need to interpret that string correctly.


Let’s take the example of saving the position of a game object to a file. You can’t save the transform of the object, but you can save 3 floats that contain the x, y and z position of the object. Or use Vector3.ToString().


Reading and Writing from/to a file

You need to be using System.IO to perform operations on files.


Reading a file

You can use the File class to read all lines from the file into a single string, or use the StreamReader to read the file line by line, and perform operations on the line that is currently being read by the stream reader. For example:

    void ReadFile(string directory)
    {
        // Getting raw text
        string text = File.ReadAllText(directory);

        // Using a stream reader
        StreamReader reader = new StreamReader(directory);

        string line;
        while ((line = reader.ReadLine()) != null)
        {
            if (line == "Line 4: ")
            {
                // Do stuff
            }
            // etc.
        }
    }

Writing to a file

You can write to a file using the File class or by using the StreamWriter as well:

    void WriteToFile(string directory)
    {
        // Overwrites any text already in the file
        File.WriteAllText(directory, "Hello world");

        // Adds on to the end of an existing file
        File.AppendAllText(directory, "Hello world");

        // Overwrites any text already in the file
        StreamWriter writer = new StreamWriter(directory);
        writer.Write("Hello world");
    }

If the file does not exist already, it will be created. Nice.


Saving your data

To save the scriptable objects, it would be good practice to go through every object, save the index of that object in the list, then underneath, save its properties, then leave a space for the next object. Here’s an example:


Scriptable object called Item:

[CreateAssetMenu(fileName = "New Inventory")]
public class Item : ScriptableObject
{
    public string Name;
    public int ID;
    public float Price;
    public Vector3 Position;
}

Class called ‘Backpack’ with a list of scriptable objects called inventory:

public class Backpack : MonoBehaviour
{
    public List<Item> Inventory;
}

The Save() method would look something like this:

    void SaveInventory(string directory)
    {
        for (int i = 0; i < Inventory.Count; i++)
        {
            Item item = Inventory*;*

File.AppendAllText
(
// Saves object index
directory, "Object " + i.ToString() + "
" +

// Saves object properties
item.name + "
" +

item.ID.ToString() + "
" +

item.Price.ToString() + "
" +

*item.Position.ToString() + "

"*
);
}
}
And the Load() method would look something like this:
void LoadInventory(string directory)
{
StreamReader reader = new StreamReader(directory);
string text = reader.ReadToEnd();

// Remember to close the stream IMPORTANT!
reader.Close();

string[] lines = text.Split(’
');

for (int i = 0; i < lines.Length; i++)
{
// Is a new object
if (lines*.Contains(“Object”))*
{
// Get properties
string name = lines[i + 1];
int id = int.Parse(lines[i + 2]);
float price = float.Parse(lines[i + 3]);
Vector3 position = StringToVector3(lines[i + 4]);

// Create new scriptable object and add to inventory
Item item = ScriptableObject.CreateInstance();

item.Name = name;
item.ID = id;
item.Price = price;
item.Position = position;

Inventory.Add(item);
}
}
}

Vector3 StringToVector3(string sVector)
{
// Remove the parentheses
if (sVector.StartsWith(“(”) && sVector.EndsWith(“)”))
{
sVector = sVector.Substring(1, sVector.Length - 2);
}

// split the items
string[] sArray = sVector.Split(‘,’);

// store as a Vector3
Vector3 result = new Vector3(
float.Parse(sArray[0]),
float.Parse(sArray[1]),
float.Parse(sArray[2]));

return result;
}
----------
Let me know if this works or not, and I’ll try to help. @SaintA721