Allegro.cc - Online Community

Allegro.cc Forums » Off-Topic Ordeals » Event System using C++ 11

This thread is locked; no one can reply to it. rss feed Print
Event System using C++ 11
Fishcake
Member #8,704
June 2007
avatar

I was exploring C++ 11 and somehow I ended up writing a simple event system. The design is kind of based on Mike McShaffry's, as seen in his book, Game Coding Complete. However, I dislike having to hardcode the hash for each of the event defined, which is quite cumbersome. So I decided to use compile-time string hashing.

Defining an event goes like this:

struct MyCustomEvent : public BaseEvent<hash<"MyCustomEvent">> {
   MyCustomEvent(const std::string &message)
   // The first parameter is for debug purposes
   // (since you can't pass a string as a template parameter);
   // I might remove it for release build
   : BaseEvent("MyCustomEvent", time(NULL))
   , message(message) {
   }

   const std::string message;
};

It's quite long and error-prone, but that can be solved using macros:

#define EVENT_DEF(__s__) struct __s__ : public BaseEvent<hash(#__s__)>
#define EVENT_DEF_CTOR(__s__, ...) __s__(__VA_ARGS__): BaseEvent(#__s__, time(NULL))

// ...

EVENT_DEF(MyCustomEvent) {
   EVENT_DEF_CTOR(const std::string &message)
   , message(message) { // <-- This still looks weird though...
   }

   const std::string message;
};

The main feature of this event system is that it supports anonymous listeners:

auto dispatcher = std::make_shared<EventDispatcher>();

// ...

// Registers an anonymous listener to listen to any MyCustomEvent events
// It returns an id, which is used for removing it later
auto listenerId = dispatcher->addAnonymousListener<MyCustomEvent>(
   [](const std::shared_ptr<MyCustomEvent> event) -> bool {
      printf("Message: %s\n", event->message.c_str());
      // yada yada yada
      return true;
   });

And not to forget object listeners:

