How to make a robust input system for a game?

Hi Everyone,

I could use some help putting together some psudo code for an input system that supports multiple keys/controller buttons pressed at the same time as well as delay and repeat. At first I checked for the keydown events and then set a command based on the keypress, but this only captures the last key pressed. I then added a command queue which works a little better but still only sends a command on a keypress. I would like this input system to handle key repeats as well.

Currently I have something that is fairly elementary. Can everyone help me learn how to put this together better?

//pollEvents is called in every iteration of the game loop.

ley::Command ley::Input::pollEvents(
bool& fullscreen, 
ley::KeyBindings* bindings, 
std::queue<ley::Command>* commandQueuePtr, 
ley::TextEntry* te, 
const std::function<void(ley::Command c)>& function //function pointer for handling textentry.
) 
{
    
    SDL_Event event;
    ley::Command command = ley::Command::none; //direction for this frame;

    while(SDL_PollEvent(&event))   {    //SDL_PollEvent calls pumpevents.
        const Uint8 *state = SDL_GetKeyboardState(NULL);
        switch (event.type)     {       
            case SDL_QUIT:         
                command = ley::Command::quit;
                break;

            case SDL_TEXTINPUT:
                /* Add new text onto the end of our text */
                te->onTextInput(event.text.text);
                break;
            case SDL_TEXTEDITING:
                break;
            case SDL_KEYDOWN:

                
                //anyInputsMatch will handle cases where multiple keys can be used for a single command
                if (anyInputsMatch(state, &bindings->debugkeystoggle.second)) {
                    command = bindings->debugkeystoggle.first;
                    SDL_Log(SDL_GetScancodeName(SDL_SCANCODE_F12));
                    commandQueuePtr->push(command);
                }
                if (state[SDL_SCANCODE_C]) {
                    command = ley::Command::debugclear;
                    commandQueuePtr->push(command);
                }
                if (state[SDL_SCANCODE_F]) {
                    command = ley::Command::debugfill;
                    commandQueuePtr->push(command);
                }
                //Full screen mode
                if ((state[SDL_SCANCODE_LALT] && state[SDL_SCANCODE_RETURN])
                    ||(state[SDL_SCANCODE_RALT] && state[SDL_SCANCODE_RETURN])) { 
                        fullscreen = !fullscreen; 
                }
                //Rotate Block counter clockwise
                if (anyInputsMatch(state, &bindings->cclockwise.second)) {
                    command = bindings->cclockwise.first; 
                    commandQueuePtr->push(command);
                }
                if(state[SDL_SCANCODE_RETURN] && !(state[SDL_SCANCODE_LALT] || state[SDL_SCANCODE_RALT])) {
                    command = ley::Command::enter;
                    commandQueuePtr->push(command);
                }

                // etc ...
                
                function(command); //handles text entry.

                break;

            case SDL_KEYUP :

                break;

            case SDL_CONTROLLERBUTTONDOWN :

                switch (event.cbutton.button) {
                    case SDL_CONTROLLER_BUTTON_A:
                        command = ley::Command::enter;
                        break;
                    case SDL_CONTROLLER_BUTTON_START:
                        command = ley::Command::pause;
                        break;
                    case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
                        command = ley::Command::left;
                        break;
                    case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
                        command = ley::Command::right;
                        break;
                    case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
                        command = ley::Command::down;
                        break;
                    case SDL_CONTROLLER_BUTTON_DPAD_UP:
                        command = ley::Command::cclockwise;
                        break;
                    case SDL_CONTROLLER_BUTTON_X:
                        command = ley::Command::clockwise;
                        break;
                    case SDL_CONTROLLER_BUTTON_Y:
                        command = ley::Command::cclockwise;
                        break;
                    case SDL_CONTROLLER_BUTTON_B:
                        command = ley::Command::space;
                        break;
                    case SDL_CONTROLLER_BUTTON_BACK:
                        command = ley::Command::quit;
                        break;
                    // Add more cases for other buttons...
                }
                SDL_Log("Controller was pressed");
                break;

            default:
            break;
        }
    }

    return command;  //not really used anymore now that there is a command queue.
}

