Note: the ScriptableEvents package I'm using is available here, however this problem has to do with the Unity engine itself, and will affect any project using both Addressables and ScriptableObjects.
If you're like me, you probably read this article or watched this Unite talk on architecting your Unity project using ScriptableObjects, and got excited about how you might use the patterns in your own project. You also like the idea of using Addressables to control your game's memory footprint. However, as you develop, you will notice ScriptableObjects loaded via Addressables behave differently on a build than they do in the editor. This can be very discouraging, as it was for me. If you do not understand the nuance from the start, you might waste days of work before finally making a build and see that none of those ScriptableObjects you were so excited about are working!
Lesson learned: test your project on your target platform early and often!
- Here is a good Unity forum thread on exactly this issue that discusses a few different workarounds.
- Here's another one that explains the problem very clearly.
In this project I demonstrate how to use a single built in scene to load your game content, so as to avoid unexpected instances of a ScriptableObject. I feel this solution remains true to goal of ScriptableObjects (an alternative to Singletons, highly decoupled logic, obvious and designer-friendly location of the game's state and data) while retaining the usefulness of Addressables (finer control over memory, more flexibility with Scenes in relation to memory).
Running this project in Unity, you will notice spawning cubes via AssetReference will increment the count in the editor, but not on a build. Spawning via direct prefab reference will work regardless. The way I've set this up is the first scene is a mandatory built-in Loader scene which prompts the user to load the next scene, which is either addressable or a built-in. Built-in scenes will not work as expected, and this is not fixable. However, the addressable scene will work if and only if the ScriptableObjects in that scene are also addressable.
In the built-in scene, two instances of the ScriptableObject exist: one in the scene itself and the other with the assets loaded via Addressables. This can be verified by comparing the values of Object.GetInstanceID. In the addressable scene, because the scene, the prefabs, and the ScriptableObject are loaded along side each other, there is just one instance of the ScriptableObject and all references point to that instance!
Recording.2023-05-30.222809.mp4
screen-20230530-215349.2.mp4
The red counter references a ScriptableObject that is marked as addressable, the orange counter does not. The orange counter and the addressable prefab have separate instances of the ScriptableObject! The red counter's setup therefore demonstrates a viable workaround for using ScriptableObjects and Addressables.
screen-20230531-215650.2.mp4
The best practice when working with Addressables is to ensure that your project has exactly one built-in scene, and that scene's only job is to load (via Addressables!) the next scene that actually contains your games content. This ensures you are always working with the expected instances of your ScriptableObject assets.
While developing in the editor, it is not as important to be strict on how your assets and scenes are loaded and in what order, since the editor will always reference the same instance of your ScriptableObjects. This is good news for developers working on teams, since the typical Unity workflow stays exactly the same while editing scenes and prefabs. However at build time, it is a requirement that the game loads through the initial built-in scene. Hopefully, these requirements are flexible enough for large, professional projects.