How I Created My Single Threaded Event System

Published August 26, 2013 by Will Song, posted by incertia
Do you see issues with this article? Let us know.
Advertisement
So this is going to be my attempt at trying to explain to you guys how I came up with my event system in C++. Anyways, to start off, why do we need event systems? Well, often times in code we come across situations similar to this: when A happens, we want to do B and C. Sure, it's easy to handle with if statements if you only have a few things running at once, but what if you have many different types of objects that can have different things happening to them? Writing if code for those is going to take a while for sure. Now that we know why we need them, let's see what they are. An event system will contain:
  1. A bunch of custom events that can be triggered
  2. Event callback functions to handle specific events when they are triggered
  3. A management system to attach/remove/call event callbacks
  4. Objects with triggerable events
In code, this will look something like (from Infinity): //INFINITY_CALL is just __stdcall namespace Infinity { typedef INFINITY_VOID (INFINITY_CALL *EventCallback_t)(IEvent *); /// ///The event manager: implement this if a class has events that should be listened to /// /// ///The implementing project should probably write its own class implementing IEventManager to avoid writing duplicate code /// struct IEventManager { virtual INFINITY_VOID INFINITY_CALL AttachEventListener(INFINITY_GUID EventID, EventCallback_t f) = 0; virtual INFINITY_VOID INFINITY_CALL DetachEventListener(INFINITY_GUID EventID, EventCallback_t f) = 0; virtual INFINITY_VOID INFINITY_CALL TriggerEvent(IEvent *e) = 0; }; /// ///The event interface /// struct IEvent { virtual INFINITY_GUID INFINITY_CALL GetEventType(INFINITY_VOID) const = 0; virtual INFINITY_VOID INFINITY_CALL GetEventData(INFINITY_VOID *pData) = 0; }; } First you see a generic callback function. This is the form that all event callbacks within my system will have. Next, I essentially have a pure virtual interface that each project will implement to allow flexible control over how they want to manage their own special event system for flexibility. I also made this design choice because exporting classes with STL containers is generally not a good thing to do. I needed a way to associate certain events with certain callbacks so I took the easy route and used GUIDs to identify the different types of events that are going to potentially occur within my system. Callbacks are easily identifiable because of their addresses in memory, so that's all good. AttachEventListener should register the specified callback with the specified event, while DetachEventListener should do the exact opposite. TriggerEvent does exactly what it sounds like it should - it is given an event and it should call all the callback functions that are registered to process the specified event. The only problem I foresee is that TriggerEvent has public visibility so idiots may decide to spawn random and meaningless events. For the event interface, we have a function that will return the GUID of the event, for identification purposes and potential casting later on in the code. We also have a function that will retrieve all the data stored with the event. How this data will be filled is left to the implementation of the event. Perhaps the data is encapsulated in a class, or maybe a struct, or maybe it's just a single byte of data. This is why we use the void pointer. Now that you see how I laid out the event system, you should take the time to see why this works. After that, move on down to see how I used this in my test implementation. YAY YOU MADE IT TO MY IMPLEMENTATION Okay, let's get serious. We need a manager base class so our objects can be created without copy pasting event management code. I decided to do something like this: class CEventManager : public Infinity::IEventManager { public: CEventManager() {} ~CEventManager() {} //GUID comparison functor typedef struct { bool operator()(const INFINITY_GUID &left, const INFINITY_GUID &right){ if(left.Data1 < right.Data1) return true; if(left.Data2 < right.Data2) return true; if(left.Data3 < right.Data3) return true; for(register size_t i = 0; i < 8; i++) if(left.Data4 < right.Data4) return true; return false; } } GUIDComparer; virtual INFINITY_VOID INFINITY_CALL AttachEventListener(INFINITY_GUID EventType, Infinity::EventCallback_t f){ //Just insert it event_map[EventType].insert(f); } virtual INFINITY_VOID INFINITY_CALL DetachEventListener(INFINITY_GUID EventType, Infinity::EventCallback_t f){ //See if the event type has any listeners auto it = event_map.find(EventType); if(it != event_map.end()){ //If so, see if it has our listener function auto func_it = it->second.find(f); if(func_it != it->second.end()){ //Then remove it it->second.erase(func_it); } } } virtual INFINITY_VOID INFINITY_CALL TriggerEvent(Infinity::IEvent *e){ //Launch listeners INFINITY_GUID magic = e->GetEventType(); std::for_each(event_map[magic].begin(), event_map[magic].end(), [&] (Infinity::EventCallback_t f) -> INFINITY_VOID { f(e); }); delete e; //Because it's called with something like TriggerEvent(new IEvent(...)); } private: //Global event listener storage for each event //Maps (GUID EventType) -> set of listener functions std::map, GUIDComparer> event_map; }; As you can see, I am mapping each GUID to a set of callbacks. I could have used a linked list, vector, or any other container, but I chose a set. However, GUIDs do not have any sort of comparator so I had to define my own, which just compares each part of the GUID in a logical order. However you decide to compare GUIDs is up to you, but make sure that if a < b and b < c, you have a < c. Notice how I set a standard of triggering events. There are alternate ways. One way would be for the object triggering the event to create the event before calling the trigger event and deleting the event after calling the trigger event. Another way would be to create the event on the stack because the event itself is not abstract and pass the address on to the trigger function. The 3 main functions are all self-explanatory with their purpose. Next, I needed an object. I chose to create a button that could be pushed by a person. When the button was pushed, it would spawn an event saying that it was pushed. To store all this data, a struct seemed like the logical choice. typedef struct { const char *who; const char *bname; } ButtonPushedEventData; class __declspec(uuid("{0AE142DB-2B0F-45B4-8D24-A3390F7EC18E}")) ButtonPushedEvent : public Infinity::IEvent { public: ButtonPushedEvent(const char *_who, const char *_bname){ data.who = _who; data.bname = _bname; } ~ButtonPushedEvent(){} INFINITY_GUID INFINITY_CALL GetEventType() const { return __uuidof(ButtonPushedEvent); } INFINITY_VOID INFINITY_CALL GetEventData(INFINITY_VOID *pData){ memcpy(pData, &data, sizeof(data)); } private: ButtonPushedEventData data; }; class CButton : public CEventManager { public: CButton(const char *_name) : name(_name) {} ~CButton() {} void push(const char *who){ this->TriggerEvent(new ButtonPushedEvent(who, this->name)); } private: const char *name; }; Now all we need are event handlers and we are done. I chose to create 2 so I could test removing one of them: INFINITY_VOID INFINITY_CALL generic_bpress_handler(Infinity::IEvent *e){ if(e->GetEventType() == __uuidof(ButtonPushedEvent)){ ButtonPushedEventData data; e->GetEventData(&data); printf("Button \"%s\" pushed by \"%s\"\n", data.bname, data.who); } else { printf("We shouldn't be here...\n"); } } INFINITY_VOID INFINITY_CALL oops(Infinity::IEvent *e){ if(e->GetEventType() == __uuidof(ButtonPushedEvent)){ printf("HERPADERPADURR\n"); } else { printf("We shouldn't be here 2...\n"); } } int main(int argc, char **argv){ CButton b1("lolwat"), b2("button"); b1.AttachEventListener(__uuidof(ButtonPushedEvent), generic_bpress_handler); b2.AttachEventListener(__uuidof(ButtonPushedEvent), generic_bpress_handler); b2.AttachEventListener(__uuidof(ButtonPushedEvent), oops); b1.push("person"); b2.push("herp"); b2.DetachEventListener(__uuidof(ButtonPushedEvent), oops); b2.push("herp"); system("pause"); return 0; } Aaaand here's an image of it in action: mkO9Fvp.png You can currently find the source for this in the dev branch of its Github repository under commit 01a4f93.
Cancel Save
0 Likes 8 Comments