Thanks for your help,

Electrosys.

p.s. if your interested in seeing this in its full contextual glory you can look at my project on github. I have a tetromino game on steam. sdl2-blocks/Input.cpp at master Ā· electrosy/sdl2-blocks Ā· GitHub

There is no ā€œPerfect Answerā€ for this question, it depends on the use-case. Hereā€™s a book that I would recommend.

Hereā€™s a simple tank-driver example using WASD and/or arrow key inputs, where left and right steer the tank and can cancel each other out, and up and down are gas and reverse, and also cancel each other. (It uses simple flag-states to catch multiple key presses)

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>

class GameStates
{
	public:
	GameStates()
	{
		w = false;
		a = false;
		s = false;
		d = false;
		space = false;
		running = true;
	}

	public:
	bool w;
	bool a;
	bool s;
	bool d;
	bool space;
	bool running;
};

class Player
{
	public:
	Player(SDL_Renderer * screen, GameStates * gameFlags)
	{
		renderer = screen;
		states = gameFlags;
		setPos(10, 10);
		setSize(40, 40);
		speed = 7.0f;
		angle = 90.0f * 0.0175f;
		angleStep = 5.0f * 0.0175f;
		boundary = {0, 0, 900, 900};
	}
	void setPos(float x, float y)
	{
		pos.x = x;
		pos.y = y;
	}
	void setSize(float w, float h)
	{
		pos.w = w;
		pos.h = h;
	}

	void update()
	{
		if(states->w)
		{
			pos.y -= speed * SDL_sin(angle);
			pos.x += speed * SDL_cos(angle);
		}
		if(states->s)
		{
			pos.y += speed * SDL_sin(angle);
			pos.x -= speed * SDL_cos(angle);
		}
		if(states->a)
		{
			angle += angleStep;
		}
		if(states->d)
		{
			angle -= angleStep;
		}

		if(!SDL_HasRectIntersectionFloat(&pos, &boundary))
		{
			setPos(400, 400);
		}
	}

	void draw()
	{
		SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
		SDL_RenderRect(renderer, &pos);
		float lineStartX = pos.x + pos.w/2.0f;
		float lineStartY = pos.y + pos.h/2.0f;
		SDL_RenderLine(renderer , lineStartX, lineStartY, lineStartX + 50.0f * SDL_cos(angle), lineStartY - 50.0f * SDL_sin(angle));
	}

	public:
	SDL_Renderer * renderer;
	SDL_FRect pos;
	SDL_FRect boundary;
	GameStates * states;
	float speed;
	float angle;
	float angleStep;

};

int main()
{
	SDL_Init(SDL_INIT_VIDEO);
	IMG_Init(IMG_INIT_PNG);
	SDL_Window * win = SDL_CreateWindow("title", 900, 900, SDL_WINDOW_RESIZABLE);
	SDL_Renderer * screen = SDL_CreateRenderer(win, 0);
	SDL_SetRenderVSync(screen, 1);
	GameStates states;
	Player player(screen, &states);
	
	while(states.running)
	{
		SDL_Event ev;
		while(SDL_PollEvent(&ev))
		{
			switch(ev.type)
			{
				case SDL_EVENT_KEY_DOWN:
					switch(ev.key.key)
					{
						case SDLK_UP:
						case SDLK_W:
							states.w = true;
							break;
						case SDLK_LEFT:
						case SDLK_A:
							states.a = true;
							break;
					
						case SDLK_DOWN:
						case SDLK_S:
							states.s = true;
							break;
						case SDLK_RIGHT:
						case SDLK_D:
							states.d = true;
							break;
						case SDLK_RETURN:
						case SDLK_SPACE:
							states.space = true;
							break;
						case SDLK_ESCAPE:
							states.running = false;
							break;
					}
					break;
				case SDL_EVENT_KEY_UP:
					switch(ev.key.key)
					{
						case SDLK_UP:
						case SDLK_W:
							states.w = false;
							break;
						case SDLK_LEFT:
						case SDLK_A:
							states.a = false;
							break;
						case SDLK_DOWN:
						case SDLK_S:
							states.s = false;
							break;
						case SDLK_RIGHT:
						case SDLK_D:
							states.d = false;
							break;
						case SDLK_SPACE:
							states.space = false;
							break;
					}
					
					break;
				case SDL_EVENT_QUIT:
					states.running = false;
					break;
			}
		}
		player.update();
		SDL_SetRenderDrawColor(screen, 10, 10, 10, 255);
		SDL_RenderClear(screen);
		player.draw();
		SDL_RenderPresent(screen);
	}
	IMG_Quit();
	SDL_Quit();
}

