I’ve been at this for two long days, trying to figure out a way to handle saving and loading data for a mobile puzzle game I’m working on. The game has level packs and each level pack has multiple levels. Level packs can be purchased via IAP.
I need access to the fixed data and an easy way to add new data, so I used Scriptable Objects to store my Level Pack and Level data:
LevelPack
- id
- type (paid, free)
- IAPproductId
- levels (list of levels)
Level
- id
- totalTime (total time to solve puzzle)
- warningTime (give a warning during level)
- levels (list of levels)
I also need to store the following dynamic data, which can change during runtime:
- completedTime for each level
- isPurchased for each level pack
I tried adding these to the scriptable objects and it’s really easy to maintain and work with. But I can’t figure out a way to persist that data between sessions or game updates.
I created a save system that saves just the completed levels to an external file using binary formatter. So every time a level is completed, I change Level.completedTime
to the new time on the scriptable object, then do GameState.Instance.UpdateLevelProgress(levelPack.id, levelId, time);
to save the file that contains a dictionary with all the completed times.
public void UpdateLevelProgress(int levelPackId, int levelId, float time)
{
if (!levelProgress.ContainsKey(levelPackId))
{
levelProgress.Add(levelPackId, new Dictionary<int, float>());
}
// set time
levelProgress(levelPackId)(levelId) = time;
// save to file
SaveGame();
}
Even though I figured out how to save, I don’t know how to load the data from the file into each Level scriptable object. There is an onEnable
function for ScriptableObject
, but that runs way before my GameState.LoadGame()
function that runs inside a preload scene. So GameState.Instance.levelProgress
is empty at that point.
Is there a better, cleaner way to do this?
PS:
Here’s my GameState file:
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
(System.Serializable)
public class GameStateData
{
public Dictionary<int, Dictionary<int, float>> levelProgress;
public GameStateData(GameState gameState)
{
levelProgress = gameState.levelProgress;
}
}
public class GameState : Singleton<GameState>
{
// Prevent non-singleton constructor use
protected GameState() { }
// declare variables
public Dictionary<int, Dictionary<int, float>> levelProgress = new Dictionary<int, Dictionary<int, float>>();
#region Load / Save Methods
public void LoadGame()
{
string path = Path.Combine(Application.persistentDataPath, "game.save");
if (File.Exists(path))
{
BinaryFormatter formatter = new BinaryFormatter();
FileStream stream = new FileStream(path, FileMode.Open);
GameStateData data = formatter.Deserialize(stream) as GameStateData;
stream.Close();
// overwrite current data with loaded data
levelProgress = data.levelProgress;
}
}
public void SaveGame()
{
BinaryFormatter formatter = new BinaryFormatter();
string path = Path.Combine(Application.persistentDataPath, "game.save");
FileStream stream = new FileStream(path, FileMode.Create);
GameStateData data = new GameStateData(this);
formatter.Serialize(stream, data);
stream.Close();
}
#endregion
public void UpdateLevelProgress(int levelPackId, int levelId, float time)
{
if (!levelProgress.ContainsKey(levelPackId))
{
levelProgress.Add(levelPackId, new Dictionary<int, float>());
}
// set time
levelProgress(levelPackId)(levelId) = time;
// save to file
SaveGame();
}
}
```