- Home /
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!
Answer by AbandonedCrypt · Feb 02, 2021 at 12:19 PM
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.
The problem isn't so much that I need to save the data inside of the Scriptable Object (from my understanding the data inside the SO is mean't to stay static since the data will get reset after the game is closed). I'm more in the market to understand how to save the specific SO a player would have acquired over their game play session. It sounds like with the Asset Database approach I could somehow put the SO in the database, grab the path to where that item lives in the database then save it to a file. When the game loads again, I would get the path to that asset from the file, then load/add it in to my inventory (lets say its a list) via the AssetDatabase.LoadAssetAtPath() you mentioned?
No SO data is meant to be persistent. Think of it as a data container that keeps its data consistent between editor time and run time. You are not supposed to save data into a scriptableobject during runtime. Scriptableobjects are not supposed to be altered at runtime.
i might misunderstand your use-case but the way i get it you try to keep track of player progress or something and save that into a scriptableobject at runtime? in that case a SO is the wrong thing to use and you are better off using a custom class of values that you serialize the old-fashioned way, into json or bytestream etc.
Answer by Llama_w_2Ls · Feb 01, 2021 at 01:49 PM
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[i];
File.AppendAllText
(
// Saves object index
directory, "Object " + i.ToString() + "\n" +
// Saves object properties
item.name + "\n" +
item.ID.ToString() + "\n" +
item.Price.ToString() + "\n" +
item.Position.ToString() + "\n\n"
);
}
}
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('\n');
for (int i = 0; i < lines.Length; i++)
{
// Is a new object
if (lines[i].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>();
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
Wow this is great thanks! I have a couple of edge cases such as when the Scriptable Object has other fields such as Prefabs, enums and references to other Scriptables Objects. But it sounds like the solution to that would be to increase the complexity of the Load and Save to include functionality to break apart the info inside the prefabs and SO's so they can be instantiated and passed into the original Scriptable Object later?
Exactly. For more complex objects, you need to interpret saved data. For example, you could already have a list of prefabs available to choose from, and when you load your saved data, you read the string, and choose the correct prefab from the list that matches the name of the string.
I would strongly recommend to not roll your own text based format. If you really want to do it anyways you have to pay much more attention to potential issues. One is versioning. If you roll out an update of your game and something has changed in your save format, you have to take that into account. This generally requires you to know the format the old save files was stored in. So including a version number / string is a must have. Next thing is the usage of float.Parse without an explicit culture will fail depending on your local culture setting. For example here in germany (but also in france and spain if I'$$anonymous$$ not mistaken) we use the comma as decimal point and not the dot. So your vector parsing code would completely break down. The same care has to be taken when saving the data as ToString also by default uses the local culture. You should always use the InvariantCulture for any kind of serialization. Next is your text based parsing is error prone. You search for a line that contains the string Object. However what happens when an object has the name "Object"? since you parse over every single line you would start parsing a new object at the name. You also really, really shouldn't use the static File.AppendAllText in a loop on the same file. Each call will open a file handle, seek to the end of the file, writes your line and closes the file again. This is not only horrible for performance reasons but also bad from a file updating point of view (thinking of SSD and their write cycles)
I would highly recommend to use a framework that does all the serialization for you in a consistent way. Commonly we use json as it's very compact and at the same time can represent arbitrarily complex data structures. I've written a very compact json library. There's also an extension file for some Unity types which allows direct writing of Vector, Quaternion or Color values.
$$anonymous$$y bad. I wasn't aware of the better approaches to saving data.
Your answer
Follow this Question
Related Questions
Save and (later) read scriptable objects 1 Answer
Problem when saving and loading 1 Answer
Saving Player Data for Multiple Scenes/Levels 2 Answers
Save and load an inventory 0 Answers