/ User Interface

Options Menu

I may be running out of options, but running out isn't an option.

--- Mark Lawrence, Prince of Thorns

To further showcase our new state system, let us create an options menu to allow the user to change the fullscreen state, to set the game resolution and to chose his own keybindings.

Boost Preprocessor

Since getting the strings of enum variables is a bit tedious when done using a switch, I used the boost preprocessor to create enums with an automatic toString function:

#include <boost/preprocessor.hpp>
#include <boost/preprocessor/wstringize.hpp>

namespace input
{
	// use boost to define enums with strings

	// this first macro is used internally by the second one
#define ENUM_TO_STRING(r, data, elem)    \
    case elem : return BOOST_PP_WSTRINGIZE(elem);

	// this second macro first generates the enumeration and then generates a ToString function
	// that takes an object of that type and returns the enumerator name as a string
	// obviously this implementation requires that the enumerators map to unique values)
#define ENUM_WITH_STRING(name, enumerators)                \
    enum name {                                                               \
        BOOST_PP_SEQ_ENUM(enumerators)                                        \
    };                                                                        \
                                                                              \
    inline const wchar_t* getGameCommandAsString(name v)				      \
    {                                                                         \
        switch (v)                                                            \
        {                                                                     \
            BOOST_PP_SEQ_FOR_EACH(                                            \
                ENUM_TO_STRING,                                               \
                name,                                                         \
                enumerators                                                   \
            )                                                                 \
            default: return "[Unknown " BOOST_PP_WSTRINGIZE(name) "]";        \
        }                                                                     \
    }

	// define the game commands
	ENUM_WITH_STRING(GameCommands,	                    (Select)\
														(Back)\
														(ShowFPS)\
														(MoveLeft)\
														(MoveRight)\
														(MoveUp)\
														(MoveDown)\
														(Shift)\
														(Preview)\
														(nGameCommands))

	// define application events
	ENUM_WITH_STRING(Events,	                (StartApplication)\
												(PauseApplication)\
												(ResumeApplication)\
												(QuitApplication)\
												(SwitchFullscreen)\
												(WindowChanged)\
												(ChangeResolution)\
                                                (nApplicationEvents))
}



This might look weird and it obviously is a bit more work than just defining an enum, but the ease of use easily beats the added implementation time.

The first macro ENUM_TO_STRING is internally used by the second one. The second macro ENUM_WITH_STRING first generates the enumeration, then generates a toString function called getGameCommandAsString that takes a GameCommand as input and returns the enumerator name as a string, using the first macro to stringify enumeration elements into wide chars. Note that this implementation obviously requires the enumerators to map to unique values.

Defining the actual enumerations is easy enough, as you can see, but the real power comes from the ease of use:

First notice that I like to add a count at the end of each enum, for example nGameCommands to be able to easily loop through the enum:

// create action text layouts
	for (unsigned int i = input::GameCommands::Select; i < input::GameCommands::nGameCommands; i++)
		if (!addTextToActionTextLayoutList((input::GameCommands)i).wasSuccessful())
			return std::runtime_error("Critical error: Unable to create header action text layouts!");



And retrieving the string values of an enum is now as easy as it gets:

util::Expected<void> KeyMapMenuState::addTextToActionTextLayoutList(input::GameCommands gameCommand)
{
	std::wstring text = input::getGameCommandAsString(gameCommand);
				
	Microsoft::WRL::ComPtr<IDWriteTextLayout4> textLayout;
	util::Expected<void> result = d2d->createTextLayoutFromWString(&text, textFormat.Get(), (float)dxApp->getCurrentWidth(), 100, textLayout);
	if (!result.isValid())
		return result;
	actionTextLayouts.push_back(textLayout);

	// return success
	return { };
}



Just have fun with the new enums! Thanks boost!

Updated Buttons

First, please notice that I have updated the game buttons. The most important updates are that the buttons can now hold four animation cycles, one for each state, deselected, selected, clicked and locked, and that a button is now only clicked if it is not locked:

util::Expected<bool> AnimatedButton::click()
{
	if (state == ButtonStates::Locked)
		return true;

	if (nAnimationCycles > 2)
		sprite->changeAnimation(2);
	state = ButtonStates::Clicked;
	return onClick();
}




Options Menu State

To show the options menu, we will create a new state, the options menu state, where the user can set the fullscreen state of the game as well as specify its resolution:

Options Menu

I won't show the details for each little function in those states, but I will briefly talk about the lambda functions of each button as well as the functions that provide the main functionality.

Creating the main navigation Buttons

The Back Button

