ALEMIL

Welcome to a blog about application and game development

Event-driven programming

Event-driven programming 2019-02-04 16:22:50
Est. read time: 9 minutes, 22 seconds

Event-driven programming is a very popular concept that encourages writing reactive code that triggers on changes made through input or other events. When certain conditions are met, your component sends a message through the system. It is then picked up by one or many registered listeners, subscribers or observers as they are often called. A listener is just another component that is supposed to act when a specific event is triggered. Let's try to visualize this on a common example.

The problem of tight coupling


Let's examine a common example of a simple save game button to see how event-driven programming can help us. The button should persist our game state to the hard drive after the user clicks it. First, we need to know the bounds of the button - where it is on the screen and where the mouse cursor is. If the cursor is inside the Save button bounds and the user clicks left mouse button, we want to run the logic that persists the state of the game. Let's visualize this with simplified code.

Rectangle bounds = Rectangle(bottomLeft.x, bottomLeft.y, width, height);
Vector2D mousePosition = this->inputManager.getMousePosition();

if (Collision::pointInRectangle(mousePosition, bounds) && this->inputManager.isKeyPressed(KEY_LEFT_MOUSE)) {
// Logic that will persist game state to hard drive
}

We defined bounds of the button - a rectangle on the screen that is positioned at the specified coordinates of its bottom left corner. We pulled mouse position from the input manager and we checked if the left mouse button is pressed inside the bounds of the button. If conditions are met, we run the logic to save the game.

This solution doesn't use any events yet and it's working fine, we could leave it here and move to implement another feature. However, there is an issue. Button logic is tightly coupled with saving logic. This means that the above code will have to be changed whenever either button logic or persisting logic has to change. We will also have to copy the button logic for every button that we will implement. Ideally, we only need information that the button was clicked and save the game when it does. Button bounds and click checks can be done independently from the action that has to be taken and shared across all buttons.

Using events


Let's take our logic from above and wrap it in Button class. Instead of implementing our save game inside the class, let's send an event that describes what happened.

enum class ButtonEvent : int {
BUTTON_CLICK
}

class Button {
void function update() {
Rectangle bounds = Rectangle(bottomLeft.x, bottomLeft.y, width, height);
Vector2D mousePosition = this->inputManager.getMousePosition();

if (Collision::pointInRectangle(mousePosition, bounds) && this->inputManager.isKeyPressed(KEY_LEFT_MOUSE)) {
this->processEvent(ButtonEvent.BUTTON_CLICK)
}
}
}

Before we can implement what processEvent does, we should register a listener that will act when our click event occurs.
class Button {
...
// Map of functions assigned to specific ButtonEvent
std::map<int, std::vector<std::function<void()>>> listeners;

void function onClick(std::function<void()> callback) {
this->listeners[static_cast<int>(ButtonEvent.BUTTON_CLICK)].push_back(callback);
}
}
In our case, the listener will be persisting the game state, but at this point what will happen is independent of the button. The callback function can be doing anything.

The last thing we need is to process the click event.
class Button {
...
void function processEvent(ButtonEvent buttonEvent) {
for (auto& callback : this->listeners[static_cast<int>(buttonEvent)]) {
callback();
}
}
}

The above method will run every registered function for a given event. We can already see this is a great improvement over our initial implementation. Not only we can reuse this code to any button we will implement, but also register multiple callbacks for click event without writing everything in one place. This simple example is often already implemented for you, either by browsers or game engines, so the only thing you need to do is register a callback to different button events, but it serves as a simple real-world example of the event-driven programming.

How to use event-driven programming


Let's consider some other use cases where event-driven solutions can be a solid choice.

  • AI casts a spell on the player, we play an animation. During the animation play, at the moment of impact, we could send an event that the player took damage. The event can trigger independent listeners: play damage animation, check if the player died, play sound.

  • Send an event when the player enters a new map so independent listeners can act, even on different threads: clean current map, load / generate a new map, activate a cutscene, change the music, start a new quest. It would be easy to add another system without changing any of the existing code.

  • Exposing inner workings of the game to external sources. An example would be building a game in C++ and sending events to Lua binding so they can be used by mod creators. Then mods could act on exposed events and implement their own custom logic into the game.