#SelectExpand
1class MyObserver: public IEventObserver { 2public: 3 // ... 4 5 bool onMyCustomEvent(const std::shared_ptr<MyCustomEvent> event) { 6 printf("Message: %s\n", event->message.c_str()); 7 // yada yada yada 8 return true; 9 } 10}; 11 12// ... 13 14auto myObserver = std::make_shared<MyObserver>(); 15// Registers an object listener to listen to any MyCustomEvent events 16dispatcher->addObjectListener(myObserver, &MyObserver::onMyCustomEvent);

Triggering an event is quite straightforward:

dispatcher->triggerEvent<MyCustomEvent>("Hello, world!");

An event can also be set to trigger in the future:

// Trigger the event 5 seconds in the future
dispatcher->queueEvent<MyCustomEvent>(5.0f, "Hello, future!");

// ...

// You need to call the update function in your update loop
// or somewhere to update the event dispatcher
dispatcher->update(dt);

Removing listeners is also easy:

// This removes an anonymous listener (the id is obtained when you call addAnonymousListener)
// (I really should rename this function to removeAnonymousListener...)
dispatcher->removeListener(listenerId);

// This removes an object listener for the event MyCustomEvent
// Currently, my design makes it impossible to remove all listeners
// associated with the object, but hopefully I will resolve it someday
dispatcher->removeObjectListener<MyCustomEvent>(myObserver);

The source code is available in Github (the source code is still quite messy, so please forgive me :P). I'm looking forward to feedbacks. :)

SiegeLord
Member #7,827
October 2006
avatar

I don't see the point for all of this complication when you can just write a simple signal/slot system.

Additionally, I don't see the point in having a numerical event type (that you use the hash function to compute). What's wrong with just the event name?

Lastly... the countdowns are not implemented efficiently or accurately. Instead of decrementing a lifetime of the event every update call, you should store the time when the event is meant to fire and then compare it to the passed time.

I.e.

void update(double time)
{
   for(event : events)
   {
       if(time > event.timeout)
           event.fire();
   }
}

This method avoids updating a bazillion float variables for no reason, avoids floating point errors accumulating from adding dt's all the time, allows the user to control how the time is stored, amongst other benefits. And don't use float for storing time. Even using double is questionable.

"For in much wisdom is much grief: and he that increases knowledge increases sorrow."-Ecclesiastes 1:18
[SiegeLord's Abode][Codes]:[DAllegro5]:[RustAllegro]

Fishcake
Member #8,704
June 2007
avatar

SiegeLord said:

Additionally, I don't see the point in having a numerical event type (that you use the hash function to compute). What's wrong with just the event name?

If you have a catch-all event handler function (though I'm not sure if my current implementation allows that), you can use a switch statement :

bool MyObserver::onEvent(const EventPtr event) {
   switch (event->type) {
      case EnemyCreatedEvent::GetType() :
         // yada yada
         break;

      case EnemyDestroyedEvent::GetType() :
         // yada yada
         break;
   }
}

Without it, you have to resort to comparing strings, which can be slow if the event handler is called lots of times.

SiegeLord said:

Lastly... the countdowns are not implemented efficiently or accurately.

Yes, I just realized that. :P Thank you very much.

Edgar Reynaldo
Member #8,592
May 2007
avatar

Uhm, I don't see the need for deriving an event class at all. All objects / entities / whatever can share the same kind of id without subclassing and using templates.

In my gui, I have a simple int NextId() {static int i = 0;return i++;} and then there is a Message class :

class Msg {
public :
   int topic;
   Widget* from;
   int message;// bit field or value relaying pertinent information
};

and then each new widget grabs an id to use in the event messaging system.

extern const int MY_WIDGET_ID;
//const int MY_WIDGET_ID = NextId();// In cpp file

class MyWidget : public Widget {
public :
   int id;

   MyWidget() : id(MY_WIDGET_ID) {}
};

And then use that id in all your messages. That separates events into topics or types, and then you give the address of the caller, and then give the message stored in a bitfield or value in an int.

relpatseht
Member #5,034
September 2004
avatar

If you want every type to have a unique id, why don't you do this instead:

Header:

unsigned GetNextId();

template<typename T>
unsigned GetId(){
    static unsigned id = GetNextId();
    return id;
}

Source:

static unsigned curId = 0;

unsigned GetNextId(){
    return curId++;
}

It cuts down on the amount of work you need to do to get a new type id. No extern consts necessary.

Edgar Reynaldo
Member #8,592
May 2007
avatar

Well, it just comes down to the difference between widget->GetId() and MY_WIDGET_ID. I don't think it matters but your way is good too.

Fishcake
Member #8,704
June 2007
avatar

Uhm, I don't see the need for deriving an event class at all. All objects / entities / whatever can share the same kind of id without subclassing and using templates.

The reason I subclass is to not restrict how the message payload is stored. Having an integer variable to store my payload is very limiting. If I were to stick to one class, then I will have to do something like this:

#SelectExpand
1class Event { 2public: 3 // ... 4 int id; 5 std::map<std::string, std::string> payload; 6}; 7 8// ... 9// Constructing an event: 10 11Event event (EVENT_ENEMY_DAMAGED); 12event.payload["health"] = to_string(health); 13// .. 14dispatcher->dispatch(event); 15 16// ... 17// Unwrapping an event: 18 19float health = from_string<float>(event.payload["health"]);

While that doesn't seem pretty bad, what if I want to attach some custom data structure to the message? I can't just magically convert it from and to a string.

If you want every type to have a unique id, why don't you do this instead:

The problem with that approach is that for each subclass to have a unique id, I must re-implement the GetID() method for each subclass:

#SelectExpand
1 2static unsigned curId = 0; 3unsigned GetNextId() { 4 return curId++; 5} 6 7// ... 8 9class IEvent { 10public: 11 // ... 12 virtual unsigned GetId() = 0; 13}; 14 15class BaseEvent : public IEvent { 16 // I can't provide the implementation for GetId() here, 17 // or else, all subclasses of BaseEvent will have the same 18 // id 19}; 20 21class MyCustomEvent : public BaseEvent { 22public: 23 // Which means I have to define this function for every 24 // event subclass, so that each will have a unique id 25 static unsigned GetId() { 26 static unsigned id = GetNextId(); 27 return id; 28 } 29};

relpatseht
Member #5,034
September 2004
avatar

No you don't.

#SelectExpand
1static unsigned curId = 0; 2template<typename T> 3unsigned GetId(){ 4 static unsigned id = curId++; 5 return id; 6} 7 8template <typename T> 9class BaseEvent{ 10 public: 11 unsigned GetId(){ 12 static unsigned id = GetId<T>(); 13 return id; 14 } 15}; 16 17class MyCustomEvent : public BaseEvent<MyCustomEvent>{ 18 public: 19 // ... 20};

Not to mention, wherever you want to, you can get the id of MyCustomEvent via GetId<MyCustomEvent>().

kazzmir
Member #1,786
December 2001
avatar

Static globals have the downsides of not being threadsafe and being non-deterministic relative to the ordering of your code. An alternative strategy is to cast a static member to an int and use that as the id.

#SelectExpand
1#include <stdio.h> 2 3template <class T> 4class Event{ 5public: 6 virtual int getId() const { 7 return (int) &id; 8 } 9 10private: 11 static int id; 12}; 13template <class V> int Event<V>::id; 14 15class Event1: public Event<char>{ 16public: 17 Event1(){ 18 } 19}; 20 21class Event2: public Event<int>{ 22public: 23 Event2(){ 24 } 25}; 26 27int main(){ 28 Event1 e1; 29 Event2 e2; 30 printf("e1 %d e2 %d\n", e1.getId(), e2.getId()); 31} 32 33$ e1 134520980 e2 134520984

Fishcake
Member #8,704
June 2007
avatar

kazzmir said:

An alternative strategy is to cast a static member to an int and use that as the id.

Awesome trick! :o

Go to: