Project: Treasure HunterCodename: Dungeon ExplorerGoal: Develop and release first gameInitial Commit: 7 Aug 2019 Devlog: 10 (see previous one)Dev Hours Since Last Devlog: 17.44Total Dev Hours: 178.55Allocated Dev Hours: 160 […]
Project: Treasure Hunter Codename: Dungeon Explorer Goal: Develop and release first game Initial Commit: 7 Aug 2019
Devlog: 10 (see previous one) Dev Hours Since Last Devlog: 17.44 Total Dev Hours: 178.55 Allocated Dev Hours: 160
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, enjoy your quest!
Git Commit History
15f8a61 2020-05-24 Enable multiple goals in game c5ddd40 2020-05-23 Add close button and setup two builds c9a729c 2020-05-23 Add research tree to load game bf896e0 2020-05-22 Start refactoring of loading research 88d647a 2020-05-22 Fix loading game state 328e499 2020-05-22 Refactor goal achieved event 78502ea 2020-05-21 Add goal updates to DisplayLocation 235adc8 2020-05-21 Refactor CurrencyGO properties 9045058 2020-05-21 Add documentation comment 88da4d6 2020-05-21 Add encounter updates to DisplayLocation 132bcc6 2020-05-21 Add currency updates to DisplayLocation 7572620 2020-05-21 Create DisplayLocation script 731bc18 2020-05-21 Minor update 616de84 2020-05-20 Add multiple items fb23785 2020-05-18 Add autosave feature c18e211 2020-05-17 Fix progress bar prefab to display correctly ac35bdf 2020-05-17 Refactor game saving/loading ece5021 2020-05-16 Add save capability 194c378 2020-05-15 Laid groundwork for implementing save/load mechanics 16e00ea 2020-05-14 Cleanup GameObjectExtension using statements df5642b 2020-05-14 Resolved merge conflicts 3cf9124 2020-05-14 Add company and project name
One day a crazy, wild-haired troll gave me a quest to create video games. I know C#, so I armed myself with Unity and loads of game dev tutorials and went questing. Now I call myself a "game dev" and pretend like I have a clue. :)