Saving and Loading For the Win!


At a Glance

Originally, I planned to write this a few days ago, however, I had to stomp out a few bugs in my “load game” code. I finished stomping and can happily report I have successfully saved, loaded, and reapplied the save game state. Woot!

Read on to find out how I managed my first ever attempt of saving/loading game data and how I coded it.

Saving and Loading

Trials and Tribulations

At my day job, I have worked with SQL Server databases and web site state management for years. I figured how hard is it to save and load game data? Haha! It turned out it was a wee bit more challenging than I thought. I found that setting a List<Location> object to null and then applying the loaded state would leave me with dreaded null reference errors. Ugh. Armed with my trusty VS Code Unity Debugger I went to work.

First obstacle I overcame was to learn not to set a breakpoint in the Update() method, cause, uh, the game stops every frame and I couldn’t even hit the load button. I did learn that VS Code has conditional breakpoints, however, I didn’t use them as I was too frustrated to learn something new. I added it to my learning list. Instead I set the breakpoint after I hit the load button and found the bugs that way.

Now…

To the Code!

I reviewed a few tutorials, however, I didn’t write them down, so I’ll just say thanks to all the saving/loading tutorials out there. At a high level, I chose to copy all my data to a SaveData class and then pass it to a BinaryFormatter for serialization/deserialization. I wrote a GameFile class that encapsulates all the actual file management code. I figured I would need similar code in all my games so why not start creating reusable content now.

I copied my GameFile class below, and I added it and SaveData to my Unity 2D Examples repo on GitHub. I probably need to rename that repo as I plan to add more than just 2D examples to it. Anywho, GameFile uses a preset file name because there can be only one save game at a time. I highlighted the properties and methods below. If you have any questions or suggestions please leave me a comment below.

using UnityEngine;
using SpaceMonkeys.Idle.DungeonExplorer;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace SpaceMonkeys.IO
{
    public static class GameFile
    {
        #region Private Members
        private static readonly string fileName = @"zones.smd";
        private static readonly string fullPath = Path.Combine(Application.persistentDataPath, fileName);
        #endregion Private Members

        #region Public Properties
        public static bool SaveFileExists => File.Exists(fullPath);
        #endregion Public Properties

        #region Public Methods
        public static void SaveGame(SaveData data)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            using (FileStream stream = File.Create(fullPath))
            {
                formatter.Serialize(stream, data);
            }
        }
        public static SaveData LoadGame()
        {
            var data = new SaveData();
            if (File.Exists(fullPath))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                using (FileStream stream = new FileStream(fullPath, FileMode.Open))
                {
                    data = (SaveData)formatter.Deserialize(stream);
                }
            }
            else
            {
                Debug.Log($"No save file found, reset the game.");
                data = null;
            }

            return data;
        }
        public static void DeleteSave()
        {
            // TODO: make this more robust in the future
            try
            {
                File.Delete(fullPath);
            }
            finally { }
        }
        #endregion Public Methods
    }
}

Currently, I added buttons for saving and loading, that way I controlled the testing. Once I move to beta then I will enable auto-save and auto-load functions. The save button will stay so the player can manually save if they wish. I will add some style to it.

Save / Load Buttons

Now that I had the GameFile class I created simple methods in my GameManager class to handle the work. Below, I copied select snippets from my GameManager class, it holds a reference to all 12 zones in the game (line 1) and exposes them through the Zones property (line 3). This allowed me to create a new SaveData class by passing in the Zones and then feeding that to my SaveGame() method. On the loading side, I called the LoadGame() method, checked for null to reset the game, otherwise I loaded the data back into each zone. I don’t have an actual reset method yet, well except by deleting the save file and restarting, but I’ll get there. How do you handle your save/load routines? Drop me a comment and let me know.

[SerializeField] private List<ExplorationZone> _zones = default;

public List<ExplorationZone> Zones
{
    get => _zones;
    private set => _zones = value;
}

public void SaveGame()
{
    GameFile.SaveGame(new SaveData(Zones));
}
public void LoadGame()
{
    var save = GameFile.LoadGame();
    if (save is null)
    {
        ResetGame();
    }
    else
    {
        foreach (var item in save.Zones)
        {
            Zones[(int)item.ZoneType].LoadData(item);
        }
    }
}

