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.
15 thoughts on “Unity Architecture Pattern: the Main loop”
Thank you for this post!
It will be good to see some code examples if it is possible
Sure! I can set something up next week
I added an example project on GitHub and linked at the foot of the post.
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.
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.
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.
The pattern is really useful for most of what you are describing, but actually the timescale thing can be fixed for animations & particles as well. They can be switched over to unscaled time for exactly that ☺️
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.
very understandable article
Thank you! I hope it helps
Excellent content as usual! Thanks for sharing your knowledge dude!
Thank you so much for saying so! 😀
Great content! It’s interesting there is a performance boost with this pattern. I was playing around with a similar idea but assumed the manual iteration and initialization would be worse! I guess all magic comes with a cost. Do you see any issue with having a GameObject (say IManagedMono) register itself on OnEnable/Awake to a ScriptableObject “List” or “System” which can be injected into the GameManager, thereby removing the hard link between the two? Basically, there shouldn’t be any performance hit by adding the intermediate (or multiple, mono->list->system->list->gamemanager) because it’s just passing references right? I guess maybe a negligible bit because of the multiple iteration invocations, but it should still be O(n). Am I thinking about that right? Also, If you. wanted to do a post on scene management architecture (ideally adaptive) I wouldn’t be mad… 😉
To be honest I can’t say for certain without testing. However with an indirect lookup like that you may lose performance from a lack of cache coherence.
Thanks for the idea! I’ll definitely think about a post on scene management architecture! 😀
Just a month in my own project, caught this just in time ! I’ll definitely try it as I’m already wasting precious time rummaging through the Start() and Update() of a dozen classes.
Top quality as usual, Bronson 🙂
Thanks so much! I’m glad it was helpful! 😀