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.
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.
Treasure Hunter: Quixxen's Suit
An idle quest to find the legendary Quixxen's Suit!
Status | On hold |
Author | WeirdBeardDev |
Tags | Idle, Incremental |
Languages | English |
More posts
- Alpha 5 Released - AdventurersSep 08, 2020
- Fixed Speed BugJul 18, 2020
- Alpha 4 ReleasedJul 04, 2020
- Working On More FeaturesJun 23, 2020
- Updated Goals For TestingJun 16, 2020
- Treasure Hunter Alpha 3 ReleasedJun 14, 2020
- Upcoming Features for Treasure HunterJun 05, 2020
- All Previous DevlogsMay 28, 2020
Leave a comment
Log in with itch.io to leave a comment.