auto onClickBack = [this]() -> util::Expected<bool>
{
	this->isPaused = true;
	if (!dxApp->changeGameState(&UI::MainMenuState::createInstance(dxApp, L"Main Menu")).wasSuccessful())
		return std::runtime_error("Critical error: Unable to change game state to main menu!");
	return false;
};



This is an easy button to implement. We simply change the game state to the main menu, meaning that we delete all other states. Note that a return value of false notifies the observer stack of the input handler that the stack of states was changed.

The Save Button

auto onClickSave = [this]
{
	// write options to lua file
	dxApp->saveConfiguration(supportedModes[currentModeIndex].Width, supportedModes[currentModeIndex].Height, currentModeIndex, fullscreen);

	// activate desired screen resolution and fullscreen mode
	if (currentModeIndex != dxApp->getCurrentModeIndex())
		dxApp->changeResolution(currentModeIndex);

	if (fullscreen != wasInFullscreen)
	{
		wasInFullscreen = !wasInFullscreen;
		dxApp->toggleFullscreen();
	}

    return true;
};



When the user clicks on the save button, we write the current options to the LUA file on the hard drive and, if so desired, change the resolution and/or toggle the fullscreen state. Please note that in the above screenshot, the resolution buttons are grayed out, because the application does not have resizable graphics yet.

The Gamepad Button

auto onClickGamepad = [this]() -> util::Expected<bool>
{
	// open key map menu
	if (!dxApp->pushGameState(&UI::KeyMapMenuState::createInstance(dxApp, L"Key Map Menu")).wasSuccessful())
		std::runtime_error("Critical error: Unable to push key map menu to the state stack!");
	return false;
};



When the user clicks on the gamepad button, we push another state to the stack, the key map menu state, allowing the user to change the key bindings. Please note that pushing a state to the stack does not delete the previous states; they are simply paused and will be resumed later on.

Toggle Fullscreen

		auto onClick = [this]
		{
			this->fullscreen = !this->fullscreen;
			return true;
		};



There is nothing here, the main work is done when the save button is clicked.

Browse Supported Resolutions

To browse through all supported resolutions, we simply implement a left and right button, chosing the next or previous resolution:

auto onClickScreenResolutionLeftArrow = [this]
{
	if(this->currentModeIndex > 0)
		currentModeIndex--;
	return true;
};

auto onClickScreenResolutionRightArrow = [this]
{
	if (this->currentModeIndex < this->nSupportedModes-1)
		currentModeIndex++;
	return true;
};



To grey out the buttons once we are at one of the ends of the list, we update their state during the game update as follows:

util::Expected<void> OptionsMenuState::update(const double deltaTime)
{
    ... 
    
	if (currentModeIndex == 0)
	{
		menuButtons[1]->lock();
		if (currentlySelectedButton == 1)
			currentlySelectedButton = -1;
	}
	if (currentModeIndex == nSupportedModes - 1)
	{
		menuButtons[2]->lock();
		if (currentlySelectedButton == 2)
			currentlySelectedButton = -1;
	}

	// lock resolution buttons for now as we do not have resizable graphics yet
	menuButtons[1]->lock();
	menuButtons[2]->lock();

	// return success
	return {};
}




Key Bindings Menu

This menu gives an overview of the currently active key maps. Clicking on the gamepad symbol next to a key binding allows the user to select his own mapping:

Key Binding Menu

The Main Navigation Buttons

As you can see, the save button is gone. The new key bindings are saved when chosen during the next state. The gamepad button remains red, as it was clicked before the main menu state went into pause mode. The only button that is left is the

Back Button

auto onClickBack = [this]() -> util::Expected<bool>
{
	this->isPaused = true;
	if(!dxApp->popGameState().wasSuccessful())
		return std::runtime_error("Critical error: Unable to pop the key map menu!");
	return false;
};



This is easy. We simply pop the current state, the key binding menu, and resume the previous state, the main game options menu.

Pagination

We only show five game commands at once. To navigate through the game commands, we draw two pagination buttons with the following lambda functions:

auto onClickLeft = [this]
{
	if (currentPage > 0)
		this->currentPage--;
	return true;
};

auto onClickRight = [this]
{
	unsigned int n = 0;
	for (unsigned int i = 0; i < currentPage + 1; i++)
		n += keyBindingsPerPage;
	if (n < actionTextLayouts.size())
		currentPage++;
	return true;
};



The New Gamepad Buttons

When a small gamepad button next to a key binding is clicked, we enter a new state, the NewKeyBindingMenuState where the user can select a new chord for the specified action:

auto onClickGamepad = [this]
{
	return this->changeKeyBinding();
};

