|
Event System using C++ 11 |
Fishcake
Member #8,704
June 2007
|
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: 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 ). I'm looking forward to feedbacks.
|
SiegeLord
Member #7,827
October 2006
|
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. 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 |
Fishcake
Member #8,704
June 2007
|
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. Thank you very much.
|
Edgar Reynaldo
Major Reynaldo
May 2007
|
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. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
relpatseht
Member #5,034
September 2004
|
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
Major Reynaldo
May 2007
|
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. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
Fishcake
Member #8,704
June 2007
|
Edgar Reynaldo said: 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: 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. relpatseht said: 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: 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
|
No you don't. 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
|
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. 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
|
kazzmir said: An alternative strategy is to cast a static member to an int and use that as the id. Awesome trick!
|
|