Save system questions

I am wondering how to implement a save system into the game I am making. Every tutorial I have looked at has used simple scripts and scenes to show off their version of a save system and not a single one I have watched has given me even a hint as to where to start.

My scenes are very complex (e.g. multiple “enemy” prefabs with multiple scripts attached to each) and all the tutorials I watched (I might be receiving the wrong message from them) have given me the impression that i would have to make a save script with every iteration of each var/int/bool/float/vector3 for every object in the scene (e.g. enemy1 has 40 health and enemy2 has 10 health, and I would have to make the save script contain enemy1’s health float as well as enemy2’s health float)

In other words, it seems like for every scene I would need a specific scene save script that has all the variables, integers, floats, booleans, and vector3’s of every separate object in that scene. And if I wanted to change something (like place down another enemy prefab and make an enemy3) I would have to redo the save script to account for the changes made in the scene.

Do I put a single master “save script” for each scene, or do I put a “save script” on everything in the scene?

Am I missing something?

This is rather interesting, I have never attempted a save/load setup nor read up on one.

If I was to have a crack at it myself, this is where I would start. Note, this code is theory/sudo. You would have to work with it to get something going.

The idea being every game object you want to save data on requires a SaveItem component, and you then use reflection to gather all the public variables available and choose which ones you want to store. I did not write that part as its a whole different piece of work for the editor script… Then in a scene, you have a save item manager that you can call so save/load.

The way this is written is open to flaws. It doesn’t deal with parent transforms, for instantiation. But its something to get your mind thinking :slight_smile:

[System.Serializable]
public class SaveItemDetail
{
    FieldInfo info;
    Object value;
    Component targetComponent;   
}

public class SaveItem : MonoBehaviour
{
    private string gameObjectName;
   
    public List<SaveItemDetail> saveItemDetails;
   
    public void SavePrefs()
    {
        foreach(SaveItemDetail item in saveItemDetails)
        {
            item.value = info.GetValue(targetComponent);
        }
    }
   
    public void LoadPrefs()
    {
        foreach(SaveItemDetail item in saveItemDetails)
        {
            if (this.GetComponent<targetComponent.GetType()>() is null)
                targetCompoent = this.AddCompoent<targetComponent.GetType()>()
            else
            item.SetValue(targetComponent, item.value);
        }
    }
   
    void Reset(){
        gameObjectName = this.GameObject.name;
    }
}

public class SaveItemEditor : Editor
{
    // Create your own editor view to handle editing the array list, where you use reflection to grab public valraibles.
}

public class SaveItemManager
{
    private const string savePath = "....";
   
    public void LoadSaves()
    {
        List<SaveItem> saveItems = //Deserialize from save path.
        foreach(SaveItem item in saveItems)
        {
            GameObject go = (GameObject.Find(item.gameObjectName) is null) ? Instantiate(new Gamoebject(item.gameObjectName)) : GameObject.Find(item.gameObjectName);
            if (go.GetComponent<SaveItem>() is null)
                go.AddComponent<SaveItem>() = item;
            else
                go.GetComponent<SaveItem>() = item;
               
            item.LoadPrefs();
        }
    }
   
    public void StoreSaves()
    {
        List<SaveItem> saveItems = GameObject.FindObjectsOfType<SaveItem>().ToList();
        // Serailes class to SavePath
    }
}

I should have mentioned that I am entirely new to game making, so what may be “complex” to me is “simple” to veteran game-developers.

If it helps, an example of a scene (Using words, I don’t know how to screenshot)
Scene

Level environment (A bunch of small and large meshes fit together like small hallway meshes and large special room meshes)

Load zones, a static area that acts like an airlock. (side1 is active and side2 is not, when the load zone is triggered, the sides swap activity so side2 is active and side1 is inactive).

Enemies, with health, AI, target, drops, and damage scripts. Prefabs are attached to these that tell what each enemy drops.

Weapons, with ammo, clip, and a script that puts a prefab in the player’s inventory. Prefabs are attached to these that tell what weapon is put in the player’s inventory.

Lockers, with an open/close script.

