For the last couple of months, I’ve been struggling with a really obscure and elusive bug that has cost me quite a lot of hair-pulling. Today, I finally solved it. It turned out to be such a bizarre combination of technical issues, Unity engine specifics, and plain sloppiness on my part that I need to write it down just to vent.
The story starts in late November 2023 when I was two and a half years into the development of “Wright Files: Egerton’s Pickle”. At that point, I was kind of fed up with the project and decided that I needed a break. I wanted some quick and easy, low-effort diversion to let my brain cool off. By pure chance, I came across the Thinky Game Jam (TGJ) that was planned to start at the beginning of December 2023 and last for two weeks. It looked like a perfect opportunity to start a short and simple side project that would give me new energy to finish “Egerton’s Pickle” (obviously, this side project turned out to be neither short nor simple, but that’s a different story).
I started work by doing what solo indie game developers typically do in such cases: copying the whole project folder into a new location and reusing as much of the code and assets as possible. Now I need to get a little bit technical. As this is a Unity project, I’m using ScriptableObjects
extensively. ScriptableObject
is a type of asset that lives inside the project whether the game is running or not (including in the editor). This is really convenient as it makes it very easy to persist and pass various types of data between different scenes, levels, and in the editor itself. At that point in time, I had also been using the Unity Atoms library, which makes ScriptableObjects
even more awesome. Thanks to this setup, I was able to build the logic of the game (story, gameplay events, gameplay event reactions, etc.) out of reusable blocks that could be added, deleted, and recomposed without needing to touch the code. For example, I could configure a reaction for interacting with a specific NPC, and this reaction could consist of several specific actions, like adding an item to the player’s inventory, opening some door, or simply setting the value of a gameplay state flag. All these reactions were stored in a dedicated ScriptableObject
type that I’ll call story controller. For the TGJ side project I’ve created a separate story controller with separate gameplay event reactions and actions.
Now, Unity Atoms is a really advanced library, packed with tons of features. This is both good and bad. It’s good because it can do many things out of the box and supports a lot of use cases. It’s bad for the same reasons: it can do many things out of the box and supports a lot of use cases, which makes it cumbersome to extend or omit features that are not needed. Because of this, I had been mulling over the idea of creating my own library that would be simpler and less advanced than Unity Atoms, but at the same time would be leaner and more focused on the feature set I actively used. However, I didn’t want to start working on this so late in the development cycle of “Egerton’s Pickle”. Yet, after a week of the TGJ, it suddenly became more tempting to reconsider, as I now had a small and more focused playground project and adding new features based on Unity Atoms seemed to slow me down.
This is also when the TGJ project started to slip and eventually missed the deadline, because implementation of this new library (I called it Mites because, you know… atoms) took almost three weeks. With the deadline missed but the library coming along nicely, I decided to dig deeper into refactoring and started exchanging parts of the story controller based on Unity Atoms with similar elements (gameplay events, gameplay event reactions, etc.) based on Mites. I was really proud of myself because this new architecture made it possible to create much more advanced gameplay event reactions and actions with more elaborate conditions and whatnot. As a last step of refactoring, I changed the type of objects stored in the list of reactions in the story controller’s ScriptableObject
while retaining the name of the variable.
If you’re a programmer (especially a Unity game developer), you’re probably starting to see where this is going. Now I’ll get a little bit more technical again to explain: in Unity, all assets backed by code (MonoBehaviours, and ScriptableObjects
) exist in a kind of dual mode that enables persistence. The editor instantiates them in an editable/runtime form based on their definition from code but also stores them in files in a serializable format (also based on their definition from code). This usually works fine, but data corruption may occur when the underlying code definition is modified. In such cases, Unity’s deserialization mechanism will do its best to fix the corruption, but typically all it can do is clean up corrupted data and instantiate the object without the broken elements.
Data loss due to corruption might not become evident until much later, and this is exactly what happened in my case. Some time passed, some more refactorings were performed, and suddenly the story controller started throwing errors during gameplay. The errors seemed random and rare at first but became more frequent recently. Usually, once an error occurred, I was only able to make it go away by restarting the Unity editor. But since yesterday, it became permanent and repeatable every time, at the same moment in the game. The strangest part was that even though the TGJ story controller had ~30 gameplay event reactions attached, it reported exactly 74 null reactions attached every time the error occurred.
Actually, the only reason I was able to trace this error down was due to another technical aspect of ScriptableObjects
related to their lifecycle. ScriptableObjects
live outside of the currently active scene, so they can react to various events even if the game is not running. In the case of the story controller, this means that it starts to listen for gameplay events when it is enabled (i.e., when the game is launched in standalone mode or when the game project is loaded in the editor). After several long debugging sessions, it finally became evident that the error came not from the TGJ story controller but from the “Egerton’s Pickle” story controller copied over from the original project and lurking in some long-forgotten folder. And the actual reason why it was throwing errors was because I had changed the type of gameplay event reactions from Unity Atoms to Mites, and Unity was not able to correctly deserialize 74 reactions persisted in the story controller’s file, so it just instantiated a list full of 74 null references. And when I run the game in the editor and gameplay events started flying around, both story controllers started reacting to them but the one for “Egerton’s Pickle” tried to react with null reactions. How convenient. FML!
Good thing is that once the error was traced down, I was able to fix two issues – corrupted list of reactions in “Egerton’s Pickle” story controller and making sure that only one story controller reacts to gameplay events during runtime.
And the moral of the story would be: if you’re a game developer, be careful about what you copy over from previous projects and what testing and refactoring leftovers remain unattended in your current project. Copy-paste errors exist and can hurt you. Or even cost you $36 Million.
0 comments