Better Resources
Better Resources is a library that offers a better API to interact with Unity3D resources. Most notably, it extends the existing Resources with the capability to query all resource paths in your project and imported packages, both at design-and-run-time!
Rationale
A while a go I was writing some code to extend the Unity3D editor at work. It dealt with ScripableObjects (SO) that combined numerous resources of different kinds into a single collection that could be used to instantiate assets at runtime. We had a couple of such SOs which are located at known locations in our Unity Package. However, there is no way for us to know if there would be custom versions of these SOs out there. Which leads to some awkward lookup table of some sort that needs to be injected for example. In the end we settled on only having to inject a single version of such SO into some public API. But the management of all of these objects is thus in the hands of the consumers of our code. Which is great for us :p
But this did get me thinking about why the Resources in Unity3D are not queriable? I mean, they are compiled into the build so why not add a simple query API for it. It has been this way for years upon years so surely people must have written plugins for this. And indeed, there are some plugins that support this, but they all do through drag-and-drop inspector magic management. Which I’m not a big fan of.
So in the end I decided to write quick little quality of life improvement to interact with the Unity3D Resources and allow queries at design-and-run-time with a focus on simplicity in both usage and code.
And Lastly; The name of this asset is a tribute to another great asset I think everyone working with Unity uses, or will use in the future: BetterStreamingAssets. If you don’t know BetterStreamingAssets, here you go:
DevLog
The number one feature I want to provide in this plugin is the option for querying resources of which-ever type at what-ever location. But before I got started on that I wanted implement the existing Resources API, like Resources.Load.
So BetterResources.Load simply delegates the call to Resources.Load and it’s like this for all built-in Resources functionality. I also added a lot of Unit-Tests around this in order to confirm my understanding of this API. I got some interesting results like being able to create resources with different types, yet identical names would lead to two unique assets. Which is weird since the file system should not allow this as the names are identical. But, the test passes so, yeah…
It also gave me the opportunity to setup a stable test base-class for generating resources and disposing them. I did really felt the need to test through the Resources API and thus also the file system. This lead me to manipulating the resources in the project during tests, which you can observe while the test are running. The Resources API is used in multiple stages of BetterResources and I feel like injecting some abstraction to deal with all that would have complected the code a lot. I tried to keep the code for this plugin as simple as possible and this approach made the tests a bit harder to write, but the production code remains really simple.
Resources Cache
Once I got all the built-in Resources functions covered with tests I started working on the actual feature I wanted to add; Queries. But first I needed some sort of data store. I cleverly combined the AssetDatabase API and Resources API to produce, which I call, the ResourcesCache. This is a simple flat list of Resource objects that list the name, path, package, components and GUID.
This list is then written to a JSON file and persisted in the Resources folder of the Unity project and can later be used to Initialize BetterResources. The code I had to write to generate the content of the cache file proved pretty simple in the end, you can find it here. It simply uses the AssetDatabase to get all asset paths and filters our all resources. Then, for each resource I need to load it into memory (yikes) and query all its top-level components because if you have some prefab in your resources folder with let’s say a Camera and an AudioListener, you can load it through the Resources with Resources.Load<Camera>(“MyAsset”); as well as Resources.Load<AudioListener>(“MyAsset”);. So I need to be able to call GetComponents() on the object.
Resources Manifest
The next step was to load this ResourcesCase file back into memory at runtime. My first thought was to get some SQLite database up and running since Queries need to be performant. But the longer I thought about that, the less interested in that solution I got haha. I would have to compile the latest version of SQLite for all available platforms that Unity supports which was way beyond my attention span for this little project.
In the end I simply settled on an in-memory ‘datastore’ aka a Dictionary :D. When, if ever, people cry out loud for an SQL based implementation just for performance reasons, I might dive into it. But for now, I’m settled on this.
So, the ResourcesCache is deserialized and loaded into memory into it’s domain-level variant which I call the ResourcesManifest. This is a slightly more processed version of the cache to support the kinds of queries I want to support. But in the end, it’s still a single, flat list of objects. Nothing special, really.
QueryBuilder
Since I decided I wasn’t going to use some SQL database as a datastore, yet I might be in the future, I wanted some future proof public API design to support many options. I choose a Builder Pattern/ Fluent API approach to construct queries. I think it ended up in a really nice spot in the end as well.
One reason I decided to go for the builder pattern is its high testablity. Its really, really easy to write unit tests against each filter since they should all work in isolation. Once you have them all tested, you’re done.
The internals of this QueryBuilder are also very, very simple. Each function simply adds a predicate function to a list and when a variant of GetResult is called, all predicates are combined and slung into a LINQ.Where query. That’s it! This was the simplest thing I could come up with and it works very well. It might not be the most performant though; but as I said before I might tackle performance issues once they arise or once many people ask for it.
Pre-Build Hook
Another requirement I wanted to tackle was some sort of pre-build hook to generate the ResourceCache juuuuust before a build is created. Well it wasn’t really a requirement but I did want to provide an example of how to do it since people will most likely ask for it anyway.
A comon ‘problem’ with pre-build actions in Unity3D is that they all need to be synchronous. And since most of the code related to cache generation in BetterResources is async, I needed to find some synchronous work-around. So instead of using the PackageManager API I simply parse the packages manifest.json file to get all the package names. I need these names to figure out wether assets belong to packages. Once I got this syncrhonous version done I could tie it to a pre-build mechanism and generate it.
GitHub Actions
The reader with a keen eye might also have noticed this is the first personal project I host on GitHub instead of GitLab. Since we’re using github at work and it’s more or less the standard anyway I thought it would be a good idea to switch to GitHub from now on.
What’s nice is the similarity between GitLab CI and GitHub actions. Most of it seems to be ripped off, in a good way :D. I really enjoyed the simplicity of GitLab CI and since its so similar I’ll just use GH Actions going forward.
I also added an action to generate the documentation using DoxyGen, and then deploy them to the GitHub pages url for this project.
Editor integration
Next I also wanted to add some kind of hook to automatically generate the ResourceCache and subsequently, initialize BetterResources. This will be very useful when other custom made Unity3D editor plugins should interact with BetterResources.
I found this thing called the AssetPostprocessor which does exactly what I need it to do. It simply lists all files that were imported, deleted or moved once the AssetDatabase is done. So I can implement this AssetPostProcessor and check the paths if they include resources. If so, a new cache is generated and BetterResources will be initialized. Pretty simple in the end.
I also added this [InitializeOnLoad] attribute to the class to make it generate a cache and initialize when the Editor starts up. This way, the user will always be able to use BetterResources straight out of the box when the editor is started. Pretty neat!