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. |
|