jojoleprowebsite

[Done] The Sauce of https://jojolepro.com
git clone https://git.jojolepro.com/jojoleprowebsite.git
Log | Files | Refs | README | LICENSE

commit dec6973a6af12bbcd4c09de72f650de69704fbeb
parent 821c1831665135235a8c3c992967257c7a4aefb3
Author: Joël Lupien (Jojolepro) <jojolepro@jojolepro.com>
Date:   Mon, 24 Aug 2020 12:41:59 -0400

Update formatting of last blog post.

Diffstat:
Msrc/blog/2020-08-20_event_chaining/index.txt | 219+++++++++++++++++++++++++++++++++----------------------------------------------
1 file changed, 90 insertions(+), 129 deletions(-)

diff --git a/src/blog/2020-08-20_event_chaining/index.txt b/src/blog/2020-08-20_event_chaining/index.txt @@ -8,45 +8,40 @@ 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. +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. +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). +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. +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. +audio source, etc.. are entities.We will not discuss much about entities +in this paper. Components -------------------------------------------------------------------------------- @@ -62,26 +57,22 @@ 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 +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). +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(). +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 ================================================================================ @@ -99,10 +90,9 @@ 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. +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 ================================================================================ @@ -111,41 +101,34 @@ 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. +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. +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. +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). +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. +the user interface unaware of the input system. - The Input System creates input Events (MouseClicked, KeyboardPress, ControllerPadLeft). -- A Driver (System) converts input Events into user interface Signals -according to -a configuration structure stored as a Resource and other contextual Resources +- A Driver (System) converts input Events into user interface Signals according +to a configuration structure stored as a Resource and other contextual Resources (are we inserting text? selecting user interface elements? dragging something?) - The User Interface System makes changes using a combination of - Signals (Press(x,y), InsertCharacter(char), SelectLeft) @@ -159,29 +142,25 @@ UiEvent::Clicked(label1_entity). 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. +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. +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: +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 -------------------------------------------------------------------------------- @@ -190,19 +169,16 @@ Asset Hot Reloading Watched files are configured through a resource, which could itself be loaded from disk and hot reloaded. - A FileLoader loads the file from disk (triggered by the signal) and converts -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). +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). - An AssetLoader picks up this signal and does some module specific action. For 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 +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 @@ -214,24 +190,20 @@ Audio Processing (including audio) and store them in a shared format. - We have a AudioPlayer Driver which will create Signals as time passes with -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}) +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}) - We have a AudioSink System that forwards the corresponding audio data into 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. +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. - UIDriver: Converts input Events into UI Signals. - UserDriver: Converts input Events to User-Defined Events. (KP_Escape -> @@ -243,21 +215,15 @@ FileUpdated(fun_file.jpg)) - WtfDriver: Converts input Events to ResolveWorldHunger Signals. (KP_P -> 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. +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 @@ -265,32 +231,27 @@ 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. +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. +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. +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