avatarMina Pêcheux

Summary

This text discusses the implementation of an event system in Unity/C# for an RTS game project, emphasizing the importance of modularity and decoupling components for better code design and architecture.

Abstract

The text is an interlude in a series of tutorials for creating an RTS game in Unity/C#. It focuses on introducing an event system to improve the game's architecture and modularity. The author explains the concept of events in video games, their advantages, and the reasons for using them, such as broadcasting messages to all components and decoupling systems. The tutorial demonstrates how to create an EventManager class using the Singleton pattern and a custom event system that can handle data. The author then refactors previous code to remove dependencies between scripts and improve data encapsulation. The tutorial concludes by emphasizing the importance of modularity and separation of components for better unit testing and collaboration among developers.

Opinions

  • The author believes that object-oriented programming (OOP) should be based on autonomous entities communicating via messages, inspired by Alan Kay's original vision.
  • The author argues that events are essential for video games to interact with all systems and react to user inputs.
  • The author highlights the importance of modularity in code design, as it prevents components from becoming entangled and difficult to separate.
  • The author emphasizes the benefits of using events for decoupling components and creating a "fire-and-forget" mechanism, which makes code more robust and easy to unit test.
  • The author suggests that using events can help break connections between components, such as the player manager and UI system, making them more independent and easier to manage.
  • The author recommends using the Singleton pattern for the EventManager class, as it ensures a unique version of the object that is initialized only once and accessed as needed.
  • The author encourages developers to separate components as much as possible to avoid unnecessary dependencies and improve the overall architecture of their projects.

CODEX

RTS Interlude #1: Introducing an event system (Unity/C#)

On with our RTS project — in this interlude, we’re going to talk about events!

⬅️ Tutorial #3: Setting up in-game resources | TOC | Tutorial #4: Selecting units ➡️

📕 Get the ebook and bonus material on Gumroad! 🚀 Find the code of this tutorial series on my Github!

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 MonoBehavior-derived class.

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.

Creating the EventManager class

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 EventManager:

Cutting the connection between the BuildingPlacer and UIManager

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):

Conclusion

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.

⬅️ Tutorial #3: Setting up in-game resources | TOC | Tutorial #4: Selecting units ➡️

📕 Get the ebook and bonus material on Gumroad! 🚀 Find the code of this tutorial series on my Github!

If you liked this article, you can find more blog posts about tech, AI and programming on my website :)

Games
Development
Unity
Csharp
Tutorial
Recommended from ReadMedium