Goodbye MissingReferenceException! Hello, Automated Validation in Unity.
It’s late on a Friday. You’ve finally found the fix for that bug you’ve been looking at all week. Code. Commit. Push. You lean back into your chair and congratulate yourself on a job well done.
Two weeks later, you realize that your fix broke a poorly tested button callback. That parameter you added to the SendHelpEmail() method had unforeseen consequences. For days, customers have been pressing the “Get Help” button, only to have nothing happen in response.
The culprit here is serialization, the way Unity saves the raw data which forms the prefabs and scenes that make up the games we know and love. It’s far too easy for a developer to delete an old asset or change a simple method and accidentally break a flow somewhere in the project, especially for bigger and more complex projects and teams.
How bad can these mistakes be? Sure, a developer might catch it after a quick editor play-through. Or maybe QA finds the bug before release. But if the bug makes it out into the wild customers will be forced to live with a broken game for days until the new build is fixed, tested, and finally approved.
If you want to ship fast and often, there has to be a better way to prevent these mistakes from happening. How can we release products with confidence?
Enter Automated Validation
The heart of the problem lies in understanding how Unity serializes assets. For each asset inside a Unity project, there is a corresponding .meta file. In that .meta file, there is a value named
guid which is a globally unique identifier. So when Unity saves a reference to an asset like a script or a prefab, it‘s actually saving the guid of the asset’s .meta file. If the asset is renamed or moved, Unity looks for a .meta file with the same name in the same folder. If that .meta file isn’t found, Unity treats it as a new asset and all previous references are broken.
So what happens if a script reference is broken? The attached script no longer matches any guid, so the type is lost and the script instance can’t be created. Instead the inspector will show a “Missing Mono Script” notification.
But missing scripts aren’t the only thing that can break. A similar system is also used when serializing links to GameObjects or Components within a scene, except the ids are not globally unique, but unique within the scene. Whenever there is a serialized UnityEngine.Object field in the inspector, there is a corresponding drag-and-drop outlet that needs to be fill out. If a component is removed or a GameObject is deleted, the referencing fields will be broken. Since these fields are usually exposed by developers for a reason, when an outlet is broken or missing it should raise a red flag.
Lastly, there are serialized UnityEvents. They serialize both a UnityEngine.Object target and a method call, so not only can you break the target reference, but you can also break the event by changing the method.
Now we have a set of cases for the validator to catch. How do we dynamically validate the components and fields? To start off: we need to retrieve all the components. A quick breath-first-search of the hierarchy tree will visit all GameObjects, and a call to
GetComponents<Component>() will grab their components. For every component that is null, we have a missing MonoScript!
Now that we have all the components, we need to check that all their fields are filled out. We can use reflection to dynamically retrieve all the fields that are public or are marked with [SerializeField]. Reflection is a powerful tool that can give you information about types during runtime. Again, if any of these fields are null then they are missing or broken.
Finally, let’s validate all the serialized fields derived from UnityEvent. At first I tried checking that the method name exists, but then realized that if there are multiple methods with the same name the arguments would be needed as well. After looking through the UnityDecompiled repository, I discovered that there is more saved to the UnityEvent than is exposed in the public API. In the end, we can use reflection to expose internal functions Unity uses to validate the UnityEvent for our own use.
And that’s it! You can now build your own validator for all these different types of references in Unity. But I’ve also open-sourced my own version for those of you interested. It handles additional edge cases, allows for more complex customization on what to validate, and has an exhaustive set of unit tests to make sure it works for all cases.
Okay, how do I get started?
- Download the DTValidator project from the repository here. It should automatically download the latest state of the master branch.
- Place the downloaded folder in your project. I recommend placing it in the Assets/Plugins directory so it doesn’t add to your compile time.
Now that DTValidator is installed in your project, you can find the validation errors by opening the
DTValidator Window which is located under Window top bar menu.
Validate! button to find errors in your project. You can see where the error originated from and even highlight the object in the editor. Most likely most of the validation errors are outlets that don’t necessarily need to be filled. To have the validator ignore these fields, you can mark them with
[DTValidator.Optional]. See the FAQ for any additional questions you have on how to resolve errors.
Once you have no errors, you’re done!
However, it’s very unlikely that you’ll remember to run these tests manually, so I recommend running validation as an automated step in your build-pipeline. This is why the validator includes a set of unit tests to validate your project. You should set your build system to run unit tests for every build, with Unity Cloud Build it only takes one click!
Now you can easily find and check new validation errors when they come up, deciding whether they are actual errors (and fixing them) or new outlets that were introduced that are optional.
I hope that DTValidator helps you reduce the number of bugs that are shipped out to customers, play-testers, or whoever! If you encounter a bug or something that I didn’t think about, check the FAQ first, then file an issue if your question is not answered.