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
virtual void OnEvent(Event& event);
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#pragma once

#include "pch.h"
#include "Engine/Core/Core.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
enum class EventType
{
None = 0,
WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved,
AppTick, AppUpdate, AppRender,
KeyPressed, KeyReleased, KeyTyped,
MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled
};

enum EventCategory
{
None = 0,
EventCategoryApplication = BIT(0), //1
EventCategoryInput = BIT(1), //10
EventCategoryKeyboard = BIT(2), //100
EventCategoryMouse = BIT(3), //1000
EventCategoryMouseButton = BIT(4) //10000
};
}
  • BIT(x) is a macro defined in Core.h, which is used to shift 1 to the left by x bits.
1
#define BIT(x) (1 << x)
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	//Macros used to implement GetStaticType/GetEventType/GetName/GetCategoryFlags in the inherited class
// # is used to transform type into a string
// GetStaticType can be used as a static method, could be used without an object
// GetEventType can be used as a property method, can only be used with an instance/object
#define EVENT_CLASS_TYPE(type) static EventType GetStaticType() { return EventType::type; }\
virtual EventType GetEventType() const override { return GetStaticType(); }\
virtual const char* GetName() const override { return #type; }

#define EVENT_CLASS_CATEGORY(category) virtual int GetCategoryFlags() const override { return category; }
// Abstract event class
// All kinds of events inherates from event
class Event
{
// Make the Dispatcher to be a friend class
// Authorize the Dispatcher to edit m_Handled
friend class EventDispatcher;
public:
// = 0 means pure virtual/ it has to be implemented elsewhere in inherited class
// const means the function would not change the property of the class
virtual EventType GetEventType() const = 0;
virtual const char* GetName() const = 0;
virtual int GetCategoryFlags()const = 0;
virtual std::string ToString() const { return GetName(); }
// Check whether the input is in certain category
inline bool IsInCategory(EventCategory category)
{
// GetCategoryFlags returns flags Such as 10000/01000/00001
// category usually used as EventCategoryMouseButton | EventCategoryMouse | EventCategoryInput
// the code is equivelent to (example):
// return 10000 & 11010, which is 10000
// For bool, if there's at least on bit not 0, the return would be true.
return GetCategoryFlags() & category;
}
// Check whether the event still have to be handled
bool m_Handled = false;
};
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Class Event Dispatcher
class EventDispatcher
{
// Define EventFn() Template
// Input: T&
// Output: bool
template<typename T>
// Set EventFn as another name for std::function<bool(T&)>
using EventFn = std::function<bool(T&)>;
public:
// Bind the event to the dispatcher
// Each dispatcher could only bind with one kind of event
EventDispatcher(Event& event)
: m_Event(event) {}
// Make a template funtion to dipatche the event
// Input: a function
// Output: bool
template<typename T>
bool Dispatch(EventFn<T> func)
{
// Compare the bound event with the input event associated with the function
// GetStaticType is implemented in class inherited from Event, in this context it is T
if (m_Event.GetEventType() == T::GetStaticType())
{
// Call the input function with *(T*)&m_Event, which means:
// &m_Event = the address of the event bound to the dipatcher
// (T*) = the cast to the pointer towards the type T
// * = dereference the pointer
// The output of the function determins wheter to consume the event or not
m_Event.m_Handled = func(*(T*)&m_Event);
return true;
}
return false;
}
private:
Event& m_Event;
};
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#pragma once

#include "Events.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
// KeyEvent
class KeyEvent : public Event
{
public:
inline int GetKeyCode() const { return m_KeyCode; }

EVENT_CLASS_CATEGORY(EventCategoryKeyboard | EventCategoryInput)

protected:
// Bind it with a certain key
KeyEvent(int keycode)
: m_KeyCode(keycode) {}

int m_KeyCode;
};

// Keypressed Event
class KeyPressedEvent : public KeyEvent
{
public:
KeyPressedEvent(int keycode, int repeatCount)
:KeyEvent(keycode), m_RepeatCount(repeatCount) {}

inline int GetRepeatCount() const { return m_RepeatCount; }

std::string ToString() const override
{
std::stringstream ss;
ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_RepeatCount << " repeats)";
return ss.str();
}

EVENT_CLASS_TYPE(KeyPressed)

private:
int m_RepeatCount;
};

class KeyReleasedEvent : public KeyEvent
{
public:
KeyReleasedEvent(int keycode)
: KeyEvent(keycode) {}


std::string ToString() const override
{
std::stringstream ss;
ss << "KeyReleasedEvent: " << m_KeyCode;
return ss.str();
}

EVENT_CLASS_TYPE(KeyReleased)
};


// Keytyped Event
class KeyTypedEvent : public KeyEvent
{
public:
KeyTypedEvent(int keycode)
:KeyEvent(keycode) {}

std::string ToString() const override
{
std::stringstream ss;
ss << "KeyTypedEvent: " << m_KeyCode;
return ss.str();
}

EVENT_CLASS_TYPE(KeyTyped)

};
//--------------------namespace: Illusion ends--------------------
}
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#pragma once