util::Expected<bool> KeyMapMenuState::changeKeyBinding()
{
	UI::NewKeyBindingState* bindNewKey = &UI::NewKeyBindingState::createInstance(dxApp, L"New Key Binding");

	this->isPaused = true;
		
	// get currently selected button modulo 9
	// 0-8: primary key binding
	// 9-17: secondary key binding
	bool primary = currentlySelectedButton > 8 ? false : true;
	unsigned int gameCommand = currentlySelectedButton % 9;

	std::wstring text = input::getGameCommandAsString((input::GameCommands)gameCommand);
		
	// get game commands associated with the selected game action
	std::vector<input::GameCommand*> commands;
	dxApp->getCommandsMappedToGameAction((input::GameCommands)gameCommand, commands);
		
	// get command (primary or secondary)
	if (primary)
	{
		// primary key binding
		if (!commands.empty())
		{
			// the command already exists -> change the associated chord
			bindNewKey->setCommandToChange(commands[0]);
			bindNewKey->setOldKeyBindingString(keyBindings1Texts[gameCommand]);
			bindNewKey->setGameCommand((input::GameCommands)gameCommand);
			dxApp->pushGameState(bindNewKey);
		}
		else
		{
			// the command does not exist -> create a new one
			input::GameCommand* gc = new input::GameCommand(text);
			dxApp->addNewCommand((input::GameCommands)gameCommand, *gc);
			bindNewKey->setCommandToChange(gc);
			bindNewKey->setOldKeyBindingString(L"not bound");
			bindNewKey->setGameCommand((input::GameCommands)gameCommand);
			dxApp->pushGameState(bindNewKey);
		}
	}
	else
	{
		// secondary key binding
		if (commands.size() > 1)
		{
			// the command does exist -> change it
			bindNewKey->setCommandToChange(commands[1]);
			bindNewKey->setOldKeyBindingString(keyBindings1Texts[gameCommand]);
			bindNewKey->setGameCommand((input::GameCommands)gameCommand);
			dxApp->pushGameState(bindNewKey);
		}
		else
		{
			// the command does not exist -> create it
			input::GameCommand* gc = new input::GameCommand(text);
			dxApp->addNewCommand((input::GameCommands)gameCommand, *gc);
			bindNewKey->setCommandToChange(gc);
			bindNewKey->setOldKeyBindingString(L"not bound");
			bindNewKey->setGameCommand((input::GameCommands)gameCommand);
			dxApp->pushGameState(bindNewKey);
		}
	}
	return true;
}



What is happening here is that we chose the appropriate game command, based on which gamepad button was clicked, and we then enter the new state with the appropriate game command from the key map to change.


New Key Bindings Menu

In this menu, the user can chose his own key bindings for the game commands:

New Key Bindings Menu

The Back Button

The back button is, as always, easy to implement:

auto onClickBack = [this]() -> util::Expected<bool>
{
	this->isPaused = true;
	if (!dxApp->popGameState().wasSuccessful())
		return std::runtime_error("Critical error: Unable to pop new key bindings state");
	return false;
};



We simply pop the current state from the stack and resume the previous state, the key bindings menu. Note that any new chords the user may have selected are discarded.

The Save Button

auto onClickSave = [this]() -> util::Expected<bool>
{
	// store the new key binding
	dxApp->saveKeyBindings();
	if (!dxApp->popGameState().wasSuccessful())
		return std::runtime_error("Critical error: Unable to pop new key binding state!");
	return false;	// notify stack change
};



This button is as easy as the back button. We simply save the key bindings using boost serialization as described in a previous tutorial on marshalling game commands and then pop the state and resume the key bindings menu.

Listening for User Input

Once the NewKeyBindingState starts, we tell the input handler to start listening for user input.

util::Expected<bool> NewKeyBindingState::onNotify(input::InputHandler* const ih, const bool listening)
{
	if (!listening)
	{
		// get active key map
		if (!isPaused)
			return handleInput(ih->activeKeyMap);
	}
	else
	{
		// got new chord
		ih->disableListening();
		setNewChord(ih->newChordBindInfo);
	}

	// return success
	return true;
}



The input handler then captures any user input that was not already mapped to a (different) game command and notifies us to save the new chord:

