Jump to content

  • Log In with Google      Sign In   
  • Create Account

We're offering banner ads on our site from just $5!

1. Details HERE. 2. GDNet+ Subscriptions HERE. 3. Ad upload HERE.


Like
1Likes
Dislike

Effective Event Handling in C++

By Szymon Gatner | Published Feb 08 2008 05:10 AM in Game Programming

event class type void handling method code message classes
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

Many times in your game different (not just different in terms of instance but in terms of type especially) game objects have to communicate with each other. To achieve loose coupling between communicating classes, a generic message passing system is needed

First approach (or good old C approach):

One of the most popular methods of implementation for such a system is some kind of structure containing an integer value indicating the actual type of message (event). Based on this value, the message structure (or union) is treated differently by client code. For example:
 
void VeryBadMonster::handleEvent(Event* evnt)
{
   switch(evnt->type)
   {
   case MSG_EXPLOSION:
      handleExplosion(evnt->position, evnt->damage); 
      //event is union 
      //or handleEvent( (ExplosionMsg*) event) if event is struct
   case MSG_TRAP:
      // etc
   }
}
 
Although simple and fast, this method has some serious flaws. First: a centralized source of MSG_XXXX identifiers. The header file containing them has to be appended carefully to avoid duplicates. On top of that it has to be modified every time a new message type is added so recompilation of all files that include it is necessary. That basically means that all classes that are totally unrelated to this message now depend on it. Second: the switch statement has to be rewritten for every event (message) handling object and is therefore prone to programming errors. Additionally, the presented code has very little to do with object-oriented programming and its reuse capabilities are reduced to copy-and-paste.

Second approach(or C++ approach – postfix increment means that we have one more than C now but still have access to C ;):

This more elegant solution is based on C++ dynamic RTTI capabilities. In this approach the main message handling function determines the actual message type using dynamic_cast<>.

First the Event class:
 
class Event
{
protected:
   virtual ~Event() {};
};
 
Not very impressive. But we need this as a base for all our specific events. Now the only common code knowledge by two unrelated classes that want to be able to communicate with each other is just this class definition, that’s it.
 
void onEvent(Event* event)
{
  if(Explosion* explosion = dynamic_cast< Explosion* >(event))
  {
    onExplosion(explosion);
  }
  else if(EnemyHit* hit = dynamic_cast< EnemyHit* >(event))
  {
    onEnemyHit(hit);
  }
}
 
After a chain of if-else casting, the proper handling method is called. In the above example event handling method name depends on event type it handles but using overloading and templates can simplify this code a bit:
 
template < class EventT >
bool tryHandleEvent(const Event *event)
{
  if(const EventT* concreteEvent = dynamic_cast< const EventT* >(event))
  {
    return handleEvent(concreteEvent);
  }
  return false;
}

void onEvent(const Event* event)
{
  if(tryHandleEvent< Explosion >(event)) return;
  else if(tryHandleEvent< EnemyHit >(event)) return;
}
 
This solution is much better than the previous one: casting is now checked for validity, integer (MSG_XXXX) type identifiers (and unrelated class dependencies) are gone. Also, events are full-fledged objects now. But there are some drawbacks too (of course): changing from switch to if-else means event type identification complexity is now linear instead of constant, also the efficiency of dynamic_cast<> depends greatly on both the compiler and the depth of event class hierarchy; last but very important is that order of type queries has to be picked carefully, for example (Figure 1) : if Event pointer is dynamic_casted for Explosion first, and actually points to NukeExplosion object, the code will improperly detect its type and the handler function for Explosion will be called. The if-else chain has also the same drawback as switch statement: it is not reusable.


Posted Image
Figure 1


++C approach (or we love virtuals and templates, no more C ;):

The previous solution is fast (between first and second), safe (more than second), loosens coupling between unrelated event handling classes (as second) and is completely reusable (as none;). The only one thing that the event handling class is aware of is a stable (never changing) base event class and concrete message classes that it has interest in.

The idea is to derive concrete event types from the base class Event and register member functions to handle that event in EventHandler class instance. EventHandler is responsible for mapping from the type of event to the proper method that handles that event. Here is the code:
 
class EventHandler
{
public:
  void handleEvent(const Event*);