Comments

Cygon

What do you consider to be the advantages of such an event distribution system compared to a signal/slot approach (like in libsigc++, Boost.Signals or sigslot)?

It seems to require a downcast (putting type safety into the hands of the user, any mistake would lead to a crash that is only detectable at runtime when the event gets fired). It also adds a map lookup to event processing that isn't present in the signal/slot design.

A possible advantage you could mention might be that a visual editor or scripting language binding could enumerate the available events (a feature easily implemented). Though of course one might still argue that decoupling such reflection abilities (by using a signal/slot system and an external reflection information provider class) would be truer to the SRP and allow seamless binding of third-party classes, too.

An unrelated note on the implementation:

Your CEventManager::TriggerEvent() deletes its argument, but the ownership transfer doesn't seem apparent from the outside. Or even required: a const ref would allow construction of the event arguments on the stack of the caller and dissolve any ownership concerns.

August 26, 2013 07:25 PM
incertia
In retrospect I suppose a finite signal/slot approach would be better to reduce the minimal amount of look-up overhead because nobody ever needs infinitely many unique events. Notice how mine is essentially an infinite version with the map.

As for the type safety concern, it is primarily the user creating all the different events and data structures related to that event, so if the user doesn't know what data type to use then it would be his error. However, there is the off chance that the user may accidentally connect a listener not designed for that specific event, which would definitely cause an error if the expected struct was smaller than expected.

