My input system in Ablockalypse is quite simple and also very powerful.
I'm using a multimap to store the key-presses and their associated modifiers as well as the command to be executed.
mKeyBindings2.emplace(SDL_SCANCODE_EQUALS,
std::make_pair(KMOD_NONE,
ley::Command::increaseVolume));
mKeyBindings2.emplace(SDL_SCANCODE_EQUALS,
std::make_pair(KMOD_RSHIFT,
ley::Command::increaseTransparency));
The input system also has a custom delay and repeat mechanism. I have two timers associated with every key. I can check which modifiers are pressed when the key is initially pressed as well as when the key needs to be automatically repeated. I also check which modifiers are still pressed when its time to repeat the key.
At the end of the input phase of the game loop I check the timers to see if any additional input needs to be added to the command queue.
auto check_timers = [this, commandQueuePtr, alt_mod, bindings2,
buttonBindings2, get_mod_bitmask,
bindingsNewType]() {
for(auto &[key, keyPress] : mKeysPressed) {
keyPress->getDelayTimerPtr()->runFrame(false);
keyPress->getRepeatTimerPtr()->runFrame(false);
//if the delay timer has expired and
//the repeat timer has expired
if(keyPress->getDelayTimerPtr()->hasExpired()
&& keyPress->getRepeatTimerPtr()->hasExpired()) {
ley::Command command = lookupCommand2(
(SDL_Scancode)key,
get_mod_bitmask(),
bindingsNewType);
//don't repeat the enter command.
if(command != ley::Command::enter) {
commandQueuePtr->push(command);
}
//reset the repeat timer
keyPress->getRepeatTimerPtr()->reset();
}
}
...
};
This is my SDL_KEYDOWN event. Notice how it skips over key repeats. This is where I start to handle the delay and repeat timers.
case SDL_KEYDOWN:
if(!event.key.repeat) {
SDL_Scancode pressedKey = event.key.keysym.scancode;
Uint16 pressedModifiers = event.key.keysym.mod;
//reset the timers if this key was previously not pressed
if(mKeysPressed.find(pressedKey) == mKeysPressed.end()) {
//skip modifiers
if(pressedKey <= SDL_SCANCODE_LCTRL
|| pressedKey >= SDL_SCANCODE_RGUI)
{
mKeysPressed.insert({pressedKey,
std::make_unique<InputPressed>(pressedModifiers)});
mKeysPressed[pressedKey]->getDelayTimerPtr()->reset();
mKeysPressed[pressedKey]->getRepeatTimerPtr()->reset();
}
}
//command = lookupCommand(pressedKey, bindings2);
command = lookupCommand2(pressedKey, pressedModifiers, bindingsNewType);
if(command == ley::Command::fullscreen) {
fullscreen = !fullscreen;
}
else {
// push on repeatable commands
commandQueuePtr->push(command);
}
}
Here in the main controller is where I handle the commands after calling pollEvents().
/**** GET INPUT ****/
//pollEvents updates running and full screen flags
mInputSystem.pollEvents(fs, mGm->getButtonBindingsPtr(), mGm->getKeyBindingsPtr(), mGm->getKeyBindingsPtr2(), &mCommandQueue, mGameStateMachine.activeUIElement(),
[this](ley::Command c) {mGameStateMachine.activeUIElement()->onKeyDown(c == ley::Command::backspace ? ley::Character::backspace : ley::Character::none);});
ley::Command command = ley::Command::none;
if(!mCommandQueue.empty()) {
command = mCommandQueue.front();
mCommandQueue.pop();
}
/**** INPUT PROCESSING ****/
if(fs != mVideoSystem->fullScreen()) {
mVideoSystem->setFullScreen(fs);
}
processCommands(command);
mGameStateMachine.update(command);
processStates(command);
Then I execute the commands by matching the command to some specific logic. mGm-> is a pointer to the GameModel.
void ley::GameController::processCommands(ley::Command command) {
if(command == ley::Command::nextSong) {
playNext();
}
if(command == ley::Command::decreaseVolume) {
mGm->audio()->decreaseVolume();
}
if(command == ley::Command::increaseVolume) {
mGm->audio()->increaseVolume();
}
/**** DEBUG INPUT PROCESSING ****/
if(command == ley::Command::debugkeystoggle) {
mGm->debugCommandsToggle();
}