• entries
16
12
• views
27678

# "Gamedev Lego": Putting all the parts together into a playable game using an event system

1802 views

Hi everyone!

So, again, it's been a couple of weeks since my last entry. Moving on from where we were, to where we are now, I am now in a position to start putting together various subsystems and linking them together to form a flowing gameplay experience.

In previous journal entries I have discussed writing a spritefont implementation, and writing various bits and bobs for animation etc.

Today I will cover my event system which I have used to link together the component parts such as the menu, boss (tutorial character and all-round plot device) and the gameplay portion proper. The event system is at its simplest level a base class from which other classes may be derived, named "Action":

class Action{private: // True if the action is currently repeating, e.g. a held key bool keydown; // The name of the action, e.g. "Camera/Up" std::string name;protected: // Creator class FireworkFactoryWindow* creator;public: // Instantiate a new action. Creating an action inserts it into a map // of named actions where it can be located and bound to key presses. Action(FireworkFactoryWindow* parent, const std::string &actionname); // Destructor automatically removes the action from the named actions map ~Action(); // Called when repeated execution of the action is possible, e.g. key held down virtual void ExecuteAction() = 0; // Returns the name of the event std::string GetName(); // Returns true if the key bound to the action is currently held down bool IsPressed(); // Called once when the button bound to the action is first depressed virtual void Pressed(); // Called once when the button bound to the action is finally released virtual void Released();};As Action objects are instantiated, they are inserted into a global std::multimap of Actions, each of which has a sensible name which is used to identify it. Each action may be triggered manually (by locating its object and calling its methods) or by associating it with an input, e.g. a keyboard button or a mouse button. Note that although the methods are called "Pressed()", "Released()", and "ExecuteAction()" they do not neccessarily have to map to a physical input, and derived forms may implement derivation-specific methods which may be useful. For example all Actions who's names start with "Audio/" could contain for example a "SoundEffectFinished()" method...

The general idea of action objects is to disassociate behaviours with the triggers for those behaviours. Many games will directly plug the behaviour of the space bar to the fire action, for example. This is all well and good, until you later come along and want to run a demo sequence, or multiplayer, or perhaps just want to remap keys.

In Firework Factory, a seperate input buffer (fed from the windows message loop thread) populates a buffer of key presses, which are then used to dispatch events to Actions:

* This function handles input coming in from windows message loop, but buffered into our own * buffer to deal with at a time when we want to deal with it. */void FireworkFactoryWindow::Input(double frameTime){ /* We process a maximum of ten events per frame, so that all players have a constant known * input rate for fairness. Else, faster machines respond quicker. */ const UINT32 MaxEventsPerUpdate = 10; UINT32 EventsRead = 0; BufferedKey bk(0, VK_NONAME, KE_NOTHING); Action* k = nullptr; /* Iterate the buffer removing all keypresses that exist and processing them */ do { bk = keybuf.GetNext(); /* Dispatch each different event type to the right method */ switch (bk.GetEvent()) { /* Keyboard key pressed */ case KE_KEYDOWN: { ActionListKeys eventlist = keymap->Get(bk.GetKeycode()); for (auto k = eventlist.first; k != eventlist.second; ++k) { /* Note: We call Action() here too on key press, because if the * key is both pressed and released within the same set of buffered * events, Action() would not be called AT ALL otherwise. This would be * technically wrong. */ k->second->Pressed(); k->second->ExecuteAction(); } } break; /* Keyboard key released */ case KE_KEYUP: { ActionListKeys eventlist = keymap->Get(bk.GetKeycode()); for (auto k = eventlist.first; k != eventlist.second; ++k) k->second->Released(); } break; } EventsRead++; } while (bk.GetEvent() != KE_NOTHING && EventsRead < MaxEventsPerUpdate); /* Iterate all active command handlers triggering the Action() method if their * key is currently held. There is a special case for this above for keys which are * not held long enough to cross two frames. */ keymap->DoActions();}As you can see, all input events are rate limited and tied to a fixed 16ms (70fps) clock, and handled at a set position in the code. This ensures that the input system does not run at different speeds on different PCs. The GameWindow::Input() method used here is called in a similar way to GameWindow::Update() but is seperated purely for readability. Any key may be mapped to multiple actions, and any action name may be claimed by multiple objects. At any time the key mappings may be changed by calling the InputMap class's SetNew() method, or by simply instantiating the InputMap class:

keymap->SetNew(L"Keymaps/Game.txt");There is also a method to insert and remove individual key mapping entries, which is not used any more from the public interface. These keymap text files are stored within the game's main archive file, and are a simple tab seperated list of key name and action name, e.g.

VK_A Camera/LeftTo map the names (e.g. VK_A) to an actual platform specific keycode, I programattically generated a huge header file, which contains the name and platform specific keycode for each possible key, plus a description for the UI:

#include typedef std::pair KeyDetail;typedef std::map KeyNames;KeyNames KeyNameList = KeyNames(boost::assign::map_list_of ("VK_ADD", std::make_pair(VK_ADD, "Numpad +")) ("VK_ATTN", std::make_pair(VK_ATTN, "Attn")) ("VK_BACK", std::make_pair(VK_BACK, "Backspace")) ("VK_CANCEL", std::make_pair(VK_CANCEL, "Break")) ("VK_CLEAR", std::make_pair(VK_CLEAR, "Clear"))[...] ("VK_VOLUME_DOWN", std::make_pair(VK_VOLUME_DOWN, "Volume Down")) ("VK_VOLUME_MUTE", std::make_pair(VK_VOLUME_MUTE, "Volume Mute")) ("VK_VOLUME_UP", std::make_pair(VK_VOLUME_UP, "Volume Up")) ("VK_XBUTTON1", std::make_pair(VK_XBUTTON1, "X Button 1")) ("VK_XBUTTON2", std::make_pair(VK_XBUTTON2, "X Button 2")));We now have a facility where any particular system may be called independently of its association with what the user is doing. We use this for good effect while the menu is on screen. Although the game is paused, we can continually call the Camera/Left event to rotate the playing field below the translucent menu (see the video below for this effect in the flesh):

void MainMenu::Update(double frameTime){ Menu::Update(frameTime); ActionList al = creator->GetInputMap()->Get("Camera/Left"); for (auto it = al.first; it != al.second; ++it) { if (auto cla = dynamic_cast(it->second)) cla->ExecuteAction(); }}The dynamic_cast isn't strictly required in this case, as all instances of Action must implement the ExecuteAction() method. This would be used however in a templated "CallAction" method, to call an action class given it's type and action name. Similarly, we can do the same to trigger a "quit the game" action from anywhere. We could attach a dialog box to this, if we wanted, or perform any other cleanup behaviour such as an autosave, and it would be triggered from anywhere that calls the event:

case MO_QUIT:{ ActionList al = creator->GetInputMap()->Get("Gameplay/Quit"); for (auto it = al.first; it != al.second; ++it) it->second->Pressed();}As promised, here is a video demonstrating the game so far, with all of this humming away quitely in the background. As you can see, it is now appearing much more like a game, and less like a bunch of loosely related parts, our lego kit is coming together to form the whole:

There are no comments to display.