#include "Events.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
// Mouse Moved Event
class MouseMovedEvent : public Event
{
public:
MouseMovedEvent(float x, float y)
: m_MouseX(x), m_MouseY(y) {}

inline float GetX() const { return m_MouseX; }
inline float GetY() const { return m_MouseY; }

std::string ToString() const override
{
std::stringstream ss;
ss << "MouseMovedEvent: " << m_MouseX << ", " << m_MouseY;
return ss.str();
}

EVENT_CLASS_TYPE(MouseMoved)
EVENT_CLASS_CATEGORY(EventCategoryMouse | EventCategoryInput)

private:
float m_MouseX, m_MouseY;
};

// Mouse Scolled Event
class MouseScrolledEvent : public Event
{
public:
MouseScrolledEvent(float xOffset, float yOffset)
: m_XOffset(xOffset), m_YOffset(yOffset) {}

inline float GetXOffset() const { return m_XOffset; }
inline float GetYOffset() const { return m_YOffset; }

std::string ToString() const override
{
std::stringstream ss;
ss << "MouseScrolledEvent: " << GetXOffset() << ", " << GetYOffset();
return ss.str();
}

EVENT_CLASS_TYPE(MouseScrolled)
EVENT_CLASS_CATEGORY(EventCategoryMouse | EventCategoryInput)

private:
float m_XOffset, m_YOffset;
};


// Mouse Button Event
class MouseButtonEvent : public Event
{
public:
inline int GetMouseButton() const { return m_Button; }
EVENT_CLASS_CATEGORY(EventCategoryMouse | EventCategoryInput)

protected:
MouseButtonEvent(int button)
: m_Button(button) {}

int m_Button;
};

// Mouse Button Pressed Event
class MouseButtonPressedEvent : public MouseButtonEvent
{
public:
MouseButtonPressedEvent(int button)
: MouseButtonEvent(button) {}

std::string ToString() const override
{
std::stringstream ss;
ss << "MouseButtonPressedEvent: " << m_Button;
return ss.str();
}

EVENT_CLASS_TYPE(MouseButtonPressed)
};

// Mouse Button Released Event
class MouseButtonReleasedEvent : public MouseButtonEvent
{
public:
MouseButtonReleasedEvent(int button)
: MouseButtonEvent(button) {}

std::string ToString() const override
{
std::stringstream ss;
ss << "MouseButtonReleasedEvent: " << m_Button;
return ss.str();
}

EVENT_CLASS_TYPE(MouseButtonReleased)
};

//--------------------namespace: Illusion ends--------------------
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#pragma once

#include "Events.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
// Window Resize Event
class WindowResizeEvent : public Event
{
public:
WindowResizeEvent(unsigned int width, unsigned int height)
: m_Width(width), m_Height(height) {}

inline unsigned int GetWidth() const { return m_Width; }
inline unsigned int GetHeight() const { return m_Height; }

std::string ToString() const override
{
std::stringstream ss;
ss << "WindowResizeEvent: " << m_Width << ", " << m_Height;
return ss.str();
}

EVENT_CLASS_TYPE(WindowResize)
EVENT_CLASS_CATEGORY(EventCategoryApplication)
private:
unsigned int m_Width, m_Height;
};

// Window Close Event
class WindowCloseEvent : public Event
{
public:
WindowCloseEvent() {}
EVENT_CLASS_TYPE(WindowClose)
EVENT_CLASS_CATEGORY(EventCategoryApplication)
};

// App Tick Event
class AppTickEvent : public Event
{
public:
AppTickEvent() {}

EVENT_CLASS_CATEGORY(EventCategoryApplication)
};

// App Update Event
class AppUpdateEvent : public Event
{
public:
AppUpdateEvent() {}

EVENT_CLASS_TYPE(AppUpdate)
EVENT_CLASS_CATEGORY(EventCategoryApplication)
};

// App Render Event
class AppRenderEvent : public Event
{
public:
AppRenderEvent() {}

EVENT_CLASS_TYPE(AppRender)
EVENT_CLASS_CATEGORY(EventCategoryApplication)
};
}

Conclusion

So far, we have completed a simple event system. In order to use it, we could add some code in Application.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "pch.h"
#include "Application.h"
#include "Engine/Core/Log/Log.h"
#include "Engine/Events/Events.h"
#include "Engine/Events/AppEvent.h"

namespace Illusion
{
void Application::Run()
{
WindowResizeEvent e(1280,720);
ENGINE_CORE_TRACE(e);
while (true);
}
}

Illusion Engine 04 - Event System
https://rigel.github.io/EventSystem/
Author
Rigel
Posted on
August 5, 2022
Licensed under