itch.io is community of indie game creators and players

Devlogs

Why DontDestroyOnLoad is Bad

Vapor Trails
A browser game made in HTML5

DO NOT FOLLOW THIS ADVICE

This is based on the incorrect assumption that ScriptableObjects are retained in memory between scenes. This is false, and has resulted in other bugs in VT.

Original Sin

Here's the simplest way to transfer runtime data (inventory, player stats, etc) between scenes: have a MonoBehaviour that stays with you when you load a new scene. This was one of the first things I wrote for Vapor Trails, and it's caused me a lot of grief ever since.

  1. I'm making the Main Menu and Tutorial levels. Each one needs the player with the GlobalController to work, so each scene has a GlobalController.
  2. I decide to test it, and start the Main Menu level and load the Tutorial level.
  3. The GlobalController contains the player, camera, and everything else that's common between levels, as well as a bunch of static references. There's only ever supposed to be one, but there are suddenly two, and it fights with itself like two clones insisting they're the real one.

Band-Aid time

Here's how I "fixed" it at first.

I save a static variable that's common between classes. On load, it checks if it's the "real one," and destroys itself if it's not. This would be fine, if nothing else ran on Awake.

Band-Aid Bad

Unfortunately, I had a bunch of other scripts that had Awake functions that were nested with the GlobalController. Options menu setting, player HP setting, enemy AI initialization, camera initialization, and more all happen there. Changing the script execution order doesn't help, because Destroy waits for the end of the frame to actually remove the GameObject (which will be after all the other scripts have run Awake).

There's also DestroyImmediate, but Unity docs recommend not using it.

So for the last few releases, what I'd do is carefully go through every scene that wasn't the first one, and disable the GlobalController in that scene. Unfortunately, I'm only human, and a very absent-minded one at that, so I'd often miss one level or forget to do this entirely before building and releasing the build. It was very embarrassing, but I couldn't figure out a better way...until now.

Time for Scriptable Objects

People love talking about ScriptableObjects, because they're useful, but nobody ever explained why to my satisfaction when I was starting out. Here's one reason they're good:

ScriptableObjects keep their data changes at runtime and don't care about scenes.

They live outside scenes. They're like Prefabs in that MonoBehaviours can reference them, but they can't reference specific things inside a scene. Unlike Prefabs, changes to variables in ScriptableObjects persist as long as the game is running. They revert to their default state when the game exits, so they're not an easy way to save game data to the disk.

However, you can do this very easily.

I'm storing the runtime data in a ScriptableObject. You can change it in one scene, and it'll stay changed for when something in another scene references it.

Note that this still doesn't cover saving and loading the data. In my real game code, the save data is contained in the ScriptableObject as a normal class marked with [System.Serializable]. That way I can change its values in the editor and save and load it to disk with a binary formatter.

Refactoring all the other systems to use this took quite a while, but I thought it was about time it happened.

Further reading on architecture with ScriptableObjects

Color theme: Github Sharp Dark
Screenshots: CodeSnap

Download Vapor Trails
Read comments (3)