1 Like

Thanks for taking a look and offering some suggestions. I have read some of Game Programming Patterns. Iā€™m glad you pointed this book out as its really a great book and I need to spend some time reading the rest of it.

I like that you have created a button state that is set to true on keydown and false on keyup. I was thinking about doing something similar. Basically having a button object with the state and also a delay timer and a repeat timer. Iā€™ll switch the flag and reset the timers as needed on the keydown and keyup events. Iā€™ll then check at the end of the input method which buttons are pressed and check the timer and then send the commands based on the state of the buttons.

So far instead of creating an object Iā€™m just using some basic STL data structures like this:

std::map<Uint8, std::tuple<bool, ley::Timer, ley::Timer>> mKeysPressed;

Then I can initialize the structure like this:

for(Uint8 i = 0; i < UINT8_MAX; ++i) {
mKeysPressed.insert({i, std::make_tuple(false, ley::Timer(1000, {0, 0, 0, 0}), ley::Timer(1000, {0, 0, 0, 0}))});
}

I have a habit of trying to use STL data structures like these instead of creating objects because they seem more convenient. In this case it may actually make sense to use a custom object because it may be easier to reason about especially when I start adding some helper methods.

Will see how it goesā€¦

There are two main ways to handle input. The first is to wait for input events and process them on the fly, and the second is to query the state of input devices. In my engine, I use the second method, and Iā€™ll describe it briefly. Just in case, my engine performs a fixed number of logic updates per second (it doesnā€™t use delta), and Iā€™ll refer to this as frames.


Each available input device (mouse, keyboard and gamepads) is represented by a dedicated data structure. It contains data about the deviceā€™s available triggers and other properties. Some data is immutable (e.g. device name), while others may change their state over time (e.g. key press state). All data that may change over time is stored twiceā€”the state from the previous frame and the current frame.

Updating a given frame works by first processing the entire SDL event queueā€”data from input events is passed to the appropriate structures representing input devices to update their state. When the event queue is completely processed (i.e. emptied), the game logic itself is updated.

Because the SDL event queue is processed in its entirety at the beginning of a frame, the game logic update process does not operate on SDL events. The game logic does not wait for input events, but simply checks the state of structures representing input devices. Thanks to this, no matter what game modules need to check the input state, they can do it themselves, any number of times and anywhere in the game code. When it comes to the mentioned game modules, I mean e.g. the hero control code, UI updater, etc. Each module can handle input independently, without the need to pass SDL events e.g. in parameters (which simplifies everything significantly).

Because input device structures have a complete set of data (including the state from the previous frame), I can not only check what the input looks like in the current frame, but also what it looked like in the previous one, and thus determine whether it has changed (e.g. whether a given gamepad button was recently pressed or released). All I need to do is query the structure of a specific device for specific data.


The structures representing input devices are at the lowest level of abstraction and are a direct representation of the state of the input devices. On top of these device structures I built an input mapper. This mapper is an intermediary between the logic updater and the input device data structures and operates on actions that can be performed by the player (e.g. move, jump, hit, etc.). The game logic does not know which devices are assigned to a given player and which ā€œtriggersā€ are assigned to specific actions. It asks the mapper whether a given action is being performed (e.g. move), while the mapper checks which device is assigned to this player, which trigger is assigned to the move action (four keys, mouse movement, gamepad stick, etc.), and then based on the trigger state determines whether a given action is being performed and returns the appropriate data to the logic. So the game logic operates on a small set of available actions, and the mapper deals with translating the input device data into the action state.

