A very neat feature of the Unity game engine is the concept of Resources. For the uninitiated, I will try to give a quick overview of how Resources work.
Every asset placed inside a folder named "Resources" can be accessed in the editor and in your game build via its path. You can create multiple folder called "Resources" in your assets and you can create subfolders within. However, the path via which you can load your asset is always relative to its nearest Resources parent folder.
Let's look at an example.
Recruiting some Orcs
A pretty common way of structuring a Unity project is creating a central Resources folder directly in the Assets folder (which is the root of your project as far as Unity's Project window is concerned). If we take this approach, we may want to create subfolders within to organize our Resources.
For our example, let's say we have created a prefab for an enemy named Orc. Your asset database may see its path like this: "Assets/Resources/Enemies/Orc.prefab".
To load the GameObject using our Resources class, we may use:
GameObject orcPrefab = Resources.Load("Enemies/Orc");
Please note that the path expected by the Resources.Load function does not include its filename extension, in this case ".prefab". For this reason, it is not safe to have multiple assets with the same filename at the same relative resource path.
It is also worth noting, that we are using the generic variant of the Resource.Load function. Similar to the Instantiate function, this can also be used to get direct access to the Components of a GameObject. In our case, let's say the Orc has a MonoBehaviour component called EnemyLogic. To get said component immediately when loading our Orc prefab, we could modify our example like this:
EnemyLogic orcPrefab = Resources.Load("Enemies/Orc");
Keep in mind that we are still only referring to the asset, in this case our prefab. To create Orcs in our game scene, we would still have to instantiate them.
It's not called "Orc Wars"!
Enough about Orcs (for now). In a real world game project there are all kinds of assets you may want to access from your scripts: ScriptableObjects, Materials, Textures, AudioClips and so on. Anything that inherits from UnityEngine.Object is fair game.
The neat thing about the Resources approach is that it requires zero setup in your code. You don't need to create any serialized fields to drag your assets into. If an asset is in your Resources, you can basically get it from anywhere in your code base, even in editor code.
Sometimes, this can be a memory safer. Say you have a GameObject that will randomly spawn an item, 1 from a variety of 20 or so options. Sadly, we only know the item we actually need at runtime. Using Resources, instead of referencing 20 different prefabs that we may instantiate, we can decide which item to spawn first and then proceeed to load exactly the prefab we need.
For loading large assets without slowdown, there is even the option of using Resources.LoadAsync.
Got it, now where's the catch?
Right here: Accessing assets via their path is not very safe. One little typo and you're in for a NullReferenceException. Not from Resources.Load itself mind you, which will simply return null in case an asset cannot be found. But your code will probably expect the asset to exist, otherwise you wouldn't try to load it, right?
It is not uncommon for assets to get moved or renamed once in a while to keep the project structure tidy. If you do this to any of your Resources, you better remember to update all the scripts that depend on their path. Try explaining that to your designers.
Eh, whatcha gonna do 'bout it?
We can't fully eliminate the problem, our paths have to be stored somewhere. However, we can do quite a lot to minimize the room for errors and reduce our workload.
A straightforward approach to manage our Resources would be to centralise the way we store our paths.
Back to our Orc example, let's take a look at what our helper class could look like:
Now everytime we want to get our Orc prefab, we can simply write:
EnemyLogic orcPrefab = Resources.Load(ResourcePaths.Orc)
What if we remove or rename our assets now? No biggy! If we are consistent in using our little helper class, it is the only place where we ever have to make any changes. That is a lot less work than going through all scripts that use Orcs.
Why stop there? We can easily take this one step further by using some cute Getter Properties and moving the Resources.Load calls into our helper class. Let's also assume our class will grow a little and we want to keep things nice and tidy. We can improve our structure by using Nested Types.
Here is what the result looks like:
This makes accessing our Resources a breeze without introducing any additional overhead or maintenance work (compared to our previous approach). Once again, to get our now infamous Orc, we would now write:
EnemyLogic orcPrefab = ResourceLoader.Enemies.Orc;
So this is a pretty great setup already. We can take this and run with it.
However, if we want to reduce the amount of code we have to write and manage by hand, we could entertain the idea of creating a GUI based solution...
Introducing black magic
For Orb Wars we have created an EditorWindow into which we can drag and drop our assets and which will write our helper class for us. For prefabs, we can even select the component type we want to use (or use GameObject).
Our ResourceMapperGenerator will also prevent duplicate assets, automatically update the paths of assets that were moved or renamed and warn us about assets or types that no longer exist in our project.
Here is a short example of the ResourceMapperGenerator in action:
Those are a lot of Menu prefabs!
The generated C# code is pretty simple and identical in structure to the ResourceLoader class we created for our example.
Let's take a look at what the beginning of the generated class looks like:
And that is all for today.
Tune in for part 2 of our Resource shenanigans to find out about other ways in which you can use Resources and how we use them to keep our network traffic low.