Unity Architecture Pattern: the Main loop
This article will show the benefits of using a main loop as a foundational structure for your Unity games. The main loop is a powerful pattern that makes projects easier to debug and maintain and scales very well for projects of any size. It even has performance benefits.
What is a main loop?
Let me start by saying this isn’t a new concept. If you were making a game outside of Unity, you’d have a loop at the core of your game. By the way, Unity has one too, but you can’t see it. In that context, a main loop is nothing but a loop that runs at 60 frames per second, collects input, updates your game objects, and renders the game. The main loop pattern in this article is similar to that.
I’m suggesting a main MonoBehaviour (let’s call it
GameMain.cs or even just
Game.cs) responsible for updating all the other game objects in your scene. In other words, there’s only a single
Update() function. This might sound tedious, but in fact, it makes your code simpler. Additionally, the benefits are well worth any extra effort, in my opinion. Before exploring the advantages, let’s first look at the structure of a typical Unity scene.
A typical Unity scene
A typical scene consists of a loose collection of MonoBehaviours that are supposed to initialize and update independently. The goal is to have modular components that drop into a scene and “just work.” In reality, this adds complexity, reduces debuggability, increases maintenance costs, and frankly makes life more complicated than it has to be. With only a little bit of structure, you can avoid these issues. In my experience, it rarely works out how I imagined it would.
There’s no clear entry point into the initialization of a scene like the one described above. What I mean is, to understand how a given scene initializes, you’d have to find every script referenced in the scene and piece together all the
Start() functions. What’s more, the order of execution is either hidden in the Order of Execution settings or implicitly defined. This might be manageable (albeit annoying) for a handful of objects, but it scales poorly as your scene grows. Furthermore, because the initialization order isn’t defined, you end up with null checks all over the place. Some objects may even wait until the first
Update() frame to initialize fully. If any of this is familiar, then the good news is I have a solution for you.
Initialization single entry point
Having a single
MonoBehaviour that’s in charge of initializing your scene solves these problems. Rather than relying on a vague group of
Start() methods, you can take complete control of your initialization by defining a single
Start() method on your
GameManager initializes all other managers in the scene, which initializes all the objects in your scene. As a result, you’ll know for certain that this single
Start() method initializes everything in your scene.
What’s more, you can remove any null reference checks because you control the order of initialization. Your code will be so straightforward to read that even a junior developer who has never used Unity could understand it. Why is that? There are two reasons. First of all, you’re not relying on any behind-the-scenes magic except for a single
Start() function call. Second, if you open any
MonoBehaviour in your IDE, you’ll be able to track its initialization back to a single entry point. That is, you no longer have to hunt down everything
Start() method in the project. You follow your initialization method (I usually call it
OnStart()) back up the chain until you reach the point where it’s called. This approach is also great for onboarding new developers to the project. If this sounds appealing to you, then the good news is it gets better. You can take everything I said here and apply it to your
Update() function as well. That gives you a single entry point into your frame.
Update single entry point
Having a single entry point into your game’s update loop has all the same advantages as initialization. The update loop is simple to follow and understand. The order of execution is explicitly defined. It’s also easier to debug a frame by dropping a breakpoint in
Update() and stepping through. There are more advantages too, such as controlling which objects are updated. That is to say, pausing object execution becomes trivial. For example, if you open a menu, you may want to pause everything that’s running behind it. Some developers pause their game by setting the
timescale to 0, but you can’t animate your menus if you do this. You could collect all the objects in your scene that aren’t part of the menu and disable them, but I find this approach unreliable. With the main loop approach, you simply don’t call
Update() on any gameplay objects if a menu is open.
Another side effect of using this pattern is that you must keep runtime lists of your objects. As it turns out, this is useful in many cases. For example, if you need to save all the dynamic props in your game’s positions, you already have a list of them to iterate over. Or maybe you need to clear out all the projectiles before a cutscene starts. When it’s time to optimize your game you could decide to update a slice of the list on each frame, granting an instant performance boost. I’ve personally benefited from having runtime lists of my objects countless times. On that note, let’s get into performance.
Aside from all these architectural benefits, there are also performance benefits for using this pattern. As it turns out, there’s a hidden cost to calling
FixedUpdate() in Unity. And so, another happy side effect of the main loop pattern is you sidestep this problem entirely and automatically have improved performance. The increased performance is especially beneficial on mobile platforms where there’s a fixed thermal headroom. I mean that mobile devices can output a certain amount of energy per frame before the hardware throttles performance. On other hardware, optimizing anything other than bottlenecks won’t result in improved performance. However, all optimizations matter on mobile devices because any optimization can lower energy usage and free up energy for other systems.
I hope you find this pattern beneficial when structuring your projects. In my experience, using a main loop leads to simple, maintainable code that scales from small jam projects to large productions.
Check out the example project on GitHub here. If you liked this article, join my mailing list to be notified whenever a new post comes out. Mailing list subscribers also have the opportunity to ask me to cover specific topics. So if you didn’t like this article, join my mailing list anyway and ask me to cover something else.