Illusion Engine 04 - Event System
Introduction
Games are inherently event-driven. Events are things that happen during the game and you want to pay attention to, such as explosions, players being seen by enemies, picking up health packs, and so on. Games usually need some way to do two things: notify objects that care about the event when it occurs, and have those objects respond to the event. The design pattern adopted by the event system is the Observer Pattern.
Principle
Abstract Events into Classes
Events usually fall into two parts: type and features. Type defines the overall information about events and features provide details. Some engine would call such structure as message or command.
There are several benefits to abstract events into classes:
- We only need one event processor function. All kinds of event could be represented by an instance of a class, so we only need a virual function to handle all of them, such as:
1 |
|
Events could be stored: Event objects store their types and parameters when they are created, so they are persistent and can be used for later processing, or to copy and broadcast to multiple receives.
Events could be easily forwarded: An object can forward an event to another object without knowing the details of the event.
Event Processing
Event Dispatcher: Most game objects only care about a small set of events, and it is very inefficient to multicast events every time. To improve the efficiency of event handling, objects can register the events they care about. For example, we can implement an event dispatcher for an object that accepts an event type to bind to. When a specific event is triggered, the dispatcher compares the incoming event type with the bound event type and passes the matching event as a parameter to the event handler of the object.
Callback Functions: Somtimes a game object receives an event and needs to respond in some way. This process is called event handling and is usually implemented as a function called an callback function. In some high-level languages, callback functions can be registered by storing function pointers (C/C++) or delegates (C#) and called when specific events are received.
Event Forwarding:There are often dependencies between game objects, and events sometimes need to be passed down the dependency chain. Usually, the order of event delivery is determined by the developer, and a Boolean value is returned by the event handler to indicate whether the object has processed the event and whether to continue forwarding.
Implementation
Event Type/ Event Category
Events include key events, mouse events, application events, etc. For key events, there would be states like pressed, released, continous pressed. And for mouse events, there would be pressed, moved, scrolled, and so on. So, we could abstract them into some enums called EventType and include them with different categories.
Thus, we need to create a file called Events.h in Illusion/src/Engine/Event/… folder and enter:
1 |
|
- BIT(x) is a macro defined in Core.h, which is used to shift 1 to the left by x bits.
1 |
|
The reason why we set EventCategory like this is based on the following requirement: A single event could be in multiple categories. For example, KeyPressed could be in EventCategoryInput, but it could also be in EventCategoryKeyboard. In case like this, how do we figure out whether an event belongs to a certain event category?
The more direct but complicated way to do this is to maintian a list of categories in each event type. When we need to figure out whether it is in the catergory, we just need to iterate through the list and return true if the categories matched with the desired one.
Here we use the idea of bitfields to implement the solution. Each category is assign a binary value instead of decimal numbers like 1,2,3. By doing so, these non-zero bits are like flags that represent different categories. We could easily mask out the value we need.
- Here’s an example: For event type belongs to EventCategoryInput, EventCategoryMouse, and EventCategoryMouseButton at the same time, its category could be represented like EventCategoryMouseButton | EventCategoryMouse | EventCategoryInput which equals to 11010. If we want to figure out whether it belongs to EventCategoryMouse, we could simply use EventCategoryMouse & EventType’s Category which would turn into 1000 & 11010 => 01000. In the end, 01000 equals to true since only 00000 equals to false. So this event belongs to EventCategoryMouse.
Event
The Event class is a virtual class. It is used as a template for all events. Create the class in Events.h.
1 |
|
There are two macros defined here: EVENT_CLASS_TYPE(type) and EVENT_CLASS_CATEGORY(catergory). By using them, event classed inherited from Event such as KeyEvent, MouseEvent, can easily complete the necessary function implementation at once.
For an input event, it is necessary to store an bool called EventHandled, which is used to detect whether the current event still can be responded to, since we don’t want everything to have the same priority. For example, when clicking on the UI, we don’t want to shoot a bullet suddenly. In short, events need to be consumed, and the consumed events will not be responded to by other logic.
Event Dispatcher
To register the events that objects care about, we need to implement an event dispatcher. Basically, it binds an event with a callback function. When the event it listens to is triggered, the dispacter would call the corresponding callback function. Event dispatcher corresponds to a function one by one, and this function returns a bool to decide whether to forward this event.
In Events.h, create a class called EventDispatcher:
1 |
|
The most tricky and important part is the function called Dispatch. This function receive a function as its parameter and for the Event T, it compares T’s type and the type that dispatcher bound to. If events’ types matched, it would call the function it received.
func(*(T*)&m_Event) seems really scary, but its principle is simple: we take the address of m_Event, cast it into a pointer of T, and then dereference it. And because func() is actually std::function<bool(T&)>, the actual parameter is a *reference of an Event.
KeyEvent
KeyEvent class inherits from Event class. Create KeyEvent.h in Illusion/src/Engine/Events/… folder.
1 |
|
ToString() is used to support the log system.
RepeatCount is used to distinguish whether the user has pressed the button once or kept pressing the button.
MouseEvent
MouseEvent class inherits from Event class. Create MouseEvent.h in Illusion/src/Engine/Events/… folder.
1 |
|
AppEvent
AppEvent class inherits from Event class. It is used to handle the events that are related to windows. Create AppEvent.h in Illusion/src/Engine/Events/… folder.
1 |
|
Conclusion
So far, we have completed a simple event system. In order to use it, we could add some code in Application.cpp:
1 |
|