  template < class T, class EventT >
  void registerEventFunc(T*, void (T::*memFn)(EventT*));

private:
  typedef std::map< TypeInfo, HandlerFunctionBase* > Handlers;
  Handlers _handlers;
};

template < class T, class EventT >
void EventHandler::registerEventFunc(T* obj, void (T::*memFn)(EventT*))
{
  _handlers[TypeInfo(typeid(EventT))]=
  new MemberFunctionHandler< T, EventT >(obj, memFn);
}

void EventHandler::handleEvent(const Event* event)
{
  Handlers::iterator it = _handlers.find(TypeInfo(typeid(*event)));
  if(it != _handlers.end())
  {
    it->second->exec(event);
  }
}
 
Small but powerful, its purpose is registering actual event handling methods for specific event types. After receiving a generic event of type Event it determines its actual type and then calls the proper handler method with valid event as a parameter. As you can see, mapping is made between TypeInfo objects and pointers to HandlerFunctionBase instances. TypeInfo is a simple wrapper around type_info class that lets us store it as a key in std::map. Using standard map as the mapping method ensures logarithmic handler identification. EventHandler class stores all event handling methods and corresponding instance pointers in classes derived from HandlerFunctionBase. Concrete derivatives store member function pointers for different specific event types and are also responsible for proper down-casting to actual event types when calling the handling method. Here is the code:
 
class HandlerFunctionBase
{
public:
  virtual ~HandlerFunctionBase() {};
  void exec(const Event* event) {call(event);}

private:
  virtual void call(const Event*) = 0;
};
 
MemberFunctionHandler's purpose is to safely cast to proper event type.
 
template < class T, class EventT >
class MemberFunctionHandler : public HandlerFunctionBase
{
public:
  typedef void (T::*MemberFunc)(EventT*);
  MemberFunctionHandler(T* instance, MemberFunc memFn) : _instance(instance), _function(memFn) {};

  void call(const Event* event)
  {
    (_instance->*_function)(static_cast< EventT* >(event));
  }

private:
  T* _instance;
  MemberFunc _function;
};
 
As you can see, static_cast is used rather than dynamic_cast because all instantiations of MemberFunctionHandler class template are created automatically by registerEventFunc method of EventHandler class so no dynamic casting is needed and is always valid and safe. In this solution we have: safety of type detection (no more problems with order-of-type queries), logarithmic handler function resolution, constant static_cast instead of dynamic_cast and code reusability. However, as one of the reviewers pointed out, that kind of templates use leads to ‘code bloat’. This basically means that the compiler has more work to do since it has to generate valid instantiations of MemberFunctionHandler for every event handling method, resulting in increased compilation times and probably a larger executable (but in contrast, this is the way that one would normally implement polymorphic callbacks). Also, there is one more virtual function call overhead per handler - but again, test for fifteen different event types (all derived directly from Event class so just if-else vs. std::map::find was compared) showed that additional indirection is still better than dynamic_casting + linear search (and results would vary even more in case of a deeper inheritance or number of event types) .

This technique proved quite useful in my current project; it simplified code for event handling objects and because type identifiers are no longer needed (MSG_XXX defines) it also improved code design by relaxing coupling between communicating classes. The included demo code shows the last presented solution in action: it creates a Monster and a Tank instance and then forces them to respond to concrete events that my happen to them in a dangerous game world. Monster class publicly inherits from EventHandler class but Tank is using a nested private class and aggregation to hide its handling methods from the outside world. This idea just popped in to my head a few days ago so it may not be perfect, still I hope you like it. Feel free to contact me at szymon-dot-gatner-at-gmail-dot-com.

P.S

Actually there is one more technique not described here and it is based on the double dispatch mechanism. Its total runtime complexity is just two virtual function calls – which is usually nothing compared to time that it takes to handle the actual event. The reason it is omitted in this article is because that technique introduces serious design problems due to the fact of cyclic dependencies in classes. Nevertheless, for a small number of events this may still be a perfect solution.

References

[1] Applied C++: Practical Techniques for Building Better Software By Philip Romanik, Amy Muntz

[2] Modern C++ Design: Generic Programming and Design Patterns Applied By Andrei Alexandrescu







Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS