Unity Architecture Pattern: Structured Prefabs

Definitely not a Goomba

This article shows a powerful Unity pattern that I’m calling Structured prefabs. Structured prefabs are a pattern I use to simplify prefab initialization and reduce bugs. They help to indicate the intention of the creator to other developers and lead to overall better maintainability.

What is a structured prefab?

Like I mentioned, a structured prefab is a pattern I use to reduce bugs and increase maintainability for all my prefabs. The idea is to add a component at the root of every prefab to identify it, and give it structure. This component often has the same name as the prefab itself, but not always. Sometimes this component won’t contain any code, sometimes it’ll only contain serialized fields, and sometimes it’ll be a full-blown MonoBehaviour with behaviours.

So let’s explore all the benefits of this pattern, starting with references.

Prefab References

More often than not, I see developers reference prefabs with generic GameObject fields like this:

public class Game : MonoBehaviour {
    public GameObject playerPrefab;
}

There are two problems with this approach. First of all, as a developer working on the project, you’ve given me no indication of what you expect the player prefab to be. Does it require specific components to work? Most likely, yes. What developers should do instead is reference prefabs by a component, like this:

public class Game : MonoBehaviour {
    public Player playerPrefab;
}

When you do it that way, you know that at the very least, the player prefab needs a MonoBehaviour of type Player at its root. What’s more, if you do this, the Inspector won’t even let you assign a prefab that doesn’t have a Player component on its root. You can’t accidentally assign the wrong prefab (well, you can, but the wrong prefab will have a Player component on it as well, at least). By the way, when I talk about the root, I mean the GameObject at the top of the prefab’s hierarchy, rather than buried inside the prefab.

Now I’ll show how this subtle change massively improves your code’s reliability.

Prefab Instantiation

This next bit is my favourite part. When you instantiate a Prefab using a generic GameObject type, the function returns a GameObject. Usually, the next step is to use GetComponent to retrieve the components you care about like this:

public class GameManager : MonoBehaviour
{
	public GameObject playerPrefab;

	void Start()
	{
		GameObject playerGo = Instantiate(playerPrefab);
		Player player = playerGo.GetComponent<Player>();
	}
}

Instantiating prefabs this way is pretty dangerous because there’s no guarantee the Player component will be there, and then your whole game breaks. But, did you know that if you call Instantiate using a reference to a Player component, it’ll return the Player component instead:

public class GameManager : MonoBehaviour
{
	public Player playerPrefab;

	void Start()
	{
		Player player = Instantiate(playerPrefab);
	}
}

When you do it this way, you know with absolute certainty that you have a Player component (unless playerPrefab is null). By the way, if you use the main loop pattern from my previous article, and you avoid unnecessary null checks, you’ll know for sure that a null reference on initialization is due to not assigning something in the Inspector. Think of all the hours of your life you wasted chasing null reference errors. Now you can take that time back for the rest of your life. Spend that newly found time with your family and loved ones.

Now that we’ve solved prefab references and instantiation let’s cover the last piece of the puzzle: prefab initialization.

Prefab Initialization

After instantiating a prefab, you often need to initialize it before using it. You could wait for the Awake and Start methods to be called, but then you have to rely on a collection of loose initialization functions to run in an undefined order before your prefab is fully initialized. In my experience, a much simpler approach is to give your root component an initialization function that you call explicitly. For example:

public class GameManager : MonoBehaviour
{
	public Player playerPrefab;

	void Start()
	{
		Player player = Instantiate(playerPrefab);
		player.OnCreated(this);
	}
}

By the way, you might notice I passed this into my OnCreated function. That’s because I give my objects a reference to their parent object to simplify lifetime management. It’s the owner’s job to manage any GameObjects that it creates, so having a reference to your parent makes it easier to ask to be cleaned up and destroyed. If you don’t know what I’m talking about, I recommend reading my previous article on the main loop pattern to understand how I structure my projects.

Anyway, let’s get back to talking about initialization. We’ve already seen how we can remove GetComponent, Start and Awake, and make our code less error-prone, right? Well, we can take those same approaches and use them on our root component as well. In other words, rather than use GetComponent inside our Player MonoBehaviour, we can reference everything we need directly.

public class Player : MonoBehaviour
{
	GameManager _gameManager;
	public CharacterController characterController;
	public Animator animator;
	public Health health;
	//etc...
	
	public void OnCreated(GameManager gameManager)
	{
		_gameManager = gameManager;
		//call OnCreated on all the custom components...
		health.OnCreated(this);
	}
}

