💾 Archived View for jojolepro.com › blog › 2020-08-20_event_chaining › index.gmi captured on 2023-06-16 at 16:14:24. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

Jojolepro

Blog

Projects

Music

Quotes

GitHub

Archives

Event Chaining as a Decoupling Method in Entity-Component-System

Joël Lupien (Jojolepro, jojolepro@jojolepro.com)

=>https://patreon.com/jojolepro

Context

In game engines, we often have a lot of dependencies between modules.

For example, the user interface often depends on the renderer, window and

input systems.This means if we are not careful, we will end up with a

user interface that works only with a single type of input.When you add a

new input method (a controller for example), then you have to also edit the

user interface functionality to be able to use this new input device. What

is surprising however, is that not only do we need to change how input works,

but also we are modifying user interface code to add a device that... probably

does the same thing we are already doing.

This situation isn't unique to user interfaces. In fact, this is very often

how dependencies are modelled.It is intuitively what we think of when we

think of logical dependencies. A depends on B, thus A refers to B.As we

have just seen however, it can cause problems of maintainability, because

we have strong coupling.

Reducing Coupling

There are multiple ways to reduce coupling, depending on which paradigm

you use.Here, I will be specifically exposing a way to reduce coupling in

the context of an Entity Component System (ECS).

Fundamentals

First of all, let's see the building pieces that we have in a ECS.I will

be using terminology from the Amethyst Game Engine, since this is what I am

most familiar with.

Entities

Entities are "units" of the game. Each player, item, user interface element,

audio source, etc.. are entities.We will not discuss much about entities

in this paper.

Components

Components are properties of entities. Again, we will not be discussing those.

Resources

Resources are simple data that is stored inside of the ECS' context.

Systems

Systems are what drives the changes in the game. They are functions that

take data from the ECS context (resources and entity/components), perform

some computation and finally modify the ECS context.Systems can depend on

each other to sequentially perform computation. If they don't explicitly

depend on each other, they will be ordered automatically (and in parallel

when possible) in a more or less optimal ordering depending on how they use

resources or components (whether they read or write to them).If a system

writes to a resource or component, no other system can access this same

resource or component at the same time (in parallel).

Event Channels

Single writer/Multiple readers FIFO queues. Most data types can be inserted

through those (they only need to be thread safe).You need a registered

instance of ReaderId to read from them. ReaderId instances can be created by

getting a mutable reference to an Event Channel and calling register_reader().

Additional Terminology

Let's introduce distinct names depending on how those previous concepts are

used to improve the clarity of this paper.

Driver

A System used to "drive" the execution of another system, often through the

use of Signals.

Signal

A Signal is simply an event written in an Event Channel that has for only

purpose to drive the execution of one or multiple Systems. If we look at it

the opposite way, some System will look for Signals as a way to know if and

what kind of work they need to do.

Event Chaining

What Are They?

Very similar to the concept of message passing, event chains are a way to

communicate between Systems.Simply put, you have one event that is created,

which create a second event, which create a third event, which is then consumed

(received) by a System.

Compared to Message Passing.

In traditional message passing, you often have some System A that sends a

message to System B.We say that System A is "aware" of System B's existence.

A way to decouple this is through the use of some method of broadcasting

the message to anyone who is registered to receive it. This is how mailing

lists work.

Here however, we don't have a list of who should receive each message. Instead,

each System that wants to receive the event has a ReaderId which it can use

to read from the Event Channel. The reason for this is so that we don't have

a single place where all of the message buffers are and where every system

tries to get a mutable reference (which would heavily reduce performance).

How They Solve The Coupling Problem

If we continue with the example from the beginning, we can see a way to make

the user interface unaware of the input system.

ControllerPadLeft).

to a configuration structure stored as a Resource and other contextual Resources

(are we inserting text? selecting user interface elements? dragging something?)

UiEvent::Clicked(label1_entity).

./graph1.png

Drawbacks

