· 6 years ago · Sep 03, 2019, 09:50 PM
1gonna nerd out over something i did b/c i'm proud
2
3so, i'm using c++ for my game engine, largely for performance reasons -- code compiled to native machine instructions is ***fast*** compared to code that runs in an interpreter
4
5there's a lot of nice stuff in c++, but one thing that's lacking is an event system, which is an out-of-the-box feature in other languages. take javascript (or any other ECMAScript variant, like the ActionScript language in Flash)
6
7https://www.w3schools.com/js/js_htmldom_eventlistener.asp
8
9i've worked in those languages, too, and whenever I went back to c++, i missed having an event system like that
10
11i looked around to see if there was anything that i could easily find that fit my needs, and while i did find a few event libraries for c++ here and there, they were all lacking in some way, or i disagreed with their design decisions
12
13so i recreated a feature that just exists in other languages from scratch in c++. other versions exist, but i made my own flavor of it
14
15long-term architecture-wise, this is important to help keep different domains separate from each other. it's one way to achieve what's known as 'decoupling'. it keeps things in self-contained modules that are easier to reason about instead of putting everything in a giant unreadable behemoth
16
17like, let's say i have a game that has an achievement system, and it has achievements for getting your first coin, ten coins in a single run, a hundred coins, etc. what i *don't* want is to litter my coin pickup code with achievement code, i don't want them inextricably linked
18
19what i can do is have a coin pickup event type, and in my pickup code, just have it fire off an event. "hey, i picked up a coin; idk who cares but this happened." if nothing's listening to the event, cool, it drops to the floor, no harm done. on the achievement side of things, i can have it listen to coin pickup events and do the appropriate things inside that now-cordoned-off section of code
20
21this might seem a little trivial with just those two sections, but now imagine trying to maintain a codebase where picking up a coin:
22- has to check if you got an achievement, and if so, reach out to the Achievement API
23- plays the 'coin pickup' audio
24- leaves behind a sparkling animation in the air for a few frames where the coin was
25- might spawn a thief enemy once you reach enough coins
26
27and any other effects, all at once. events are starting to look a little good here, huh
28
29the end effect is the same, but it's a way to keep things readable, understandable, and less prone to risk and change if something needs to be redone or ported over to something that needs *one* half but not the other
30
31the other important part here is also that this defers functionality to happen later, eventually, and not immediately. it queues up the event and leaves it to something later down the line to handle, it doesn't need to
32
33like, imagine picking up a coin and waiting on the game while it makes some call to Steam's API and doesn't return control to the rest of the game's update flow until it finishes
34
35(network access like that, btw, is slow, at least compared to game updates)
36
37so, i've got this EventManager class, and it's got these functions:
38
39```
40//register a callback function to run when an event of the specified eventType gets dispatched
41void addEventListener(EventType eventType, EventCallbackFunction eventCallbackFunction);
42
43//removes an existing EventType/EventCallbackFunction pair
44void removeEventListener(EventType eventType, EventCallbackFunction eventCallbackFunction);
45
46//fires off an event, e.g. "hey i picked up a coin" -- adds it to queue
47void dispatchEvent(const Event& event);
48
49//called once per frame -- go through the queue, and call any event callback functions associated with events in the queue
50void pollEvents();
51```
52
53and there's some data types here that aren't C++ standard, so to give you an idea, here they are:
54
55```
56
57//most event libraries that I could find use strings for a string type, but string comparison is slow compared to integer comparison
58//secretly, string comparison is just a loop of integer comparison, so a string of 10 characters takes 10 processor cycles to compare
59//while an integer that's 10 digits long takes just 1 cycle. so i just use a 32-bit integer here.
60typedef uint32_t EventType;
61
62//calls to functions can be stored as data now, as of C++11, and it owns https://en.cppreference.com/w/cpp/utility/functional/function
63typedef std::function<void(const Event& e)> EventCallbackFunction;
64
65//
66struct Event
67{
68 const EventType eventType;
69 std::shared_ptr<void> payload;
70
71 Event(const EventType et, const std::shared_ptr<void> p = nullptr) : eventType(et), payload(p) {}
72 Event(const Event& e) : eventType(e.eventType), payload(e.payload) {}
73};
74```
75
76more on event type:
77
78you might think, "well, this makes event types inflexible and hard to read," and you're *kind* of right, but you can define constants for your event types, OR you can even do what I do normally here and use character literals. 'quit' and 'test' and 'fire' and 'jump' etc. any four characters encased in single-quotes evaluate to a 32-bit integer, not a string.
79
80take 'quit', for example. the compiler will insert a 32-bit integer that maps to the ascii values of the characters in quit.
81
82base 10: 1903520116
83base 16: 71756974, or 0x71 0x75 0x69 0x74
84
85ascii codes:
860x71 = 'q'
870x75 = 'u'
880x69 = 'i'
890x74 = 't'
90
91this means when i dispatch events, i just do this, for example:
92
93```
94EventManager& em = EventManagerLocator::getService();
95Keyboard& keyboard = KeyboardLocator::getService();
96
97if (keyboard.isKeyDown(Keyboard::KeyboardButton::Q))
98{
99 em.dispatchEvent(EventManager::Event('quit'));
100}
101
102//and then once per frame,
103em.pollEvents();
104```
105
106you can probably glean that when the player presses the 'Q' key, this code fires off a 'quit' event, and if there's anything listening for that event, it'll execute whatever callback function was registered. this part of the code doesn't care what's listening, it just kinda yells out "hey it's quittin' time"
107
108elsewhere in my code, when the game's first setting up, i do in fact set up a listener for the 'quit' event:
109```
110EventManager& em = EventManagerLocator::getService();
111em.addEventListener('quit', BIND_EVENT_CALLBACK_FUNCTION(Game::Quit));
112```
113
114BIND_EVENT_CALLBACK_FUNCTION is a macro i defined earlier to make the process of constructing a callback function easier. going "hey, make a callback to Game::Quit" is way easier than, uh, having to remember this mess each time
115```
116#define BIND_EVENT_CALLBACK_FUNCTION(func) std::bind(&func, this, std::placeholders::_1)
117```
118
119and here's what Game::Quit refers to: a function that matches the signature of void(const Event& e)
120```
121void Game::Quit(const EventManager::Event& e)
122{
123 m_shouldQuit = true;
124}
125```
126
127m_shouldQuit is what controls the game's update loop, and is its exit condition here
128
129```
130while (m_shouldQuit == false)
131{
132 time.startNextFrame();
133 this->Update(time.getLastFrameTimeDelta());
134 graphics.swapBuffers();
135}
136```
137
138if the game's execution falls outside of that loop, the program ends. that's how I have it for now, at least, though I could change what a Quit call actually does at some point later on down the line, like asking the player if they want to save first, or if they really want to quit, or do other stuff. point is, it's self-contained and easy to reason about