This article is also available on Medium.
So far in this series, we’ve set the scene for several big features and used a little palette of tools that Unity and C# provide: the new UI system, raycasting, game object instantiation, resources loading, material switching, global variables and basic data encapsulation… There is however something fundamental in video games we haven’t yet talked about: events.
Overall, programs can follow various paradigms in terms of instructions execution, data layout, components hierarchy and communication, etc. In video games, it is very common to have an update loop that runs continuously (more or less every rendering frame) and checks with all the systems in the game if they need to do or return something. In Unity, this built-in structure is visible through the
Update() method that’s available in every
To interact with all those systems and react to user inputs, most video games rely on events. Today, we are going to implement a basic event system and use it to improve how we update the game resource texts in the UI when placing a building.
Why use events?
The funny thing about object-oriented programming (or OOP) is that, at its core, it wasn’t meant to be about objects primarily. Its creator, Alan Kay, had very different things in mind for it compared to what it’s become – most famously, he once said:
I invented the term object oriented, and I can tell you that C++ wasn’t what I had in mind.
More specifically, Kay’s background in biology lead him to design a way of programming based on autonomous entities communicating between them via messages. Objects would be individual cells in a big network, properly encapsulated with “local retention”, without all the porosity we usually see in OOP these days. Hence the idea of using events as a tool for this messaging and for keeping objects separate “organisms”.
A big plus when using an event system is that our emitters broadcast their events to all other components at the same time. For example, you could send a single event and have it caught by the UI system, the audio system, a data manager… Virtually all parts of your projects can react to an event and it’s very easy to add or remove those listeners.
All of this brings us to the essential reason event systems are so great: modularity. Whenever your project starts to grow in complexity, you run the risk of having all of your components too entangled together – you basically end up with one big mix of lots of little things that you can’t separate anymore. At that point, you don’t have a network of individual organisms but one big organism with a somewhat Frankenstein body. In OOP, this can happen particularly quickly: think of how easy it was to add a reference to our
UIManager in a script and call it to update some display on the screen. Although it sounds great at first, it’s actually a pattern that slowly drags your code down because you are tying all of your systems together and creating unnecessary dependencies.
The way an event system handles this issue is by relying on a “fire-and-forget” mechanism. Roughly speaking, this means that when an emitter sends an event, it doesn’t care about whether or not there are listeners to catch it, and if they do catch it properly. Its only job is to send the event. Then, it’s up to the listeners to be up and ready. The advantage of this philosophy is that everything is decoupled and therefore more robust and easy to unit test.
Say you want to check that your mechanics for updating the player health works well. In a naive setting, you’d probably have a reference in your player manager script to your UI system so that whenever the player’s HP are modified, the healthbar shown on the screen shrinks or grows accordingly. The direct consequence of this link is that if you create a new empty scene and bring in your player manager, you’ll have a “missing reference” for your UI manager. Therefore you’ll have to import this one as well. And chances are that this UI manager relies on your global game manager class; so you’ll have to import this class, too. And so on…
Thanks to events, you break the connection between the player manager and the UI system. Instead, your player manager will simply emit an event that can be caught by the UI system if it’s there, or ignored otherwise.
To kickstart this feature, we can draw inspiration from the official Unity’s live training on how to design a simple messaging system. The
EventManager they create throughout the session holds a C# Dictionary of events keyed by names (as strings) and it uses the Singleton pattern I mentioned in a previous tutorial. The
instance getter either returns the private
_eventManager field if it is already defined, or it first assigns/initialises it and then returns it. This is a way to have a unique version of an object that is initialised just once and then accessed as many times as necessary.
Note: this pattern is like many others brilliantly explained by Bob Nystrom in his “Programming Patterns” book – I really encourage you to take a look if you’re interested in code design and architecture, especially in the field of video games (but not only!).
Just adapting Unity’s training session gives us the following script:
To use this event system, you’d create scripts like these ones, and then put them on an object in your 3D scene:
However you might notice that those events can’t handle any parameter – they just broadcast an event name that can be caught by listeners to trigger a callback… but they don’t convey any data!
Even though we won’t need it today, it will quickly be necessary – so let’s add another Dictionary of custom events that can contain data. We’ll need to create a
CustomEvent class for this specific type of event and also a
CustomEventData class to easily store the data of our events. This class is used similarly to C unions: we define several fields but we use only one at a time.
This example allows us to create events sending data of the
BuildingData type with them.
We can now easily use it in our
Cutting the connection between the
Now that we have a custom event system, we’re ready to increase our modularity by removing some links between our scripts. In particular, we currently have a link to the
UIManager in the
BuildingPlacer class because last time we added some code to update the display of game resources in the UI when placing a building:
Even if both these scripts are on our “GAME” object and should be present together, it’s better to avoid such a harsh entanglement. Instead, we can remove this
_uiManager variable and emit events:
And then, we simply add the corresponding listeners in the
UIManager class – it’s a good opportunity to change the associated callback functions into private methods to improve our data encapsulation and prefix those callbacks with “On” (it’s a common convention when defining event callbacks):
Today, we prepared an essential feature for inter-scripts communication: a basic customisable event system. We also refactored some of our previous code to benefit from this new tool and improve the modularity of our scripts.
It might not look like much but in terms of game architecture, it is very important to try and separate as much as possible your components. This way, you avoid unnecessary dependencies and you isolate the components – this is particularly interesting when:
- you want to unit test your assets and scripts, especially because you don’t require the listener to exist: events work in a “fire-and-forget” paradigm where the emitter broadcasts its event but doesn’t need listeners to answer back; so if you’re concerned about one module in your unit test, and even if it fires events, you can only instantiate this part of your project and forget the rest
- you work with several developers and each work on a different part of the project: thanks to modularity, you can all take care of your piece of code and have well-prepared interfaces/communication systems
With that new mechanic in our toolbox, next week we’ll get back on track and implement another crucial feature in any RTS game: the ability to select units by dragging a box around them or clicking directly on one.