Fun factā€”my mapper lets the player assign any number of input devices of any type to a given player, and also assign any trigger (key; mouse button; button, hat direction, gamepad axis etc.) to any action (e.g. move, jump, hit, etc.). This allows the player to play however they want and how they wantā€”they can play with just the keyboard, keyboard + mouse, gamepad + mouse, two gamepads (one in the left and one in the right hand), or even a mouse, keyboard, and gamepad at the same time (if thatā€™s what they want).

Of course, the input mapper is not mandatory to use when handling input. If a given game module does not need a mapper, it can check the state of input devices directly. An example is the UI updaterā€”it checks the state of devices directly (e.g. the state of the mouse, keyboard, and specific gamepad triggers) to handle the main game menu. However, it uses the mapper during the update of the container during gameplay. Another example is debug modeā€”a debug build can use the F12 key to show additional (internal) engine data, so it checks the state of this key directly by querying the structure with the keyboard state.


The above is a very short description of the input handling system in my engine. It should also be noted that my engine is specific (it works similarly to games for retro consoles like NES or SNES), which may be unusual, not commonly used in the gamedev industry. So Iā€™m not encouraging you to use this methodā€”Iā€™m just describing how input handling might work. In the end, it all depends on your design requirements.

1 Like

I have modified the previous example to allow for assignment of keyboard and controller buttons.
I also laid the early groundwork for run-time reassignment of those buttons [connectKey() and disconnectKey()], but I didnā€™t want to implement that whole GUI and scene switching as the code is already at 253 lines.

This current version has two players and allows for hot-pluggable game controllers.
One major concession is that each player class now manages itā€™s own state rather than attempting a single global state class.

I would argue that this is a pretty good all-rounder event handling setup for most small to mid-sized games. On the other hand, it may be a step in the wrong direction if you wanted to work in a DOD (Data Oriented Design) manner. [Iā€™m half joking, but that probably does matter to someone out there]

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <unordered_map>

class PlayerStates
{
	public:
	PlayerStates()
	{
		for(int i = 0; i < 256; i ++)
		{
			connectKey((SDL_Keycode) i, &ignore); 
		}
		for(int i = SDLK_CAPSLOCK; i < SDLK_ENDCALL; i ++)
		{
			connectKey((SDL_Keycode) i, &ignore); 
		}
	}

	bool & connectKey(SDL_Keycode key, bool * actor)
	{
		keyStates[key] = actor;
		return *actor;
	}

	void disconnectKey(SDL_Keycode key)
	{
		keyStates[key] = &ignore;
	}

	void toggle(SDL_Keycode key)
	{
		*(keyStates[key]) = !(*keyStates[key]);
	}

	void turnOn(SDL_Keycode key)
	{
		*(keyStates[key]) = true;
	}

	void turnOff(SDL_Keycode key)
	{
		*(keyStates[key]) = false;
	}

	public:
	std::unordered_map <int, bool *> keyStates;
	bool ignore;
};

class Player
{
	public:
	Player(SDL_Renderer * screen)
	{
		renderer = screen;
		setPos(10, 10);
		setSize(40, 40);
		speed = 7.0f;
		angle = 90.0f * 0.0175f;
		angleStep = 5.0f * 0.0175f;
		boundary = {0, 0, 900, 900};
		gamepadID = -1;
	}

	void setPos(float x, float y)
	{
		pos.x = x;
		pos.y = y;
	}

	void setSize(float w, float h)
	{
		pos.w = w;
		pos.h = h;
	}

	void assignKey(int keycode, bool *actor)
	{
		*actor = false;
		states.connectKey(keycode, actor);
	}

	void keyUp(int val)
	{
		states.turnOff(val);
	}

	void keyDown(int val)
	{
		states.turnOn(val);
	}

