1
\$\begingroup\$

I've been trying to understand how best to approach carrying state over between scenes, and how to save/load game state. I've built an isolated project to focus on building out this approach.

You can view the project on GitHub.

Essentially, my approach is to use a singleton StateCache, which tracks all state objects, such as the PlayerState in my example. When the PlayerMove script (and therefore the player object) is destroyed (i.e. when a different scene is loaded), it writes its latest state to the StateCache. When clicking the Save button, all state in the cache is written to a JSON file. Finally, when loading the game data, the JSON is parsed into appropriate state types and the StateCache is prepopulated with this data.

Some caveats here, though, that make this more complex:

  1. To be able to load the state using strong typing, I need to write the actual state object type names into the JSON object.
  2. When loading, I load the next scene first and then prepopulate the cache. However, the player reads its state when the scene is loaded (before the cache is populated). If I load the cache before the scene is loaded, then in the example, the player will be destroyed and overwrite the loaded data in the cache (this could be mitigated by loading from a main menu scene where no player exists).
  3. Due to #2, I need to run an additional step to find all active instances that have a state, and apply the state from the cache (if it exists).
  4. Also, not necessarily a caveat, but adds complexity, I've brought in Zenject for dependency injection to get away from having static instance references.

To try to explain it visually, I've also created this diagram:

Save/load/cache system

This is working in my isolated project. But it feels overly complicated for what seems like a common problem. I'm relatively new to Unity, building a game as a casual hobby, so I'm keen to understand recommended approaches here. Is there a better way? Or is this complexity necessary?

\$\endgroup\$
7
  • 3
    \$\begingroup\$ @fatdrogen Perhaps you would like to write an answer that explains how exactly to do that? \$\endgroup\$
    – Philipp
    Commented May 4, 2023 at 9:35
  • 2
    \$\begingroup\$ Related question: Saving player's progress in a Unity game \$\endgroup\$
    – Philipp
    Commented May 4, 2023 at 11:13
  • 1
    \$\begingroup\$ @fatdrogen I didn't want to use PlayerPrefs as it appears the recommendation is to use these for customization and options (brightness, volume level, etc). It also writes the data out in a format that can be modified (my read/write to save file approach can be extended with an encrypted binary output, if I want). I'm fairly new to the use of scriptable objects, though, so I'd be interested to understand how you would approach state caching (between scenes) and read/writing save data with them \$\endgroup\$ Commented May 4, 2023 at 14:46
  • 2
    \$\begingroup\$ Please don't extend a question with additional questions after you already received an answer. Moving the goalposts like that is unfair to the people who answered. I therefore rolled back your edit. If the answers to a question created more questions for you, please post them as new questions. \$\endgroup\$
    – Philipp
    Commented May 4, 2023 at 14:47
  • 2
    \$\begingroup\$ @fatdrogen PlayerPrefs is intended for saving preferences such as volume settings (hence the name), not complex game state. It's not suitable for Samuel's needs. \$\endgroup\$
    – Kevin
    Commented May 4, 2023 at 18:12

1 Answer 1

3
\$\begingroup\$

Unity does not have a build-in system for saving and loading gamestates, so there is no standard solution for this. Which means that there are as many approaches to gamestate serialization as there are Unity programmers.

The problem I see with your solution, though, is that you write the state of an object to the "StateCache" every time OnDestroy is called. Your line of thought is probably that when the current scene gets deconstructed, then the OnDestroy method for all the objects will be called, so they all get an opportunity to write their state to the cache.

But there are a couple problems with this.

  1. Objects might get destroyed during the scene for all kinds of gameplay or architectual reasons. You probably don't want to serialize them in that case. The opposite, actually.
  2. You have no control over the order in which objects get destroyed during scene changes. Which could be a problem if you have objects that depend on other objects for their serialization.
  3. You might want to be able to save the game without destroying everything in the scene.

So using the OnDestroy method is probably not the best idea.

A better idea might be to have a dedicated method for each GameObject that does the serialization. Then the process for scene transitions or creating a savegame would be:

  1. Clear the StateCache
  2. Populate the StaeCache from scratch by calling the save-method of each object in the scene
  3. Write the savegame file / switch the scene

How do you call the save-method of each object in the scene? Well, did I already mention that there are as many approaches to gamestate serialization as there are Unity programmers?

  • You could create an interface ISaveable and then give every saveable gameObject a MonoBehavior that implements that interface. You are then able to get all of those via FindObjectsByType.
  • Or you could do the same thing with an universal Saveable MonoBehaviour that finds out what MonoBehaviours on the same gameObject need to be serialized and how. Probably by using some custom [Attributes].
  • Or you could use the good old Unity messaging system and use the BroadcastMessage method to call every method with a specific name.
  • Or you could have a OnSave UnityEvent every saveable game-object is subscribed to
  • ...and many more.

The same applies to loading. Relying on Awake() handling it for each gameObject might turn out to be a mistake if objects spawn or respawn during gameplay. So you should also have a dedicated method for setting up each gameObject based on the data in the StateCache. You will also probably have situations where you don't know how many objects of what type are supposed to be in the scene while you build it. So you are probably going to need spawners that instantiate any number of gameObject based on what's in the StateCache.

\$\endgroup\$
1
  • \$\begingroup\$ thanks for your response. I have tried a number of these suggestions in the sample project. I've extended my question above with a little more context here. It sounds like what you're suggesting is similar to the IPreserveState mechanism I built into the sample project. There is also logic behind hooking into the Awake() and OnDestroy() lifecycle events \$\endgroup\$ Commented May 4, 2023 at 14:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .