Monday, April 7, 2025

My input system for Ablockalypse.

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();
}