	void update()
	{
		if(up)
		{
			pos.y -= speed * SDL_sin(angle);
			pos.x += speed * SDL_cos(angle);
		}
		if(down)
		{
			pos.y += speed * SDL_sin(angle);
			pos.x -= speed * SDL_cos(angle);
		}
		if(left)
		{
			angle += angleStep;
		}
		if(right)
		{
			angle -= angleStep;
		}
		if(!SDL_HasRectIntersectionFloat(&pos, &boundary))
		{
			setPos(400, 400);
		}
	}

	void draw()
	{
		SDL_SetRenderDrawColor(renderer, 255, 100, 100, 255);
		SDL_RenderRect(renderer, &pos);
		float lineStartX = pos.x + pos.w/2.0f;
		float lineStartY = pos.y + pos.h/2.0f;
		SDL_RenderLine(renderer , lineStartX, lineStartY, lineStartX + 50.0f * SDL_cos(angle), lineStartY - 50.0f * SDL_sin(angle));
	}

	public:
	SDL_Renderer * renderer;
	SDL_FRect pos;
	SDL_FRect boundary;
	PlayerStates states;
	SDL_JoystickID gamepadID;
	SDL_Gamepad * gamepad;
	float speed;
	float angle;
	float angleStep;
	bool up, down, left, right;
};

int main()
{
	SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD);
	IMG_Init(IMG_INIT_PNG);
	SDL_Window * win = SDL_CreateWindow("title", 900, 900, SDL_WINDOW_RESIZABLE);
	SDL_Renderer * screen = SDL_CreateRenderer(win, 0);
	SDL_SetRenderVSync(screen, 1);

	SDL_Log("Adding mappings of %d remotes.", SDL_AddGamepadMappingsFromFile("gamecontrollerdb.txt"));
	Player player1(screen);
	player1.assignKey(SDLK_W, &(player1.up));
	player1.assignKey(SDLK_S, &(player1.down));
	player1.assignKey(SDLK_A, &(player1.left));
	player1.assignKey(SDLK_D, &(player1.right));
	player1.assignKey(SDL_GAMEPAD_BUTTON_SOUTH, &(player1.up));
	player1.assignKey(SDL_GAMEPAD_BUTTON_EAST, &(player1.down));
	player1.assignKey(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, &(player1.right));
	player1.assignKey(SDL_GAMEPAD_BUTTON_DPAD_LEFT, &(player1.left));
	Player player2(screen);
	player2.assignKey(SDLK_UP, &(player2.up));
	player2.assignKey(SDLK_DOWN, &(player2.down));
	player2.assignKey(SDLK_LEFT, &(player2.left));
	player2.assignKey(SDLK_RIGHT, &(player2.right));
	player2.assignKey(SDL_GAMEPAD_BUTTON_SOUTH, &(player2.up));
	player2.assignKey(SDL_GAMEPAD_BUTTON_EAST, &(player2.down));
	player2.assignKey(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, &(player2.right));
	player2.assignKey(SDL_GAMEPAD_BUTTON_DPAD_LEFT, &(player2.left));
	
	bool running = true;
	while(running)
	{
		SDL_Event ev;
		while(SDL_PollEvent(&ev))
		{
			switch(ev.type)
			{
				case SDL_EVENT_KEY_DOWN:
					player1.keyDown(ev.key.key);
					player2.keyDown(ev.key.key);
					break;
				case SDL_EVENT_KEY_UP:
					player1.keyUp(ev.key.key);
					player2.keyUp(ev.key.key);
					break;
				case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
					if(player1.gamepadID == ev.gbutton.which)
					{
						player1.keyDown(ev.gbutton.button);
					}
					if(player2.gamepadID == ev.gbutton.which)
					{
						player2.keyDown(ev.gbutton.button);
					}
					break;
				case SDL_EVENT_GAMEPAD_BUTTON_UP:
					if(player1.gamepadID == ev.gbutton.which)
					{
						player1.keyUp(ev.gbutton.button);
					}
					if(player2.gamepadID == ev.gbutton.which)
					{
						player2.keyUp(ev.gbutton.button);
					}
					break;
				case SDL_EVENT_GAMEPAD_ADDED:
					if(player1.gamepadID == -1)
					{
						player1.gamepad = SDL_OpenGamepad(ev.gdevice.which);
						player1.gamepadID = ev.gdevice.which;
						SDL_Log("Added controller to player 1");
					}
					else if(player2.gamepadID == -1)
					{
						player2.gamepad = SDL_OpenGamepad(ev.gdevice.which);
						player2.gamepadID = ev.gdevice.which;
						SDL_Log("Added controller to player 2");
					}
					else
					{
						SDL_Log("Sorry, two player limit at the moment.");
					}
					break;
				case SDL_EVENT_GAMEPAD_REMOVED:
					if(player1.gamepadID == ev.gdevice.which)
					{
						SDL_CloseGamepad(player1.gamepad);
						player1.gamepadID = -1;
						SDL_Log("Removed player1 controller");
					}
					if(player2.gamepadID == ev.gdevice.which)
					{
						SDL_CloseGamepad(player2.gamepad);
						player2.gamepadID = -1;
						SDL_Log("Removed player2 controller");
					}
					break;
				case SDL_EVENT_QUIT:
					running = false;
					break;
			}
		}
		player1.update();
		player2.update();
		SDL_SetRenderDrawColor(screen, 10, 10, 10, 255);
		SDL_RenderClear(screen);
		player1.draw();
		player2.draw();
		SDL_RenderPresent(screen);
	}
	IMG_Quit();
	SDL_Quit();
}