Breakable loot, with health, and drop scripts. Prefabs are attached to tell what they drop when broken.

The Player, with health, armor, movement, looking, keys, interact, and weapons scripts. Many prefabs are attached to the player’s child objects (like a weapon master with three “weapon holder” objects attached, and prefabs are attached to the weapon holders and deleted when weapons are dropped.)

Held weapons, with damage, clip, ammo, drop, and bobbing scripts (drop script stores a prefab weapon drop)

A Canvas, with things like ammo, and health. (UI is updated by the scripts on the player)

Doors, with an open/close script.

Keys, with a script that adds a key to the players inventory.

Pickups, with health, armor, or ammo scripts attached.

Dropped weapons, that are the same as regular weapons but their clip is changed to be the same amount as a held weapon’s was when it was dropped.

Events, (e.g. open a door and an animation plays where a enemy mesh is “killed” by a falling ceiling tile).

With all the things inside the scene it seems astronomically easier to just save the state of the game, rather than pick and choose what I want saved.

Saving/Loading is never simple.

There’s tons of tutorials out there on Youtube to give you an idea of the process.

Basically step 1 is identify what needs to be saved. Might just be a single digit, as in “what level am I on?” Might be a lot more: GameObjects, inventories, internal states of running scripts, positions, rotations, etc.

Keep in mind the more you save the more debugging and painstaking engineering you’re going to need to do. And it gets really hairy.

Step 2 - design a data structure to hold all the above data, as well as enough extra data to restore it on LOAD (i.e., a foolproof way of moving the data back to whoever originally asked for it to be saved).

Step 3 - design and implement something to iterate the stuff you want to save and extract that into the data structure

Step 4 - easy : write it to disk (generally use JSON for serialization)

On load, it’s the opposite:

Step 1 - read structure from disk into memory

Step 2 - iterate all data, reinject it into the running game. This might take multiple steps: iterate some data to figure out what scene(s) to load, then load that scene(s), then iterate the rest of the data to inject it into the running scene, possibly even creating additional GameObjects in the scene based on the save data.

Step 3 - let the game run!

Let me just add that with ALL such massive engineering efforts, start small. Savegame system version 1 should just save and restore your score, or something equally trivial.

But get that working and prove out ALL steps from extracting, storing, writing, reading, restoring, restarting, and do so with very simple data, and only then start to add the next piece, such as your gold or or what scene you are on, or whatever is next to save.

13 Likes

The tutorials on youtube make it seem like in order to save, I have to reference every var, int, float, bool, string, vector3, quaternion, and component on every unique object, light, rigidbody, and empty in the scene to put into the save script.

My understanding of computers work and how data resides in memory would agree with this. There are constructs to help you accomplish this, but these constructs require you to use them in your engineered solution.

Any hint as to what these constructs are?

Reflection can help you locate unspecified future variables, but you still have to make sense of any stored serialized data, otherwise how would you get it back into those variables?

Think of this: you build something out of Lego and you photograph it.

Is that enough to rebuild it?

It probably is if that thing is 2 bricks clipped together.

It probably isn’t if that thing is 100,000 specialized bricks all hidden from one another.

You might need a lot of photographs.

That’s why I said try to keep the saved data small. Otherwise you’ll instantly be bogged down.

You don’t need to save every single variable in your game, only the ones that need to be saved.

For example, you don’t need to store the settings of your directional light if it is static and unchanging in the scene. You probably do need to save your player’s position though.

You often don’t need to save too much data, depending on the game. For example, you can generally get away with not saving anything related to your enemy AI, if your AI is going to be immediately aggro’d anyway on load due to being near the player.

If I were to implement a save system, I would probably start with something like this:

public interface ISaveable
{
  public Dictionary<string, string> GetSaveData();
  public void SetSaveData(Dictionary<string, string>);
}

and implement it on every single entity that needs to be saved (player, enemies, but not lights, level geometry).

public class Player : Monobehaviour, ISaveable
{
  public Dictionary<string, string> GetSaveData()
  {
    return new Dictionary<string, string>(){
      {"X", transform.position.x},
      {"Y", transform.position.y},
    }
  }