Of course, we have to talk about drawbacks. Let me preface this with

the following: There is a performance cost. It is small, but it is there.

If we take the last example, we would now have one more System running (the

UI Driver) and we would have to create the Signals for the User Interface

System. In addition, in almost all cases of a Driver being present, there

is a conversion step when converting events to signals, usually a HashMap

lookup or similar.

However, we surprisingly are also getting some performance gains from this. In

the last example, instead of the User Interface System looking each frame

for the status of the input device(s), we now have this System running only

when signals are present.

Other usages

We have seen how this concept of Event Chaining allows to decouple user

interfaces from input handling.Now, what else can we apply it to?Well... a

lot of things actually. Here are some examples:

Asset Hot Reloading

Watched files are configured through a resource, which could itself

be loaded from disk and hot reloaded.

the data into a shared format. (RGBA for images, vector data for svg, vertices

for meshes, etc) A Signal is then sent (DataLoaded, DataUpdated, DataDestroyed).

example, loading or updating

a mesh into the GPU memory.

In addition to simplifying asset reloading by dividing it in steps, it now

also creates a way to notify system of data changes (a modified mesh) so that

they can update their internal states (GPU memory).This gives more power

to users, as they can now change data midway through without having to modify

the code of what depends on this data (the renderer's mesh to gpu loader).

./graph2.png

Audio Processing

(including audio) and

store them in a shared format.

audio data to play. For performance reasons, we can use references or indexes

to the audio data instead of copying the data into the Signal.

(AudioPlay{audio_data, from_point, speed})

a raw audio sink.

Additional Benefit: Configurability

In this paper, I mentioned that Drivers can use configuration and context data

from ECS Resources.This allows for a super easy way to configure complex

behaviors. Let's take input as an example. The Input System creates raw

events. The Driver converts those into signals (or events) for other systems

to use. Let's see how we can use multiple Drivers to create complex behaviors.

UserEvent::PauseGame)

Signals. (KP_Left -> Move::Left)

FileUpdated(fun_file.jpg))

Please::SolveIt)

As you can see, lots of Drivers are possible.Now we have two choices. Either

the Drivers decide based on the context if they should create the signals

OR they always create the signals according to their configuration.In the

second case, the configurations can be changed by other Systems or by external

code, depending on your preference.Personally, I find the second option

more versatile, as it is rare that the Drivers can be made context aware in

a way that fits all use cases.In this case, it is possible to have code

that, for example, disables the UiDriver when the player is controlling

their character, saving both performance and code complexity.

./graph3.png

Conclusion

In conclusion, Event Chains are a powerful and versatile tool to decouple

logical dependencies between Systems. They add a bit of complexity and

performance overhead, but they are well worth their cost in the context of

a general game engine.

General Recommendations

For those who already worked with something similar to EventChannel and

ReaderId, you might have noticed that there are issues when a System that

consume Signals is paused. When this happens, the System stops consuming

the Signals and they stay in memory forever, causing memory leaks. The

solution for this is to store the System's ReaderId inside of a Resource

and to nullify/destroy it when the System gets paused.

Testing! You can test Systems in isolation by manually sending Signals.

Take advantage of this and test <3

Use generics! Drivers almost always do the same job: Convert one event type

into another event (or signal) type by using a table or HashMap.This means

that using generics to specify the input and output types as well as the

HashMap key and value types, creating Drivers can be done in a single line.

Document as much as possible the Systems. Since we have System and Driver

which are both Systems and Event and Signal which are both Events, it can

be quite confusing without the proper documentation to understand the role

of each piece of the puzzle.

Here are some ideas of what to document:

Supporting Me

I released this for free/without limitations because I want to contribute

to the greater good.

If you like the work I do, please consider donating on Patreon:

https://patreon.com/jojolepro

Or by Bitcoin:

15NDruDUDr3KaMjt87BvUJaayEzy5c765Z

(CC0) Joël Lupien 2020-2022

View page source