Simple Event Handling
IntroductionI recently wrote a simple event handling module for a group project. I found it to be quite a useful tool, and I'm writing this article so that hopefully others may find it useful as well. Our group project was implemented by five programmers, each writing a different component of the game. The need for event handling arose when different components each needed to know about certain types of input (mainly keyboard and mouse events), but didn't need to know about the underlying lower-level implementation. For example, components needed to know when a key corresponding to a certain action was pressed (such as the “jump” key), but didn't want to know which actual key was pressed. From that arose this simple event dispatching and handling system. The goals for this system where three-fold:
The rest of the article will step through each part of the system, explaining the implementation, as well as making suggestions for improvements. The final section will illustrate the use of this event handler/dispatcher, which will make its usage clear. EventsThe first thing we will need is an Event structure to hold information about events to be sent around: struct Event { int Type; int arg1, arg2; }; The Event structure is fairly straightforward. The Type variable will indicate what type of event the structure refers to. The two arg variables are simply arguments to be passed along with the structure. When this system was first being designed, the only events to be passed around were mouse movement events (needing an x and y coordinates as arguments), and key press events (needing the key being pressed as an argument). A possible improvement here is to pass around a pointer to an Event class instead of a structure. The Event class could then contain any amount of information, and derived classes could be created for different types of events. I choose a simpler option, mainly because the scope of our project didn't require such a complex solution. The Event.Type variable is used by event handlers to identify the event. I choose to define the type of events in an enumeration structure. Here is a simplified version of the structure used in our project to illustrate its function: enum EventType { // Mouse button events. Each of the next six events gets passed // the absolute position of the mouse E_MOUSELEFTBUTTONPRESS, E_MOUSELEFTBUTTONRELEASE, // Start new games E_NEWGAMEEASY, E_NEWGAMENORMAL, E_NEWGAMEHARD, // Game play related E_INCREMENTSCORE, E_PAUSEGAME, E_CONTINUEGAME, E_GAMEOVER, // Close the App E_APPCLOSE }; IEventHandlerThe next thing we need is an interface for event handlers. I defined a simple IEventHandler interface: class IEventHandler { public: virtual void EventHandler(const Event &e) = 0; }; Every class that wants to listen to events needs to inherent from IEventHandler, and needs to implement the EventHandler virtual function. A typical implementation of this method will have a switch statement (switching on the Event.Type variable), and will have a case statement for any events the class wants to handle. Here's a short example: void Foo::EventHandler(const Event &e) { switch (e.Type) { case E_NEWGAMEEASY: // handle creating a new easy game. break; case E_MOUSELEFTBUTTONPRESS: // handle mouse button being pressed break; default: break; } } Event DispatcherWe now need an EventDispatcher class. I implemented it as a singleton class. Explaining how to implement a singleton class and how it works is beyond the scope of this article. It suffices to say that there will only ever exist one instance of the class, and to obtain a pointer to that instance, one has to call EventDispatcher::Get(). For reference, the code implementing the singleton class is included along with the rest of the code at the end of the article. For our EventDispatcher class, we only need to declare two public functions (ignoring the singleton implementation part): class EventDispatcher { public: void RegisterHandler(IEventHandler *device); // Sends the event to all the devices registered to listen void SendEvent(int eventType, int arg1 = 0, int arg2 = 0); private: IEventHandler *_deviceList; }; These two functions are again fairly straightforward. RegisterHandler is called to add a new object (device) as a listener. SendEvent is the method used to dispatch events. The two arguments to the event are zero by default. The list of registered devices will be stored as a simple singly-linked list, so we need a IEventHandler pointer. This pointer will be initialized to null by EventDispatcher's constructor (not shown here). Now we'll go over the definitions of the two functions specified above: void EventDispatcher::RegisterHandler(IEventHandler *device) { device->SetNextHandler(_deviceList); _deviceList = device; } This method just adds the device to the device list (the device is added to the front of the list, but we are not concerned with the order of the devices in the list, since every device is sent every event. The user has to make sure that _deviceList is originally set to null in EventDispatcher's constructor. void EventDispatcher::SendEvent(int eventType, int arg1, int arg2) { Event e; e.Type = eventType; e.arg1 = arg1; e.arg2 = arg2; IEventHandler * curDevice = _deviceList; for (; curDevice; curDevice = curDevice->GetNextHandler()) { assert(curDevice != curDevice->GetNextHandler()); curDevice->EventHandler(e); } } This method creates a new event and dispatches to every device registered with the dispatcher. At this point, the reader should have noticed the use of two methods (GetNextHandler and SetNextHandler) belonging to the IEventHandler class, which I haven't discussed. Well, it's time to further develop the IEventHandler interface. Revising the IEventHandler interfaceAs it stands, the interface is still fairly simple. We now need to add certain functionality to handle creating the linked list, as well as a couple extra useful methods. Here is the final full code for the IEventHandler interface: class IEventHandler { public: virtual void EventHandler(const Event &e) = 0; // Mutator and selector IEventHandler * GetNextHandler(void) {return _nextHandler;} void SetNextHandler(IEventHandler *next) {_nextHandler = next;} IEventHandler() : _nextHandler(0) { EventDispatcher::Get()->RegisterHandler(this); } protected: void SendEvent(int eventType, int arg1 = 0, int arg2 = 0) { EventDispatcher::Get()->SendEvent(eventType, arg1, arg2); } private: IEventHandler *_nextHandler; }; I've added a pointer to the next handler (_nextHandler), as well as mutator and selector methods for this variable. As noted above, these two methods are used by the EventDispatcher to update, and cycle through the list of devices. Using a linked list allows for the list to vary in size without worrying about dynamically allocating memory, or knowing beforehand how many devices will listen for events. (Early versions of this system used these less elegant methods). I've also added a constructor for this interface. A class which implements the IEventHandler interface will automatically call IEventHandler's constructor when its own constructor is invoked. In this manner, one does not have to remember to register an object as a listener as IEventHandler's constructor does so automatically. A class listening to events doesn't need to know how the event dispatching is implemented, nor does it have to rely on a third party to register it with the dispatcher. The last method (SendEvent) is used to further abstract the EventDispatcher. Any class implementing this interface doesn't need to know anything about the dispatcher. It just calls its own SendEvent method when it needs to generate an event. Putting It All TogetherNow, I'll illustrate the usage of this system with a relatively short example. Assuming we have two classes A and B defined as follows: #include "IEventHandler.h" class A : public IEventHandler { public: void EventHandler(const Event &e) { switch (e.Type) { case E_NEWGAMEEASY: cout << "Class A handling E_NEWGAMEEASY event" << endl; SendEvent(E_INCREMENTSCORE); break; case E_PAUSEGAME: cout << "Class A handling E_PAUSEGAME event" << endl; break; } } }; class B : public IEventHandler { public: void EventHandler(const Event &e) { switch (e.Type) { case E_INCREMENTSCORE: cout << "Class B handling E_INCREMENTSCORE event" << endl; break; case E_PAUSEGAME: cout << "Class B handling E_PAUSEGAME event" << endl; break; } } }; We compile and execute the following piece of code: #include The output of the program will be: Main fct sending E_NEWGAMEEASY event Class A handling E_NEWGAMEEASY event Class A sending a E_INCREMENTSCORE event Class B handling E_INCREMENTSCORE event Main fct sending E_PAUSEGAME event Class B handling E_PAUSEGAME event Class A handling E_PAUSEGAME event ConclusionNow we have a simple Event Handling/Dispatching system. There are certain performance issues to consider when using this system. Spending too much time in the EventHandler methods basically slows down the overall event dispatching. One solution is to spawn off a thread for each event handler, but as the number of events and event handling objects increase, the overhead for creating and deleting threads will become significant. The best solution is to use the event handler to set flags or store important variables (such as mouse coordinates), and make use of that during the update/render loop. The use for this type of system is fairly obvious. Aside from handling mouse and keyboard events (and abstracting those events), general events can be created and shared across the program. Sound events can be generated for specific situations (such as a collision, or a menu selection). Different components of a game might need to be informed when a new game is created or when the game play is paused. The biggest advantage of this system, in my opinion, is that it allows a higher level of abstraction between modules. I've included all c++ header and source code I've mentioned in this article, and you're free to use it in any program you want. Any questions or comments, please email me. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|