This sounds pretty reasonable. Thanks for the detailed explanation. I like the idea of working off the entire SDL event queue and dumping that into a structure that can be queried later. I plan to do something similar. Its great that you have complete freedom of input mappings, I hope to offer this as well.

I like how your doing a keymap here. I am doing something similar like this, its not user customizable yet but at least I can output the current keybinding to the screen for the user to view. It can also handle multiple keybindings for a particular command. I hope to one day add more complex logic like ā€˜this key and that keyā€™ or ā€˜this key and that key || this key and that keyā€™ offering more freedom around || and && logic as well as parens ā€˜(ā€™, ā€˜)ā€™ to group logic. My game has a defined set of Commands that it can do and the Commands are used for both gameplay and menu navigation.

void ley::GameModel::loadKeyBindings() {

mKeyBindings.debugkeystoggle.first = ley::Command::debugkeystoggle;
mKeyBindings.debugkeystoggle.second.push_back(SDL_SCANCODE_F12);

mKeyBindings.left.first = ley::Command::left;
mKeyBindings.left.second.push_back(SDL_SCANCODE_LEFT);

mKeyBindings.right.first = ley::Command::right;
mKeyBindings.right.second.push_back(SDL_SCANCODE_RIGHT);

mKeyBindings.down.first = ley::Command::down;
mKeyBindings.down.second.push_back(SDL_SCANCODE_DOWN);

mKeyBindings.cclockwise.first = ley::Command::cclockwise;
mKeyBindings.cclockwise.second.push_back(SDL_SCANCODE_UP);
mKeyBindings.cclockwise.second.push_back(SDL_SCANCODE_E);

// etc ā€¦

}

Thanks for sharing your improved version of your game, this is useful information.

Electrosys

FYI that looks fine to me. However I like to ask this anytime people talk about performance, have you tried measuring? How long does it take?

I once used xdotool to send my window input, I notice they were not evenly distributed, I didnā€™t know if xdotool did something funny or if SDL was buffering for a few ms, but either case it was fast enough that I didnā€™t care

I actually put a TODO comment in my code to profile the loop that Iā€™m using to copy the SDL state into my own mKeysPressed object. To profile this I would use a timer at the top of the program and have it measure the sum of all the calls to pollInput through the run of the program.

I have learned to never over optimize early. If Iā€™m writing code that makes sense and it is maintainable then I donā€™t worry about the speed of the application.

I always monitor CPU usage and memory usage. Anytime I see the memory usage crawl up slowly and steadily I know there is a memory leak and I always chase them down. Usually its because I didnā€™t follow the rule of 3/5 I think most of the time. When I run my game its runs with 0.5% CPU usage on my 11th Gen IntelĀ® Coreā„¢ i5-1135G7 Ɨ 8 and uses about 18.7 megs of ram steadily.

