A Unity design pattern to handle global asset references

🤔 The problem

In Unity, you will often find yourself needing to reference an asset from your project in your behaviour (scene) code. For example, you might want to instantiate a prefab (most commonly), load a shader, or read some global config values stored in assets.

The usual approaches to do this can come with some problems:

1. Direct references in each component: Tedious

You can store a direct reference to the asset in your component with the inspector. This is safe and reliable, but it can mean a lot of a manual setup to link your assets when multiple components reuse the same assets.

For example, let's say you're making a simple game where the player receives coins when they open a chest, kill an enemy, or break a vase. The chest, enemy and vase components all implement their unique events and logic to spawn the coin prefab, but they all need the same coin prefab. Using direct references, you'll have to setup the reference to coinPrefab manually three times, even though there's only ever one single global coin prefab for your entire game:

class Chest : MonoBehaviour
{
	// Set up your reference a first time...
	public GameObject coinPrefab;

	void OnOpen()
	{
		Instantiate(coinPrefab);
	}
}

class Enemy : MonoBehaviour
{
	// ... set up the same reference again...
	public GameObject coinPrefab;

	void OnDie()
	{
		Instantiate(coinPrefab);
	}
}

class Vase : MonoBehaviour
{
	// ... and again.
	public GameObject coinPrefab;

	void OnBreak()
	{
		Instantiate(coinPrefab);
	}
}

2. Resources.Load / Shader.Find: Fragile

Another approach is to use Resources.Load() (or Shader.Find() for shaders) to load your asset by specifying its path relative to the Resources folder. Unlike direct references, this is appealing because it lets you avoid having to manually set up the reference multiple times.

However, this approach is not recommended for two reasons. First, it's likely to break: as soon as you move or rename your asset, then the loading will fail, which wouldn't have happened with direct references (because Unity is very good at keeping track of references by GUID instead of path). Second, it forces you to use a folder called Resources in your project, which may break your otherwise tidy folder structure.

For the same example, your code could look like this:

static class ResourcePaths
{
  // This will break if you rename or move "Coin.prefab" in your project!
	public const string coinPrefabPath = "Rewards/Coin";
}

// But at least you don't have to manually setup your references anymore
class Chest : MonoBehaviour
{
	void OnOpen()
	{
		Instantiate(Resources.Load(ResourcePaths.coinPrefabPath));
	}
}

class Enemy : MonoBehaviour
{
	void OnDie()
	{
		Instantiate(Resources.Load(ResourcePaths.coinPrefabPath));
	}
}

class Vase : MonoBehaviour
{
	void OnBreak()
	{
		Instantiate(Resources.Load(ResourcePaths.coinPrefabPath));
	}
}

💡 The solution

The Index pattern gets the best of both worlds by setting up a single resource, the index asset, that acts as a table-of-contents for all your other manual references. This way, you only have to set your manual references once, and they won't break if they are moved or renamed later on. Win-win.

Basic Use

In the code, the setup looks like this: we subclass BaseIndex and add our reference fields, which we'll then be able to access statically via Index.instance. In our coin example, it would look like this:

// CreateAssetMenu is what will let us create the index asset in the project:
[CreateAssetMenu(menuName = nameof(Index), fileName = nameof(Index))]
public sealed class Index : BaseIndex
{
  // The static instance is what allows us to get the index from anywhere in the code:
  private static Index _instance;
	public static Index instance => GetOrLoad(ref _instance);

	// Set up your references below! 
	// You only need to assign references once with this pattern.
	public GameObject coinPrefab;
}

// Then use it safely without it breaking on renames and moves:
class Chest : MonoBehaviour
{
	void OnOpen()
	{
		Instantiate(Index.instance.coinPrefab);
	}
}

class Enemy : MonoBehaviour
{
	void OnDie()
	{
		Instantiate(Index.instance.coinPrefab);
	}
}

class Vase : MonoBehaviour
{
	void OnBreak()
	{
		Instantiate(Index.instance.coinPrefab);
	}
}

Then, on the editor side of things, the index is an asset you'll create in your project using the project Create menu. You must place it under a folder called Resources/, and its name must be Index (or something else if you prefer).

  1. First, create your index asset by right-clicking an empty space in the project folder.

    <aside> 📁 Make sure you are inside a folder called Resources/!

    </aside>

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c99b12f0-7f5c-4512-b4a4-53d242229bbb/Untitled.png