With that:

  • We probably don’t have to use GetComponent ever again. 
  • Any null references on initialization are due to something not being assigned in the Inspector. By the way, this is a reasonable error because it shows us we’re not using the object as intended, so don’t suppress it.
  • We don’t have to deal with Awake and Start methods that create spaghetti code.

So now we’ve seen all the benefits that the Structured Prefab pattern has, from improved references, instantiation and initialization, to cleaner code and greater maintainability.

If you liked this article, join my mailing list to be notified whenever a new post comes out. As a Mailing list subscriber, you could ask me to cover specific topics. So if you’d like me to write about something else, join my mailing list anyway and ask me.

10 thoughts on “Unity Architecture Pattern: Structured Prefabs

  1. David "G3oX"

    Nice article! I will start to approach this kind of architecture. Thanks so much for sharing it! 🙂

    1. bronson

      You’re welcome!

  2. Rob

    Hi! Definitely agree with using typed prefab variables, but it has one downside for me: if I do this (and I do), I can no longer assign values in the editor, because the object finder won’t show anything 🙁 Not sure if this is how unity works, or if I’ve just gotten myself into a bad state. If it’s the former, could you let me know? Thanks, and great articles btw!

    1. bronson

      You should be able to assign them in the Inspector. That’s one of the strengths: it forces you to only assign prefabs that have the desired component at the root. But you may have to drag the prefab onto the field, as opposed to using the little selector circle button. Let me know if that helps! Also thanks for the kind words! 😀

  3. Menawer

    Interesting approach, i never thought about the OnCreate way of initializing things, gonna try it out, thanks Bronson

    1. bronson

      My pleasure! I hope it helps!

  4. VISHWAH

    this method is Not SOLID ,Now the player prefab is depending on the GameManager,so if you want to test the player alone now you need GameManager in the scene to test.so it can cause lot of dependency issues.

    1. bronson

      I agree. I’ve created a hard dependency where I believe a hard dependency should exist (between the game and the player).

      Every design decision we make comes at a cost and as programmers it’s our responsibility to use the tool that best suits the particular job.

      This is a pattern I’ve used on nearly all my games. The pattern came about in response to a particular project that became very difficult, maintain and reason about. I’ve found that it creates code that is easier to reason about at the cost of hard dependencies. Of course, it may not work well in all circumstances.

      If the direct dependency between the game and the player is offensive, I could also suggest breaking up functionality by system, such as a player manager, enemy manager, UI manager, etc. This still creates dependencies between systems and their components, but the responsibilities are more clear. Additionally, this creates a structure that more closely emulates DOTS or other multithreaded code.

      I hope this helps!

  5. Jon

    Hey Bronson, thanks for the article!

    Somewhat related to the last comment, could you explain why you decided to make the player aware of the game manager (similarly, why health needs to be aware of the player) in this approach?

    Do you think it would be viable to have the GameManager & Player scripts listen for events from Player/Health respectively, rather than referencing them directly? Or does that reintroduce complexity that this approach is trying to avoid?

    Thanks!

    1. bronson

      The trouble is there are no approaches to avoid strictly. Talking to objects via events is a good approach. But it does introduce a bit of complexity as well. Let me give an example for illustration purposes.

      If you imagine that you printed the code to read it cold, that is, no tools, no IDE, you can’t run it; it’s just a piece of paper. In the explicit version, you still know what will occur and in what order. In the event-based version, it’ll take a bit more effort to piece it together. And if you hook events up via the Inspector, you can only guess what code will run. Another drawback is that debugging events is more of a pain than flat code. The advantage, though, is that you gain more flexibility when it comes to making changes. So there’s absolutely nothing wrong with that approach, but it’s your responsibility to decide if you need the added flexibility at the added cost of complexity.

      My general approach is to always start with the simplest version until I notice the scale tip in favour of a more flexible method. If you think about it, an inflexible solution also has less potential to introduce new problems. And, it’s just as important to prevent problems as it is to solve them. This point might be more important than my point about cold reading the code 😆. Plus, in my experience, it’s much easier to refactor a simple thing into a more complicated thing than it is to untangle a complicated thing into a simple thing.

      So, to sum up: if you think it’ll cause less harm than good, then go for it. And if you’re not sure, then don’t do it until you’re sure.

      Sorry, this is a pretty long-winded answer, but I wanted to get all my thoughts down for posterity 😅.

Leave A Comment