void InputHandler::update()
{
	// clear out any active bindings from the last frame
	bool isActive = false;
	activeKeyMap.clear();
		
	// loop through the map of all possible actions and find the active key bindings
	for (auto x : keyMap)
	{
		if (x.second->chord.empty())
			continue;

		// test chord
		isActive = true;
		for (auto y : x.second->chord)
		{
			if (getKeyState(y.keyCode) != y.keyState)
			{
				isActive = false;
				break;
			}
		}
		if (isActive)
			activeKeyMap.insert(std::pair<GameCommands, GameCommand&>(x.first, *x.second));
	}

	// if there is an active key map
	if (!activeKeyMap.empty())
	{
		// notify the currently active game states to handle the input
		notify(this, false);	// false: normal game input; was not listening to specially requested user input
	}
	else
	{
        if (listen)
		{
			// we are listening to specially requested user input
			newChordBindInfo.clear();

			// give the user the ability to "unbind" a key by pressing the "ESCAPE" key
			if (isPressed(VK_ESCAPE))
			{
				listen = false;			// stop listening ; produce normal input again
				notify(this, true);		// true: was listening to specially requested user input
				return;					// all done
			}

			// now loop through all possible keys and check for changes
			for (unsigned int i = 0; i < 256; i++)
			{
				// we don't care which one of the shift or ctrl keys was pressed
				if (i >= 160 && i <= 165)
					continue;

				// push the keys the user is holding down to the chord
				if (getKeyState(i) == KeyState::StillPressed)
				{
					newChordBindInfo.push_back(BindInfo(i, getKeyState(i)));
					continue;
				}
					
				// now add those keys that have been pressed
				if (kbm->currentState[i] != kbm->previousState[i]	// only listen to key state changes
					&& getKeyState(1) != KeyState::JustReleased)	// ignore when the left mouse button is released (as the menu is accessed via left mouse button click)	
					newChordBindInfo.push_back(BindInfo(i, getKeyState(i)));
			}

			// if there is a new chord, we have to make sure that we are not overwriting an already existing chord
			if (!newChordBindInfo.empty())
			{
				// check for new chord to not overwrite other commands
				bool newChord = true;
				for (auto x : keyMap)
				{
					// no chord at all -> continue
                    if (x.second->chord.empty())
						continue;

					// different sizes -> can't be the same chord -> continue
					if (x.second->chord.size() != newChordBindInfo.size())
						continue;
					else
					{
						// check all key bindings
						bool allTheSame = true;
						for (unsigned int i = 0; i < newChordBindInfo.size(); i++)
						{
							if (x.second->chord[i].keyCode != newChordBindInfo[i].keyCode)
							{
								// the keys are different
								allTheSame = false;
								break;
							}
							else
							{
								// the keys are the same; check for their states
								if (x.second->chord[i].keyState != newChordBindInfo[i].keyState)
								{
									// the states are different -> check for pressed <-> released mismatch
									if (x.second->chord[i].keyState == KeyState::JustPressed && newChordBindInfo[i].keyState == KeyState::JustReleased)
									{
										// do nothing
										continue;
									}
									allTheSame = false;
									break;
								}
							}
						}
						if (allTheSame)
							newChord = false;
					}
				}

					
				if (!newChord)
					// the just pressed chord is already bound to a command -> clear and restart
					newChordBindInfo.clear();
				else
				{
					// we have a new chord ; notify if at least one of the keys was released, else: continue
					// this is necessary to capture multipe key presses, such as "CTRL" + A
					bool sendNotification = false;
					for (auto x : newChordBindInfo)
						if (getKeyState(x.keyCode) == KeyState::JustReleased)
						{
							sendNotification = true;
							listen = false;
							break;
						}
					if (sendNotification)
					{
						for (auto& x : newChordBindInfo)
						{
							if (x.keyState == KeyState::JustReleased)
								x.keyState = KeyState::JustPressed;
						}
						notify(this, true);
					}
				}
			}
		}
	}
}



Associating the new chord with the game command is done as follows:

util::Expected<void> NewKeyBindingState::setNewChord(std::vector<input::BindInfo>& bi)
{
	// disable listening
	this->keySelected = true;

	// store new chord
	newChord = bi;

	// show new chord
	std::wostringstream text;
	if (newChord.empty())
		text << "New Key" << std::endl << L"not bound";
	else
	{
		text << "New Key" << std::endl;
		for (unsigned int i = 0; i < bi.size(); i++)
		{
			if (i != bi.size() - 1)
				text << dxApp->getKeyName(bi[i].getKeyCode()).get() << L" + ";
			else
				text << dxApp->getKeyName(bi[i].getKeyCode()).get();
		}
	}

    ...
	
    // change game command
	commandToChange->setChord(newChord);
	return true;
}




And that's it, we now have a fully functional game menu.

You can download the source code from here


In the next tutorial, although I previously said that we won't talk about HUDs yet, we will at least modify our state stack to allow HUDs, as the HUD is drawn over the game state without pausing the game.


References

(in alphabetic order)

  • Game Programming Algorithms, by Sanjay Madhav
  • Game Programming Patterns, by Robert Nystrom
  • Microsoft Developer Network (MSDN)
  • Tricks of the Windows Game Programming Gurus, by André LaMothe
  • Wikipedia

Art


<< Pushing Buttons with Lambda Functions Enabling HUDs >>

Options Menu
Share this