1 Like
    auto find_command = [this, bindings](Uint8 scancode) -> ley::Command {
                if (anyInputsMatch(scancode, &bindings->backspace.second)) {
                    return bindings->backspace.first;
                }
                if (anyInputsMatch(scancode, &bindings->quit.second)) {
                    return bindings->quit.first;
                }
                
                //Rotate Block counter clockwise
                if (anyInputsMatch(scancode, &bindings->cclockwise.second)) {
                    return bindings->cclockwise.first; // TODO this needs some work, main menu has cclockwise overloaded
                }
                //Rotate Block clockwise
                if (anyInputsMatch(scancode, &bindings->clockwise.second)) {
                    return bindings->clockwise.first;
                }

                //move block down
                if (anyInputsMatch(scancode, &bindings->down.second)) {
                    return bindings->down.first;
                }
                //move block left
                if (anyInputsMatch(scancode, &bindings->left.second)) {
                    return bindings->left.first;
                }
                //move block right
                if (anyInputsMatch(scancode, &bindings->right.second)) {
                    return bindings->right.first;
                }

                if(anyInputsMatch(scancode, &bindings->enter.second) && !(anyInputsMatch(scancode, &bindings->alt.second))) {
                    return bindings->enter.first;
                }

                if(anyInputsMatch(scancode, &bindings->tab.second)) {
                    return bindings->tab.first;
                }

        return ley::Command::none;                
    };
case SDL_KEYDOWN:
                
                if(event.key.repeat == 0) {
                
                //Anytime we have a keydown event lets see which buttons are currently set to down
//TODO profile this loop.
                for(Uint8 i = 0; i < UINT8_MAX; ++i) {
                    //copy the SDL keyboard state into the keypressed object
                    if(state[i]) {
                        //reset the timers if this key was previously not pressed
                        if(std::get<0>(mKeysPressed[i]) == false) 
                        {
                            std::get<1>(mKeysPressed[i]).reset(); //reset the delay timer.
                            std::get<2>(mKeysPressed[i]).reset(); //reset the repeat timer.
                        }
                        std::get<0>(mKeysPressed[i]) = true;
                    }
                }

                //push_commands(); //updates command
                command = find_command(event.key.keysym.scancode);
                commandQueuePtr->push(command);

                if ((state[SDL_SCANCODE_LALT] && state[SDL_SCANCODE_RETURN])
                    ||(state[SDL_SCANCODE_RALT] && state[SDL_SCANCODE_RETURN])) { 
                        fullscreen = !fullscreen; 
                }

                }

                if(te->hasFocus()) {
                    function(command);
                }

                break;

            case SDL_KEYUP :

                for(Uint8 i = 0; i < UINT8_MAX; ++i) {
                    //copy the SDL keyboard state into the keypressed object
                    if(!state[i]) {
                        std::get<0>(mKeysPressed[i]) = false;
                        std::get<1>(mKeysPressed[i]).reset(); //reset the delay timer, just for brevity.
                        std::get<2>(mKeysPressed[i]).reset(); //reset the repeat timer, just for brevity.
                    }
                }

                break;
    auto check_timers = [this, commandQueuePtr, find_command]() {
        
        for(Uint8 i = 0; i < UINT8_MAX; ++i) {
            
            if (std::get<0>(mKeysPressed[i]) == true) {
                //Run all input timers
                std::get<1>(mKeysPressed[i]).runFrame(false); //run delay timer.
                std::get<2>(mKeysPressed[i]).runFrame(false); //the repeat timer.
            
                //if the delay timer has expired and the repeat timer has expired
                if(std::get<1>(mKeysPressed[i]).hasExpired() && std::get<2>(mKeysPressed[i]).hasExpired()) {
                    commandQueuePtr->push(find_command(i));
                    
                    //reset the repeat timer
                    std::get<2>(mKeysPressed[i]).reset();
                }
            }
        }
    };