Events don't have to be simple enums that we used in our button example. It's actually just an event type. We used it to identify what kind of action was performed. We could have more event types, like "mouse enter bounds" to highlight the button or "mouse exit bounds" to return to the regular texture.

Events can transport data. When damage is calculated after NPC is hit by a fireball, an event can transport the damage to other systems that will have to act, to reduce the health by the calculated amount or animate the number presenting it to the player.

Another interesting example of the event-driven programming paradigm was introduced to javascript ecosystem in the react.js library from Facebook, or specifically from related Flux architecture and later evolution - redux. The idea here is to build whole application based specifically on events. Every component that is responsible to draw something on the screen can send events that change the global state of the application based on player input or message from server. These components subscribe to the global state to decide how they render things on the screen.

This is an interesting use case for games too. Such a system would allow for reusability of existing application code in a replay mechanic for your game. You would require a seed for your random number generator if you use one and input from the players to simulate every state change in the game. Because the state is global, it's easy to add monitoring system to view how the global state of your game is affected after every action.

Another use case for event-driven programming in game development might be quite obvious. When creating a multiplayer game, events can be used to communicate between different machines, for example, sending an event with data to the server and receiving it back from the server on a different machine. Your game loop doesn't wait for the message back, but subscribing system will act when it is received. With that event, the client application can trigger multiple listeners to change the state of the game, which is being held and verified by an external server in this case, to prevent cheating.

This example is often referred to as async / await implementation, or promises. We can send an event to another thread that we want something done and continue with our game loop. We subscribe to an event that we expect to be sent when our message is finished processing and we trigger the screen change or popup. A real world example would be entering lobby in a multiplayer game when an event "found room" was sent from the server. The code that changes the screen to lobby would be our listener.

A few software architecture patterns are also based on events, like popular microservices or plugin architecture.

As you can see, applications for event-driven programming are quite vast and chances are you have already been using some of them. We now have a solid grasp on how events can be used. We could even create a whole event-driven applications with reusable components. Let's consider what are implications of this and when we should try to avoid implementing events.

Issues with event-driven programming


There are some issues with fully event-driven applications that get significant in size. It can get difficult to understand what is happening at any given time when multiple components keep sending many events, maybe even in parallel. It can get worse in real-time applications like games that don't wait for player input to change the state. In the case of react.js and redux, an external tool was pretty much required that displayed what kind of events are being sent to the system and how the state is being changed to analyze what is going on. In games that can change in milliseconds based on some internal calculations, like for example AI, following these changes during debugging can get too complicated. In some cases, you might need events to be processed in a specific order, which is something you have to consider during implementation to prevent bugs.

There are also some arguments against usage of Observer pattern that is a specific implementation of event-driven programming. There can be relevant to your use case, so if you want to know more, this paper provides an interesting view on the matter: Deprecating Observers.

Conclusion


Event-driven programming is widely used in software development and can be a great tool to solve tight coupling problems, but it does come at a tradeoff. It can get difficult to debug or understand the code that uses events too widely. I've come across many applications that sent events through the application, where it was difficult to determine what was listening for them. Because systems that send the event don't have to know what is listening, you as a developer might have a hard time following the flow of data.

Some of these issues can be mitigated nowadays when events are bound to enumerated types, as modern IDEs can help with finding every usage of these types, but it may still require to go through all of them to understand what is the outcome of a single click. That's much more complex than having everything in a single file. When designing the application, remember that decoupling the code through events allows for great reusability and modularity, but if you don't profit from it, then you may be left with just increased complexity.

On the other hand, communicating between different threads, applications or machines with events is a sound choice. Pushing an event at the moment of impact when casting spells or shooting bullets can also simplify some synchronization issues. GUI is a particularly great use case for event-driven programming as it's commonly detached from the main game.

Do you know of any other interesting use case for event-driven programming or Observer pattern? Maybe you have a horror story to tell? Share it in the comment section below! If you found this article interesting and want to be notified when another hits the blog, consider following me on Twitter where I often share news related to this blog and my current game development.



Comments +