Unity Architecture Pattern: the Main loop

Main loop example

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 GameManager.cs, 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.

A Typical Unity Scene

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 Awake() and 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 Awake() and Start() methods, you can take complete control of your initialization by defining a single Start() method on your GameManager. The 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.

A Unity scene with a main loop

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 OnCreated() or 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.

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 Update() and 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.

6 thoughts on “Unity Architecture Pattern: the Main loop

  1. Artem

    Thank you for this post!
    It will be good to see some code examples if it is possible

    1. bronson

      Sure! I can set something up next week

      1. bronson

        I added an example project on GitHub and linked at the foot of the post.

  2. Josh

    How does one use Coroutines in a system like this? I guess one could write their own version and try to stop someone from using an engine coroutine? I also wonder if one is really being hurt on perf from too many separate Updates that perhaps it’s time to move to ECS.

    BTW for your example for UI animation and pausing other animations in the non gameloop version using Time.unscaledTime for UI will fix the issue.

    1. bronson

      As for Coroutines, it really depends what you’re using them for. It’s really important to design your solutions with the context in mind. In my case, for unimportant one-off animations and things I might still use a Coroutine. For most other situations I’d probably use an FSM like the one described in this post.

      As for Time.unscaledTime, that’s useful if you’re manually animating objects via c# scripts, but as far as I know it won’t work if you’re using physics, animations, animators or particle systems in the UI.

  3. Simon Jackson

    Actually, a perfect example of this pattern (and beyond) is the XRTK (THe Mixed Reality Toolkit) https://xrtk.io (as well as its sister project the Microsoft MRTK).

    These toolkits championed the pattern and proved out there was at least an 80% performance improvement in its adoption. Reducing the chaos of MonoBehaviours for service-led functionality. UI obviously still needs MonoBehaviour for placement and activation in a scene.

Leave A Comment