Hey guys!
This post is about how to avoid Unity’s coding pitfalls you will inevitably fall into if you follow the official tutorials.
Don’t take me wrong. Unity is an exceptional tool for creating games, and I love most of what it does for you! Especially now that I know what I know, after 4 years of using it in making Vaporum, I wouldn’t want to change my precious little engine for anything! But that’s exactly the issue — you have no way of knowing in advance that you’ll get slapped plenty of times going the “Unity” way of doing things.
The Unity Way
So, Unity teaches you to create game objects, slap your components on them, have your components listen for the “magic methods” like Awake
, Start
, Update
, and the like, and the meadows are green and roses are red. Yeah, that surely works for the mini-projects their tutorials take place in. Issues start to pop out the moment your game grows in size and complexity though. Especially if you implement some kind of full-state save system with multiple ways of loading scenes (more on that later).
In classical game architectures, you write your main loop where you poll for input, update objects, components, systems, and render audiovisuals in the order of your choosing. In Unity, this is all set in stone and you have almost no control over the flow. It’s not clear and you can never be 100% sure of what gets called when and in what order. Unity gives you some degree of control via the script execution order tool, but even that cannot solve all use-cases. Not to mention that it’s weird from a coder’s standpoint.
Another issue is the performance. When your component is initialized for the first time, Unity checks if it contains a magic method, and if yes, it adds the component to an internal list of objects that need to be notified when certain engine events happen (Awake, Update, etc.). The problem is that these notifications are done via calls from C++ to C#, which is costly in itself, and the fact that Unity does a lot of safeguarding behind the scenes. In my opinion, that safeguarding should be your responsibility — you should make sure you are calling something that exists, that you don’t remove an object from an array you’re currently iterating over, that the object has ever been initialized, or whatever your specific game requires. You are making your own piece of software so you know what you’re doing. You should have full control.
And again, in small games with a handful of components, you will hardly ever notice any perf issues. In games with 120+ total components and a few hundred game objects in a scene, this can get noticeable.
Read this excellent official Unity blog post on the specifics! As you can see, they themselves recommend creating your own way of managing the game loop.
The Classical Way
What I’m going to recommend here may or may not fit your project, but this is what we learned the hard way when working on Vaporum. Since we realized problems piling up too late in the development, we couldn’t rework the whole game from ground up, but at least some changes towards the suggested system made it in.
There is no way you can properly implement your own main loop in Unity. You have to rely on the callbacks; they are the only entry points into anything you can do code-wise. So let’s work with that!
You should have a GameObject in every scene (let’s call it GameManager) with a single script on it, and you should make this object survive scene changes. This is one of the typical simple implementations (in Unity waters also known as singleton):
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
public void Start()
{
// If this instance is the first one...
if (GameManager.Instance == null)
{
// Set it as static instance.
GameManager.Instance = this;
// We need this GameObject to survive scene change.
DontDestroyOnLoad(gameObject);
// Initialize whatever you need here...
...
}
// If this is not the first instance ever...
else
{
// Destroy this GameObject.
Destroy(gameObject);
}
}
public void Update()
{
// Update your stuff...
}
}
The GameManager is THE ONLY class in your project that has the magic methods (with an exception, later on that), so it’s the only entry point from Unity into your game. This in itself is a HUGE boost already! Not only do you drastically cut the C++ to C# calls, you also gain full control over what gets called when. The Update
method becomes your main loop from which you rule your kingdom with absolute authority!
Sure, this puts the burden on your shoulders to come up with a sound architecture for adding, removing, sorting, and updating your game objects & components. As none of your components have any of the magic methods, you have to spawn, initialize, update, and destroy them yourself.
To be clear here, you still use GameObjects with your custom components on them, but you just don’t use the magic methods on those components. That’s the crux.
There are countless ways to implement this, but I’ll describe our own here in a few words, just to give you an idea. I’m sure your game will need a specific implementation anyway.
- When the game starts for the first time (
GameManager.Start()
), you scour the current scene for any GameObjects that have your own components on them, and add them to the system. - Have arrays and managing functions to add, remove, and update objects & components in these arrays. Use simple arrays, not lists or dictionaries. Arrays are fast!
- Use integer ids in your systems and components to quickly access other objects & components in this global array(s).
- Have your own functions to spawn and destroy objects that automatically properly handle addition / removal to and from the arrays.
- Have a way of defining the order of when components or component groups should update.
Even if there is a unique special case where normally all ComponentAs update before all ComponentBs, but one specific ComponentB needs to update even before all ComponentAs, you can do it with this system. You can do whatever your game needs. That’s the point.
Now, one of the very few exceptions where you may need some magic methods in classes other than GameManager is collision handling. Unless you write your own physics, you will need OnTrigger...
and OnCollision...
methods on specific objects to handle this. Still, this can be wrapped into a single generic collision-detection component that can communicate with GameManager directly, which in turn can decide on when your game logic will get to handle the collisions (right when Unity detects them, or later on in the main loop, or ignore them in some cases — your call).
Note on Singletons
Some game systems (PrefabManager, SoundManager, SaveManager, LocalizationManager, …) are just a bunch of structures and functions. It doesn’t make much sense for them to live inside a Unity scene on a GameObject. Yet, you can find heaps of tutorials that teach you just that — create a bunch of singleton classes like our GameManager, put them on a few GameObjects, and everything is dandy!
Yeah, but why… I see zero benefit in this approach. My recommendation is to simply implement these as static classes. Handling the GameObject & Component thing is completely unnecessary in this case. Waste of your time.
Full-State Save System Hell
Here are a few issues you’ll likely run into when implementing a full-state save system without any kind of main-loop manager. By “full-state”, I mean creating a save point that is exactly reconstructed upon load, byte by byte, so you end up with the exact same game state as when you pressed the save button. For games with checkpoints, this gets much easier.
Components (classes derived from Sorry, this is not true. I mixed it up with something else. The issue we faced is that GOs that are disabled in design time will not get their MonoBehaviour
) cannot have constructors. The arcane Awake
method is a sort-of replacement for that. So you mostly use that to initialize some other objects, states, and references. But there is a nice little caveat! If you load a scene with a disabled GameObject
, the Awake
methods on its components will never run! Not even when you enable the GameObject
. Which leaves your components uninitialized for good. So for those you need to be disabled on start, you must create your own initialization method anyway.Awake
called until you enable them. But because the Awake
is like a quasi-constructor, and you use it mostly to initialize references and other basic stuff, this leaves the object in a weird state. It exists, but its “constructor” has not been called. So it’s mostly unusable before you enable them, which you don’t always want, yada yada… Just something you need to be aware of.
The sneaky Start
method has another beautiful perk. It does not adhere to the script execution order. We found out the hard way, when things were breaking every other day. Because of this, we had to stitch together a Start manager that would be able to call the method on objects in our desired order, and also be able to tell if the method has ever been called in the lifetime of that object (persisting through save files).
When you load a saved game in Vaporum and you are in the same level as where you saved the game, we do not reload the whole level (Unity scene). In this case, we just match those objects that are both present in the save data and in the scene, and load data into them instead of removing everything and re-instantiating it again. This is to make loads very quick, and damn, it does speed things up! But, there is a problem, of course. The object’s Start
will not get called, obviously, and if there is something in there that need initialization, we got issues, Houston. So, again, your own layer of creation, removal, and initialization begs to exist here.
Objects that don’t exist in the scene, but do exist in the save data, or the other way around, have to be created or removed. But because you do the loading within an Update
callback, the Start
s don’t get called on those objects until the next frame. So we had to hack this too (also forcing them to be called in our desired order).
All this shabang exists because there are multiple concepts of “loading a scene”:
- You’re entering a scene for the first time ever.
- You’re entering a scene you’ve already entered before.
- You’re entering a scene for the first time in this session, by loading a save file.
- You’re entering a scene you’ve already entered before, but it’s the same scene as in the save file, by loading a save file.
- You’re entering a scene you’ve already entered before, and it’s a different scene from that in the save file, by loading a save file.
- …
Now, to be honest, some of the issues we ran into could have been mitigated by a smarter approach, or more research, or perhaps more testing, etc.
In any case, I’m not making another Unity game without a manager I have full control over!
Sorry, the comment form is closed at this time.