Today, I implemented a configuration menu for Rktcr, which checks off one of my todo items for the impending release. The configuration menu certainly isn't the most exciting thing in the game (it may, in fact, be the least exiting), but I guess it's nice to have.
The menu is almost always accessible by pressing F10. One can use it to adjust sound volume, screen resolution (though this requires you to restart the game to see the changes applied), and most in-game key bindings.
I decided to leave menu and special key bindings out of this config screen (though they are still editable in rktcr.conf
); they make the menu much longer for very little marginal benefit.
Technical Notes
The config menu came together relatively painlessly because of three existing systems in Rktcr's codebase. This seems as good a place as any to briefly mention them.
Config
All user-configurable values in Rktcr are together in a single header, Config.hpp
.
By representing them explicitly in this way, I avoid any runtime errors associated with (e.g.) assuming the wrong type for a config variable, or mis-spelling a variable name used as the key to a hash of config values.
//Config.hpp:
#ifndef CONFIG_ITEM
#define CONFIG_ITEM(Type, Name, LocalName, Default) \
extern Type LocalName; \
const Type LocalName ## _default = Default;
#endif
namespace Config {
bool load();
void save();
namespace key {
CONFIG_ITEM(SDL_Scancode, "key.tutorial", tutorial, SDL_SCANCODE_F1);
CONFIG_ITEM(SDL_Scancode, "key.keys", keys, SDL_SCANCODE_F2);
/* ... and so on ... */
}
namespace sound {
CONFIG_ITEM(bool, "sound.enabled", enabled, true);
CONFIG_ITEM(float, "sound.music-volume", music_volume, 0.8f);
/* ... and so on ... */
}
} //namespace Config
In case you are unfamiliar with the token pasting operator (##
), it is used to glue things together during macro expansion.
For example, in expanding CONFIG_ITEM(bool, "sound.enabled", enabled, true);
,
the line const Type LocalName ## _default = Default;
becomes const bool enabled ## _default = true;
after substitution, and then is pasted to const bool enabled_default = true;
.
(And, thus, conveniently provides the default value for Config::sound::enabled
as the constant Config::sound::enabled_default
.)
The CONFIG_ITEM
macro keeps the header compact, and is used to good effect by Config.cpp
to actually implement config file loading and saving:
//A snippet of Config.cpp
#define CONFIG_ITEM(Type, Name, LocalName, Default) \
Type LocalName = Default; \
Parser< X_ ## Type > LocalName ## _parser(Name, &LocalName); \
#include "Config.hpp"
Before this snippet, Parser< T >
is defined and specialized for the various types Rktcr needs to read from and write to its config file.
Parser
objects also add themselves to a global list, so the load()
and save()
functions can look them up by name when reading/writing the config file.
Having all the configurable values in one header like this was very handy when writing the configuration menu, as all it needs to do is change the value and call Config::save()
to make the change persist.
I thought about using CONFIG_ITEM
to build the menu itself, but since I didn't want to include, e.g., all the debug settings, I decided it was easier to simply list the configurable values by hand.
Typesetting
This is probably not a big deal to those of you who use game engines, but actually getting text onto the screen can be a tricky thing.
Thankfully, I have a utility function typeset_sprite
, which takes a string to be typeset and a slew of parameters and builds a Sprite
(which is an abstract type of drawable thing).
//Typesetting for a configurable key:
TypesetParams params;
/* ... configure position, height, justification, alignment, color ... */
if (active) {
key_name = typeset_sprite("[press a key]", params);
} else {
key_name = typeset_sprite("{key:" + key_to_name(*key) + "}", params);
}
You'll notice also the use of an escape sequence, {key:}, which typesets the enclosed thing using the control font ("control"), rather than the standard Rktcr font ("blank check"). This escape sequence handling is particularly nice for writing help messages.
Overlays
Finally, actually getting the config menu displayed and receiving events was made significantly easier because Rktcr already has the notion of an Overlay
-- something that sits atop a game screen:
class Overlay {
public:
virtual ~Overlay() { }
virtual void handle_event(SDL_Event const &event, bool &is_finished) = 0;
virtual void update(float elapsed, bool &is_finished) = 0;
virtual void draw() = 0;
};
Since both team select mode and play mode already had overlay support, all I needed to add was code that sets the overlay when the appropriate key is pressed:
//During event handling:
if (e.type == SDL_KEYDOWN && e.key.keysym.scancode == Config::key::config) {
set_overlay(std::unique_ptr< Overlay >(new ConfigOverlay), false);
return;
}
No comments:
Post a Comment