  public void SetSaveData(Dictionary<string, string> saveData)
  {
    Vector2 pos = new Vector2(saveData["X"], saveData["Y"]);
    transform.position = pos;
  }
}
4 Likes

It really depends on your game. But there’s a reason Resident Evil and Final Fantasy had save points instead of letting players save at any time. Think carefully about your game and what (and when) you actually need to save. Think also about the possibility of limiting the player’s opportunities to save. Most modern games I’ve seen do some kind of autosaving, and they typically do that autosaving at or around checkpoints. For example The Last of Us 2. There’s plenty of scenes where the player is fighting a dozen different zombies. But the game never saves the health of any zombies in its save files. It pretty much just saves the player’s inventory and what checkpoint they’re up to.

For example, you talked about saving every single enemy’s health and location and rotation etc… Maybe reconsider allowing the player to save “in the action” Maybe the player can only save between battles? Just something to think about. The other thing is, you don’t need to save every variable in the scene. Maybe you need to store the enemy’s current health, but something like the enemy’s maximum health is simply part of the static data for your game. It’s on the enemy prefab. You just need to know which prefab to use for the enemy.

2 Likes

Using your example, if I have two different objects (player, enemy) and I wanted to save the second object’s position, would I have to make another “ISaveable” file? If I did, wouldn’t that make two different save files?

And in code, would bools/floats/ints/vars be referred to as “strings” or would I also have to add

public Dictionary<string, string, bool, float, var, int>(){
{"X", transform.position.x},
{"Y", transform.position.y},
{"F", float},
{"B", bool},
{"I", int},
{"V", var},
}

to it?

Both your player and enemy script can implement the same ISaveable interface.

public class Player : Monobehaviour, ISaveable
{
}

public class Enemy: Monobehaviour, ISaveable
{
}

All the interface will do is say that the class has a Save/Load function, How exactly that works is dependent on the object’s specific implementation of that function, but presumably you already have a player and enemy script, so just put that unique logic in those classes.

As for your second question, the example I gave maps strings to strings. If you want to save floats, ints, bools etc first convert them to a string

enemHealth.ToString()

And when you load them back in, convert them from a string

enemyHealth = float.parse(data["Health"]);

Here is an example based on my first post (I just realized I didn’t do any of the converting necessary in that example)

public class Player : Monobehaviour, ISaveable
{
  public Dictionary<string, string> GetSaveData()
  {
    return new Dictionary<string, string>(){
      {"X", transform.position.x.ToString()},
      {"Y", transform.position.y.ToString()},
      {"Health", myHealth.ToString()},
      {"Ammo", myAmmo.ToString()},
    }
  }

  public void SetSaveData(Dictionary<string, string> saveData)
  {
    Vector2 pos = new Vector2(float.parse(saveData["X"]), float.parse(saveData["Y"]));
    transform.position = pos;
    myHealth = float.parse(saveData["Health"]);
    myAmmo = int.parse(saveData["Ammo"]);
  }
}
3 Likes

If I put down another “enemy” and give it the exact same script that I gave to the first enemy, will it work with both, or will I have to specify that they are completely separate objects for it to work?

Am I also correct in thinking that I will have to put [System.Serializable] into the code?

However you choose to route restored save data back to the objects requiring it, it must be completely unambiguous.

You may need to save an object name or some sort of other ID, so when you go to reload the game, you know what saved data goes with which object.

No, the example code has nothing to do with Unity’s serialisation.

After putting down the second enemy and renaming it to “enemy2” in the inspector and telling the code to save the object name too, does it successfully recognize them as different objects when I save?

No, nothing I wrote does that, your supposed to do that bit yourself.

When you go to save everything, you are probably going to be iterating over each ISaveable and getting their save data. When you do that, also get the objects name and save that too.

It’s possible, I actually just thought the tmpro inputfield was a regular inputfield but with a tmpro label instead of default text.

I have been thinking it over and I think that the best way for me to implement saving is to make a code that checks for everything in the scene (gameobjects, components, vars/ints/floats/etc) and then do the complicated json/binary writing.

Thank you all so much for helping me!
(sorry for necro-posting)

2 Likes