I think Iā€™ve almost got it all working the way I want simply by adding timers to each of the keys and then checking the timer at the end of the pollInput method. Iā€™ll send the command again if the repeat timer has expired. I still need to work out a couple edge cases like when modifiers are pressed for the full screen key combination. My find_command method doesnā€™t account for multiple keys yet.

This is what I came up with in the end.

Thanks everyone for the great conversion!

Electrosys.

I have learned to never over optimize early. If Iā€™m writing code that makes sense and it is maintainable then I donā€™t worry about the speed of the application.

I always hated when people say this. Itā€™s not wrong but people think writing sloppy code is ā€˜not optimizingā€™. When people write sloppy code or experimental code it should be thrown out once you learn the lesson. I regularly commit code to git that Iā€™ll throw out that very week

Whats with line 286?!

auto alt_mod = this ā†’ bool {ā€¦

When I saw how many times anyInputsMatch was called and how boring the code looked I got suspicious. Iā€™m not saying what you have now is bad but thatā€™s a lot of arrays youā€™re looping through. Could you maybe simplify things? Maybe use a hashmap with inputs youā€™re interested in as the keys?

I looked at more and I didnā€™t exactly mean that as measure/time it like that. The whole code feels over engineered, which is a lot better than under-engineered but Iā€™m sure many lines could outright be deleted and the code easier to follow if you tried simplifying things

Maybe Iā€™m doing too much of a review. I try to stick with 2 or 3 things. But this code

I believe SDL_GetKeyboardState will give you it so you donā€™t need to build an array each time. You can see an example Lazy Foo' Productions - Key States

IIRC you check the input and stick it in a queue for later processing? Usually when I handle inputs (in games/demos/toy apps) I have an enum that looks something like { Up=1, Right=2, Down=4, Left=8, Start=0x10, Pause=0x20, ā€¦ };

Then in my main loop Iā€™ll do something like if (keyInput & Pause && !(lastKeyInput & Pause)) //pause code

Actually I think I build a new variable that holds all the newly pressed buttons but it doesnt really matter how you do it. IMO itā€™s more clear than a queue. But I used this in frame based demos where you canā€™t have a person press down let go and press down again in 0ms

Not at all. Your input is really valuable.

The reason why I have all my inputs in a data structure like this is because every button on the keyboard has its own delay and repeat timer. If the user holds the up key then the up command will automaiclly be resent once the delay and repeat timers expire. The repeat timer is always shorter than the delay timer so its sufficent to simply resend the command when the delay && repeat timers expire see the check_timers() helper which is called for every frame of the game. sdl2-blocks/Input.cpp at master Ā· electrosy/sdl2-blocks Ā· GitHub . The repeat timer is then reset and the up command will be sent every 35 miliseconds. See sdl2-blocks/Input.cpp at master Ā· electrosy/sdl2-blocks Ā· GitHub

std::map<Uint8, std::tuple<bool, ley::Timer, ley::Timer>> mKeysPressed;

An example of this map populated would beā€™ Up Key ā†’ True (is pressed) ā†’ Delay Timer ā†’ Repeat Timer.

Iā€™m actually thinking I can ditch the call to SDL_GetKeyboardState() completely because on Keydown I capture the key pressed and put it into my mKeypressed data structure as well as start the delay and repeat timers. On Keyup I set that particular key to false.

I translate the keyboard/gamepad input into a command the game can understand and work off the queue in the game controller. See sdl2-blocks/GameController.cpp at master Ā· electrosy/sdl2-blocks Ā· GitHub

I donā€™t think writing sloppy code is ā€˜notā€™ over optimizing. What I mean is that if the code is understandable and makes sense, there really is no reason to spend a lot more time on it, unless it actually has an impact on the games performance.

Actually, often times, optimized code is harder to read, thatā€™s really what I mean. No point in optimizing unless you really need to.

Iā€™m really glad you pointed this out. You might be right, instead of all these if statements I may actually be able to do a lookup. Good call! Iā€™ll try and look at this in more detail and see what I can do. I think this may simply be an artifact from the code evolving. It looks like I may be able to use the scancode for the key and the command for the value, instead of the way I have it where I have the command as the key and the scancode as the value.