The SaveData and ZoneData classes are where the “magic” happens, so let’s look at them now. I will need to constantly keep updating these as my code changes. Right now, I only have to manage the zone feature, however, in the future I plan to have other features like adventurers, achievements, and stats. As I get to them I will refactor my code, however, for now, I just needed to solve this problem.

Let’s look at SaveData first. Since I know I will have changes I include a SaveFormat variable (line 14) so I can track released changes for backwards compatibility. After that I have a List<ZoneData> (line 15) to store all my zones and currently everything exists in the zone so that covers everything. From there I created two constructors, one for loading a game (line 19) and one for saving a game (line 20). The constructor for saving explicitly converts the List<ExplorationZone> to a List<ZoneData> (line 24).

using System.Collections.Generic;
using SpaceMonkeys.Rpg;

namespace SpaceMonkeys.Idle.DungeonExplorer
{
    [System.Serializable]
    public class SaveData
    {
        /* This represents all the data needed for saving/loading the game
        ** Need to save:
        ** All exploration zones - which captures all items within
        */
        #region Members
        public float SaveFormat = 0.1f;
        public List<ZoneData> Zones = new List<ZoneData>();  // Exploration Zones
        #endregion Members

        #region Ctors
        public SaveData() { } // for loading a game
        public SaveData(List<ExplorationZone> zones) // for saving a game
        {
            foreach (var item in zones)
            {
                Zones.Add((ZoneData)item);
            }
        }
        #endregion Ctors
    }

I needed to correct some design flaws in that my data hierarchy mixed gameObjects with what I call “language classes.” A “language class” is a class that does not inherit from MonoBehaviours or any Unity based class. When I started building this game I wasn’t sure which way was best (and I’m still not), however, I know that next time I will keep some separation between gameObjects and language classes. If you look at lines 22-25 I explicitly pull out Quests (a language class) from Location (a gameobject). I ran into problems trying to get MonoBehaviours to serialize, and I didn’t want to expend tons of time to figure it out. The last important note I highlighted is the explicit operator method (line 32) which allowed me to cast from ExplorationZone to ZoneData in my SaveData class.

using System.Collections.Generic;
using SpaceMonkeys.Rpg;

namespace SpaceMonkeys.Idle.DungeonExplorer
{
    [System.Serializable]
    public class ZoneData
    {
        #region Members
        public ExplorationZoneType ZoneType;
        public bool IsZoneActive;
        public List<Quest> Quests = new List<Quest>();
        public ResearchManager ResearchTree = default;
        public Stat MaxLocations;
        #endregion Members

        #region Ctors
        public ZoneData(ExplorationZone data)
        {
            ZoneType = data.ZoneType;
            IsZoneActive = data.IsZoneActive;
            foreach (var item in data.Locations)
            {
                Quests.Add(item.Quest);
            }
            ResearchTree = data.ResearchTree;
            MaxLocations = data.MaxLocations;
        }
        #endregion Ctors

        #region Operators
        public static explicit operator ZoneData(ExplorationZone zone) => new ZoneData(zone);
        #endregion Operators
    }
}

I created a few LoadData methods that take the ZoneData and injected it back into the game state. I included a snippet from ExplorationZone below.

public void LoadData(ZoneData data)
{
    if (ZoneType != data.ZoneType) return;
    if (_contentAreaRef.transform.childCount > 0)
    {
        foreach (Transform item in _contentAreaRef.transform)
        {
            RemoveLocation(item.GetComponent<Location>());
        }
    }

    IsZoneActive = data.IsZoneActive;
    ResearchTree.LoadData(data.ResearchTree);
    MaxLocations = data.MaxLocations;
    CreateLocations(data.Quests);
    if (ZoneType == ExplorationZoneType.Plains)
        FirstLocation.IsSelected = true;
}

Keep On Questing

Well I hope you enjoyed the tour of my saving/loading code. I ran through it at a mid-level, once I have time to really dig in and learn more I’ll write up a tutorial. How do you handling saving and loading in your game?

Well, next up I will build pages here and on itch.io for my game and deploy the alpha. Oh, and I have a devlog coming up on using C# events to auto-complete my quest’s goals. It’s a great research item that I plan to fill out later in my dev process.

Life’s an adventure, what’s your quest?

I originally posted Saving and Loading For the Win! on WeirdBeard’s Blog.

Leave a comment

Log in with itch.io to leave a comment.