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();
}
}
```
GameState.LoadGame()function iterate over a list of your level packs and pass the loaded level time data to them? Or having it cache that data in a place the level pack can read it just before you try to display the saved time value? Help us understand where this needs to fit in your code and what specific obstacle you need help overcoming. \$\endgroup\$OnBeforeSerialize? That would be the only way this could work. So noGameStatesingleton. I have multiple Levels, so that would mean it would open that file that many times. Would that affect performance? \$\endgroup\$GameStatescript. Should I just remove the variables off the GameState and haveLoadGameupdate all the ScriptableObjects instead based on the data I have loaded? \$\endgroup\$