The details of CEventManager are entirely up to the user and I was setting up a quick test for myself, so I became a bit careless.

On second thought, I will probably update this with a signal/slot approach in the near future because:
(1) Nobody needs infinite events
(2) I can ensure type safety and infinite arguments with structs and templates
(3) There is no need for live pointers to be passed around
August 26, 2013 08:41 PM
b5cully

Very interesting article, seems to be a little like the event manager I planned out a while ago, just much better smile.png

A little note here though: providing a small graphic or diagram to display the rough structure would be very nice to demonstrate this system's functionality (coming from someone who is more a visual person).

Also glad to see I'm not the only one who uses "herp" and "derp" as placeholder text to test out code... :D

August 27, 2013 02:33 AM
Alpha_ProgDes
Given that there's more code than explanation of the code, I think this would be better as a journal entry. I've used events but I've never coded one before. So I'm to assume that this article is for programmers who are closer to advanced.
August 27, 2013 05:56 AM
buggypixels

The basic idea of supporting events in a game engine is nice. Actually it is a simple way of decoupling different systems. Somehow it seems that providing a gobal event manager is a common approach.

I strongly disagree here and here is a perfect explanation why: http://bitsquid.blogspot.de/2009/12/events.html

I really like the idea and in my games all that is left of my previous event system is just an event stream buffer. Now a subsystem can write to its own event buffer stream and a higher level system may ask for the stream and process it.

IMHO it makes everything so much easier and predictable.

August 27, 2013 08:13 AM
swiftcoder

Regardless of what you do in your own code, when posting to an article I'd prefer you find/replace all those [tt]INFINITY_VOID[/tt] tokens with plain old [tt]void[/tt]...

Although I am curious - why on earth do you use a macro for void?

August 29, 2013 01:26 AM
Liuqahs15

Regardless of what you do in your own code, when posting to an article I'd prefer you find/replace all those INFINITY_VOID tokens with plain old void...

Although I am curious - why on earth do you use a macro for void?

I'm confused.


typedef    INFINITY_VOID        (INFINITY_CALL    *EventCallback_t)(IEvent *);

That's not void, is it? Then again, he isn't returning anything in his INFINITY_VOID functions, so obviously you're right. I just don't understand that code.

August 31, 2013 12:31 PM
naturally

Thank you for submitting this article, I learned some new things indeed!

Several questions come to my mind, though:

  • You make use of uuid and __uuidof - those are windows specifics, right? Why do you even make such a small part platform specific? I think that is not necessary at all.
  • Why do you use `typedef struct {} Name` instead of `struct Name {}`?
  • Are you sure the image displays correct function? If i read the code in your "main" within the image, the second "herp" should not be printed as you detach the listener.
  • Why exactly do you not use the STL? E.g. favoring std::string over [const] char *
September 01, 2013 10:38 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

A walkthrough of how I implemented an event system I wrote.

Advertisement

Other Tutorials by incertia

incertia has not posted any other tutorials. Encourage them to write more!
Advertisement