#9 Lina Engine: Input System - Part 3
So far regarding the input system, we had talked about various ways to pass input handling around our systems. Using SDL to receive input either by polling events or reading states, and then we talked about transferring these events into other interested parties. If you have not read Part 1 and Part 2, it is advised to do so before continuing reading this post. In Part 2, we talked about the efficient and reliable ways of transferring events, using a pattern similar to Qt's Signals & Slots, also we mentioned it's drawbacks. So yes, we would use a pattern like that to handle high-level event transfers, but we still need something more robust and strong in the low-level classes that would reduce dependency injection mistakes and still be able to properly transfer events. So I designed a new event handling system, that is different than Signals & Slots pattern in two ways: It's more centralized, events are dispatched through a common dispatcher and dependencies are more clearly defined.It controls the event parameters before passing them. I want to emphasize the second feature a little bit more. If we are to use Signals & Slots, the parameter for the signal would be received by each listening slot. Consider this example; we have an event called KeyPress of type Keycode. Now, whenever a key is pressed, we would broadcast this event with the current value of the pressed key at this moment. And the listeners would receive this event, and then make the check of whether they are interested in that particular key on their own. This means that, if we have 100 objects that are only doing some stuff upon A key was pressed, they would still receive every single event broadcasted with every single key on the keyboard. Then of course, they would see whether the pressed key was A or not, then act upon it. Eventually, it's still a must that every single listener will receive that event. I would like to change this, I would like to carry this event type control into a more central structure, so that when Key F was pressed, the broadcaster a.k.a dispatcher would iterate through every single interested listener for the KeyPress, then check whether they are asking for the key F or not, and send the callback method to them only if they are interested. So let's take a look at my design to implement this kind of an event dispatching system. Design In order to create a seperation between Signals & Slots events and event system of this design, I will refer events as Actions. First we have an action dispatcher:
Currently as you can see I have a class called Lina_ActionHandlerBase, which is the base class for all actions. Since actions will be of type T (we can have actions for key presses, window initiations, mouse buttons, object destructions etc.), I basically wrote this base class as a wrapper. I hold raw pointer for them, firstly because I wanted to use polymorphism mainly, and also because these handlers will be created on the objects who wants to check for a particular action and will be passed to the dispatcher. Below is how it looks like for a dispatcher to dispatch an action:
Now the base action class:
So this is the wrapper base class, that uses no templates, so I can hold a list in the dispatcher with pointers that point to an object of the base class. We have an ActionType, which basically the identifier of the event, like keyPress, physicsStepCalled, editorRendered etc. GetData is a virtual method, it returns void* because the type is undefined yet. The return type will be of type T in the derived, actual Action class, which is:
As you can see, now we can create any action with the value of type T. My Action structure is like this, we have an identifier of the action(m_ActionType) and the value of the action. For example a window operation, "windowClosed" can be id, while the value can be "textureWindow", or it can be an action of a key press, id would be "keyPressed", while value can be "KEY_F" you get the idea. Now let's go to the real deal, the action handlers. As I have mentioned earlier, my dispatchers will hold a list of ActionHandlers, and these handlers can be any type T, so I have written a base wrapperclass for my handlers.
As i said earlier, what a dispatcher does is, when it receives an action, it iterates through the handlers, and asks them to control this event whether they want it or not. So my base class has a Control method, to be overriden in the derived handler classes.
I wanted to have handlers with diffent behaviour. For example, I could have a listener, who listens to key events, and doesn't care which key it is, it checks if the Action's identifier is "keyPress", if so, it just executes. Another handler may do the same, but also control which key was pressed. So here comes my concept of Condition. Some actions handlers may have conditions of type U, and what they will be doing is to check whether this type U is the same type with the action, then it would check whether the action has the same value as their condition value. So I have written another base class, which actually drives from the above ActionHandlerBase.
So, as you can see, I have a condition of type T. When the Control method is called, it uses this condition. ( Whether the Control of this class will be called or not will be determined by another subclass, which is our actual action handler class, which i will explain below, so dont worry about it now ) What control does is, it extracts the void* typed value from the action base, casts it to a pointer of type T. Then it uses is_same to check if they are the same type. As you can see in the if clause, if type check returns true, then we check for the value equality. I use comparison_trait struct to check the equality, which is this:
So if the action's value and the handlers condition is the same, our control succeeds and calls the execution method. What this gives is the flexibility of passing any kind of value to an action and action handler. I can pass ClassB, and as long as I have a comparison operator overloaded for ClassB, i can control how they are compared and everything would be fine.(in theory) So, if a control succeeds, then our execution is called. Let's look at the actual ActionHandler I will be using for handling stuff:
I wanted the action handlers to have different capabilities. They can check of a condition, or not. Completely seperate from conditions, their execution behaviours can be various. They can call a method, without any parameters upon execution, or they can call these methods with the parameter, received from the action (basically what the most of the event dispatching systems I have found does, they don't do any checks for anything else, they just send the action value to listeners.), or handlers can bind a particular value of type T, to the action value, for example a mouse motion event. You can pass a float to your handler, and the "mouseMotionX" action's value would be binded to your float, as soon as their types match and every object coupled are alive, you only have to bind once.
So remember, my dispatchers hold a list of ActionHandlerBase, but I will be creating objects of this ActionHandler class, and passing these objects to the list. So, since I use pointers in my dispatchers, the corresponding object methods, which will be ActionHandler class instances, would be called. Basically, whenever we call Control, it will be the Control of ActionHandler and it will execute from there according to the specified behaviour.
And to use the system, I would do:
So basically, this is the whole structure for now. As explained above, now I have a system that is able to produce reliable results, via multiple dispatchers that are not bound to any type. I can have a dispatcher for input actions, another for physics actions etc. Thus the only thing that my other systems would have to do is to register themselves to these dispatchers, they would not need to connect to the systems that casts the events directly.
In the next post, I will talk about creating subscriptions to these systems and an object handler that will deal with input event subscriptions for any systems that need to listen to actions. Until then, stay safe, wish you guys a good day!