I can describe what I did in some detail. I’m not suggesting that my method is best, but its what I did and it worked for our project. I thought about writing a blog post about my adventures with saveload. This’ll be more like a stream of consciousness. And before you charge ahead with anything that I write, see the limitations that I’ll write about at the bottom.
Unique IDs
Every object that is important is given a component which stores a System.Guid. The guid is assigned to each object in the editor when that object is placed in a level by designers. Uniqueness is assured by some editor scripts that know how to scan unloaded scenes and gather a list of Guids in those scenes. This was accomplished by reading the .unity3d scene file in the editor. This editor feature was a time sink, not only to implement but to maintain. It took about a week to implement and then whenever unity changed their scene’s yaml format, the scene parsing broke, which was another several hours of a developer’s time.
In a personal project, I’m considering using scriptable object assets in the project to store the unique ids. It seems promising, but I haven’t gone far enough to feel confident that it is a solution. The gist is that every time a UniqueID component requests a new unique guid, an asset is created and tucked away in a project folder to store that guid. My unknown concern is the runtime overhead of loading and storing a scriptable object that is nothing more than a guid. But this does make cross-scene uniqueness easier to track. Any time I need to generate a new id, I can compare it against all of the assets in the tucked-away project folder before validating it. At runtime, I load them all in to memory as part of the application initialization, so that unique ids created for runtime objects can check against the list. This is probably overkill, but it ensures uniqueness.
Depending on how you store your saved data, this may not be necessary. If you store your saved data by scene, then the scene becomes part of the unique id for any object that doesn’t cross scene boundaries. So guid collisions (which are exceedingly rare, btw) won’t matter.
Initialization
I took Awakes and Starts out of the equation entirely. I have a SceneLoad component which exists in each scene (and it must exist in the scene, not as a global). This component is always at the top of the Script Execution Order list. Its sole job is so that, on Awake(), it creates a gameobject, disables that game object, then reparents every root object in the newly-loaded scene to that disabled gameobject. Thus we prevent any Unity methods from being called on scene objects, except for OnLevelWasLoaded(), which I just don’t use at all. One important detail is that the SceneLoad component has to filter out some objects - the UI camera, for instance, since we don’t want the camera that is displaying the Loading image to suddenly get disabled!
Alternatively, you could structure your scenes to have a single Scene Root gameobject, and then disable that object on SceneLoad’s awake. I created the disabled gameobject programmatically because it was easier than convincing our area designers to make a change to a 100 or so scenes.
Now I have a loaded scene, which I can poke at and manipulate, without any Awakes or Starts or OnEnables having been run, because everything is disabled. At this point, you can take your saved data surrogates, write their fields to the monobehaviours of your loaded scene at your leisure. As a bonus, you can take as many frames to do this as you need, so you won’t run afoul of console frame time limits! Huzzah!
For your example with the golden idol and the bag of sand, you could disable the idol and enable the sand at this point.
Once the saved data is loaded, I enable that root game object, which causes the scene to initialize just as it would have normally.
Saved Data Format
We used Json.NET. Unity’s JSON implementation wasn’t available at the time. Json.NET’s converters were very useful. I wrote a GameObjectConverter that wrote out each of its monobehaviors surrogates to a list, and serialized that list. Although, for a first-pass attempt at saving, you could forgo the use of a full serializer. Write your own surrogate classes and teach your custom monobehaviors how to write their data to their surrogates.
My scene saving routine went like this
- Get all the objects in the scene with a UniqueID component, including inactive objects.
- Ask each object to serialize its monobehaviors’ data.
- Write that data to disk.
#1 involved some trickery. I get a list of all objects with UniqueID using Resources.FindObjectsOfTypeAll<>(), then I filter the list to remove prefabs which are resident in memory (explained below).
Limitations
The limitations that I imposed were that GameObjects were saved independently. I didn’t save parent-child relationships. If a child GameObject had a component that needed to be saved, then that child had its own UniqueID component, and needed to somehow know to reparent itself appropriately on initialization.
The other big limitation is that all prefabs had to saved enabled. A disabled prefab was verboten. The reason for the second limitation is due to the way that I went about detecting prefabs in memory at runtime in builds. In the editor, prefabs are easy to detect. At runtime, they’re trickier. Basically, given an object, if its transform.root.gameObject is activeSelf, but not activeInHierarchy, then its a prefab. However, if the transform.root.gameObject is not activeSelf and not activeInHierarchy, I don’t know anything. Hence, let’s just force all prefabs to be activeSelf - removing any ambiguity.