In this lesson, we will cover in detail how we can detect and react to the user providing input to our application using their keyboard. Similar to other forms of input, when SDL detects keyboard interaction, it pushes events into the event queue. We can capture these events through our event loop, and handle them as needed.
This lesson builds on our earlier work, where we have a Window
class that initializes SDL and creates a window:
// Window.h
#pragma once
#include <SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"My Program", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, 600, 300, 0);
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
~Window() { SDL_DestroyWindow(SDLWindow); }
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
private:
SDL_Window* SDLWindow;
};
We also have a main
function implementing a basic application loop:
// main.cpp
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while(true) {
while(SDL_PollEvent(&Event)) {
// Detect and handle input events
// ...
}
GameWindow.Update();
}
SDL_Quit();
return 0;
}
Our SDL_PollEvent(&Event)
statement will update our Event
object with any keyboard action that the user performs. We’ll then detect and react to those actions within the body of the loop.
The two most common event types for handling the keyboard are SDL_KEYDOWN
, which is created when a button is pressed, and SDL_KEYUP
, for when a button is released.
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN) {
// ...
} else if (Event.type == SDL_KEYUP) {
// ...
}
}
To find out which button was pressed or released, we need to check the key code. If we have an object called Event
whose type is SDL_Event
, and the Event.type
member variable is SDL_KEYUP
or SDL_KEYDOWN
, we can access the key code using Event.key.keysym.sym
:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN) {
std::cout << "Key: " << Event.key.keysym.sym;
}
}
Key: 32
Key codes are basic integers, but SDL provides variables to help us understand which keys these integers represent. These variables are identified by the SDLK_
prefix. For example, the integer that represents the spacebar is available as SDLK_SPACE
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN) {
if (Event.key.keysym.sym == SDLK_SPACE) {
std::cout << "Spacebar Pressed";
}
}
}
Spacebar Pressed
In this example, we react to the arrow keys being pressed:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN) {
if (Event.key.keysym.sym == SDLK_UP) {
// Up Arrow
} else if (Event.key.keysym.sym == SDLK_DOWN) {
// Down Arrow
} else if (Event.key.keysym.sym == SDLK_LEFT) {
// Left Arrow
} else if (Event.key.keysym.sym == SDLK_RIGHT) {
// Right Arrow
}
}
}
A list of all the key codes is available in the SDL_keycode header file.
SDL_KeyboardEvent
When our SDL_Event
object has a type
of SDL_KEYDOWN
or SDL_KEYUP
, most of the information we care about is within the key
struct of that event. This struct has a type of SDL_KeyboardEvent
:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN
|| Event.type == SDL_KEYUP) {
SDL_KeyboardEvent KeyEvent = Event.key;
}
}
Our main event loop can get long and complex, so we want to proactively mitigate this growth in our design.
One of the main ways of doing this is restricting our main event loop to just determining the high-level nature of each event (typically by examining the type
variable) and then handing it off to a function to implement the required reaction.
A handler for events whose type is SDL_KEYDOWN
or SDL_KEYUP
will be most interested in the SDL_KeyboardEvent
subobject stored in the key
variable, so that tends to be what we provide from our main loop:
// Event Loop
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_KEYDOWN
|| Event.type == SDL_KEYUP) {
HandleKeyboard(Event.key);
}
}
// Handler
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.keysym.sym == SDLK_UP) {
// Up Arrow
} else if (E.keysym.sym == SDLK_DOWN) {
// Down Arrow
} else if (E.keysym.sym == SDLK_LEFT) {
// Left Arrow
} else if (E.keysym.sym == SDLK_RIGHT) {
// Right Arrow
}
}
To understand whether the key was pressed or released, the SDL_KeyboardEvent
subobject also has a type
field, which mirrors the type of the original SDL_Event
. As such, we can compare it to SDL_KEYDOWN
or SDL_KEYUP
:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.type == SDL_KEYDOWN) {
// Key Pressed
} else if (E.type == SDL_KEYUP) {
// Key Released
}
}
Alternatively, we can access the state
variable of the SDL_KeyboardEvent
, and compare it to SDL_PRESSED
or SDL_RELEASED
:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED) {
// Key Pressed
} else if (E.state == SDL_RELEASED) {
// Key Released
}
}
If we have a keycode, and we want to understand what key was pressed, the SDL_GetKeyName()
function can help us. It converts a key code to a more understandable name:
void HandleKeyboard(SDL_KeyboardEvent& E) {
std::cout
<< "Key Pressed! Key Code: "
<< E.keysym.sym
<< ", Key Name: "
<< SDL_GetKeyName(E.keysym.sym)
<< '\n';
}
Key Pressed! Key Code: 113, Key Name: Q
Key Pressed! Key Code: 119, Key Name: W
Key Pressed! Key Code: 101, Key Name: E
Key Pressed! Key Code: 114, Key Name: R
Key Pressed! Key Code: 116, Key Name: T
Key Pressed! Key Code: 121, Key Name: Y
A small subset of keys on our keyboard are considered modifier keys as, by convention, applications them to modify the intent of other inputs. For example, in word processing applications, pressing a letter key whilst the shift key is held down is interpreted as requesting the uppercase form of that letter.
We can detect keydown and keyup events for these keys like any other:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED) {
std::cout << SDL_GetKeyName(E.keysym.sym)
<< " pressed\n";
} else if (E.state == SDL_RELEASED) {
std::cout << SDL_GetKeyName(E.keysym.sym)
<< " released\n";
}
}
Left Ctrl pressed
Left Ctrl released
Left Shift pressed
Left Shift released
However, for convenience, the keysym
struct also has a mod
member variable, which tells us which (if any) modifier keys were active on any keyboard button event.
This variable is an integer that acts as a bit set, so we use the bitwise operators to understand which modifiers were active
SDL provides variables to use with this bit set. For example, we can use KMOD_LSHIFT
as the right operand to the &
operator, which will return true
if the left shift button was active.
Below, we detect presses of the space bar, with different behaviors depending on whether or not left shift was active:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "Space bar was pressed\n";
if (E.keysym.mod & KMOD_LSHIFT) {
std::cout << " with left shift active\n";
}
}
}
Space bar was pressed
Space bar was pressed
with left shift active
We can build more elaborate logic by combining boolean or bitwise operators. Below, we use the boolean &&
operator to determine if multiple modifiers are simultaneously active, and the bitwise |
operator to detect if any of a range of modifiers are active:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "Space bar was pressed\n";
if ((E.keysym.mod & KMOD_LSHIFT)
&& (E.keysym.mod & KMOD_LCTRL)) {
std::cout << " with left shift AND "
"left ctrl active\n";
} else if (E.keysym.mod &
(KMOD_LSHIFT | KMOD_LCTRL)) {
std::cout << " with left shift OR "
"left ctrl active\n";
}
}
}
Space bar was pressed
Space bar was pressed
with left shift AND left ctrl active
Space bar was pressed
with left shift OR left ctrl active
The range of KMOD_
variables provided by SDL are defined in the SDL_Keymod
enum within the keycode header file.
On many platforms, holding a key down sends a continuous stream of input events after a brief pause. SDL reports these as repeated keydown events.
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "Space bar was pressed\n";
} else if (E.state == SDL_RELEASED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "Space bar was released\n";
}
}
Holding our spacebar down for a few seconds generates output similar to the following:
Space bar was pressed
Space bar was pressed
Space bar was pressed
Space bar was pressed
Space bar was released
We can distinguish between initial and repeat events by inspecting the repeat
variable of the SDL_KeyboardEvent
(or the key.repeat
variable of the SDL_Event
)
The repeat
value will be 0
for the initial keydown, and a non-zero value if it was caused by the user continuing to hold the key down:
void HandleKeyboard(SDL_KeyboardEvent& E) {
if (E.state == SDL_PRESSED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "\nSpace bar was pressed - "
<< (E.repeat ? "Repeat" : "Initial");
} else if (E.state == SDL_RELEASED &&
E.keysym.sym == SDLK_SPACE) {
std::cout << "Space bar was released\n";
}
}
Space bar was pressed - Initial
Space bar was pressed - Repeat
Space bar was pressed - Repeat
Space bar was pressed - Repeat
Space bar was released
If our interaction model needs to detect if the user is currently holding down a key, directly querying the keyboard state is often easier than keeping track of keyup and keydown events.
We can determine which buttons are being held down at any time, from anywhere in our application. We introduce how to do this later in the chapter, and its advantages and disadvantages over using the event loop to track inputs.
Keyboards can have different layouts. Much of the world uses the QWERTY layout, but others, such as AZERTY, are also common.
This variation means there are two different ways to represent keys.
Key codes, sometimes referred to as virtual key codes, are the simplest to understand. If the user presses the button labeled "Q` on their keyboard, a key code representing Q will be reported. This assumes the user configured their system correctly - that is, the layout they selected in their system settings matches the layout of their physical keyboard.
Scan codes, sometimes referred to as physical key codes, ignore the configured layout and instead represent what physical button was pressed. In SDL2, the scan code is reported based on the QWERTY layout. For example, if the user presses the button that would be labeled "Q" on a QWERTY keyboard, a scan code representing Q will be reported.
This means that if the user is on an AZERTY keyboard for example, and they press the button labeled A, the scan code we receive will be Q, as that would be the Q button on a QWERTY keyboard:
Scan codes are primarily useful when we care about the position of the buttons, rather than their alphabetic representations. For example, the W, A, S, and D keys are commonly used for movement in video games, due to their position and proximity to each other on a QWERTY keyboard.
On an AZERTY layout, those same physical buttons are labeled Z, Q, S, and D. But, if we’re using scan codes, we don’t need to care about that difference. Z will be reported as W, and Q will be reported as A, so players can use the same physical controls regardless of their keyboard layout.
Within SDL’s keysym
struct, the key code is available as the sym
member variable. whilst the scan code is available as scancode
void HandleKeyboard(SDL_KeyboardEvent& E) {
SDL_Keycode KeyCode{E.keysym.sym};
SDL_Scancode ScanCode{E.keysym.scancode};
}
This lesson introduced how to handle keyboard input using SDL. The key takeaways include:
SDL_KEYDOWN
and SDL_KEYUP
eventsSDL_GetKeyName()
functionLearn how to detect and respond to keyboard input events in your SDL-based applications. This lesson covers key events, key codes, and modifier keys.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games