SDL Wiki
[ front page | index | search | recent changes | git repo | offline html ]

Best keyboard practices in SDL3

First things first

Keyboard input is a surprisingly complicated topic after you step outside of your usual country and language (and sometimes before you do, too).

Almost any game, no matter what approach it is taking, should probably offer a means for users to configure key bindings; not only will this solve concerns about what kind of keyboard a user is typing on, it will also make your game flexible to whatever is most comfortable for a user. This is not just a question of keyboard layouts, but being kind to those that don't have full motion of their hands and would benefit from moving keys to accessible locations that don't make obvious sense to an outside observer. At least offer a config file, if not a user interface; people you've never met will thank you for it!

The four approaches

There are, as far as we can tell, four common ways that apps want to use keyboard input. Sometimes they want different approaches at different moments, too.

The 101-Button Joystick

Many games just want to treat a keyboard not as a way to input text, but just as a joystick that has a lot of buttons. The well-known "WASD" key pattern for FPS games is a fine example of this: you want the physical location of a key, regardless of what symbol is printed on the key. After all, on a French keyboard, instead of "WASD", you'd press "ZQSD", and on a Hiragana keyboard "てちとし". Same locations on the keyboard, totally different symbols.

For these, you want SDL_Scancodes: these are guaranteed to reference the physical location on the keyboard and not what is printed on it.

Specifically, they assume a US English QWERTY keyboard layout, no matter what the keyboard in use actually has at that location, but that's okay because here we just want physical location of the key, not its meaning.

Events vs States

When dealing with the buttons of joysticks, mice, and keyboards, it's common that people reach first for events. This document is mostly concerned with them as well as they apply to all of the approaches we'll cover. That said it's important to understand the differences here, as you may run into undesirable if you don't choose wisely.

Conceptually, Events are a way for the OS or SDL to inform you something has happened. You don't necessarily always want to react to this information immediately. Consider when the player is moving forward, moving the character is, in most games, going to require per-frame work. So frame to frame, you'll need to know the user is pressing the button. Button events, regardless if they're Keyboard/Mouse/Gamepad/Joystick, don't get sent every frame, although they do get repeated, which we'll discuss down below. This can make it appear as though a character is continuously moving, but it will be jerky and there will be a big delay towards the beginning.

Of course, there's still cases where you might want to use events, but they're typically singular actions. You can know when someone has pressed down on a button to try to do an action, and then you can choose to either immediately adjust state, or set a flag to evaluate the input later in the frame.

On the other hand States, in the context of input, represent what we last saw when we finished processing events. These are great when we may receive contradictory input and we want to evaluate all relevant state before proceeding with your game logic. They're also useful for continuous input, like movement.

We'll demonstrate both methods below.

A Note on Key Repeat

As mentioned above, key events are not sent every frame. There's no real way for SDL to do this for us if you're using something like SDL_PollEvent, as it has no idea when your frame starts and ends. Despite that, we do get repeated key event periodically. These repeats are sent by the OS, and typically there's two types of delays between these events. The first type is for the first repeat event. It's typically a little longer, something like ~1s, and then the OS will start sending them at some interval, something like ~200ms. These numbers are fuzzy as this is generally user configurable and dependent on the OS.

Also mentioned above is that using events for something like movement often results in jerky movement, and indeed, this is due to the delays discussed above. You can mimic the jerky cadence by putting a cursor into a simple text editor and holding it down. The intervals you see as the characters appear is what the key repeat looks like on your system.

Using them in practice

States

For states you check during the gameloop, you can retrieve the state of the keyboard via SDL_GetKeyboardState. This will give you an array of boolean values representing if a button is up (false), or down (true), indexed by SDL_Scancodes. There's no need to worry about the lifetime, SDL owns this array, but you may want to cache it in practice, just so you don't have to call into SDL all the time.

Here's an example of how you might index into this array to implement the forward/backward axis of the "WASD" pattern mentioned above.

/* returns 1 if moving forward with this keypress, -1 if moving backward, 0 if not moving. */
int direction_user_should_move()
{
    const bool *key_states = SDL_GetKeyboardState();
    int direction = 0;

    /* (We're writing our code such that it sees both keys are pressed and cancels each other out!) */
    if (key_states[SDL_SCANCODE_W]) {
        direction += 1;  /* pressed what would be "W" on a US QWERTY keyboard. Move forward! */
    } 

    if (key_states[SDL_SCANCODE_S]) {
        direction += -1;  /* pressed what would be "S" on a US QWERTY keyboard. Move backward! */
    }

    /* (In practice it's likely you'd be doing full directional input in here, but for simplicity, we're just showing forward and backward) */

    return direction;  /* wasn't key in W or S location, don't move. */
}

Events

On the contrary, you might want to initiate an action from events, setting your own state from them that you process in your game loop.

Simply grab the scancode field from SDL_EVENT_KEY_DOWN and SDL_EVENT_KEY_UP events.

enum Action { ACTION_NONE, ACTION_RELOAD, ACTION_JUMP };

/* returns ACTION_RELOAD if reloading with this keypress, ACTION_JUMP if jumping, ACTION_NONE if no actions were attempted. */
Action action_user_should_take(const SDL_Event *e)
{
    SDL_assert(e->type == SDL_EVENT_KEY_DOWN); /* just checking key presses here... */
    if (e->key.scancode == SDL_SCANCODE_R) {
        return ACTION_RELOAD;  /* pressed what would be "R" on a US QWERTY keyboard. Reload! */
    } else if (e->key.scancode == SDL_SCANCODE_SPACE) {
        return ACTION_JUMP;  /* pressed what would be "Space" on a US QWERTY keyboard. Jump! */
    }

    return ACTION_NONE;  /* wasn't key in W or S location, don't move. */
}

The Specific Key

Some games might want to know the symbol on a key. This tends to be a "press 'I' to open your inventory" thing, and you don't really care where the 'I' key is on the keyboard.

(But, again: offer keybindings, because some keyboards don't have an 'I' key!)

This is also useful for looking for the ESC key to cancel an operation, or Enter to confirm, etc; it doesn't matter where the key is, you still want that key.

These are SDL_Keycodes. They name specific keys and don't care where on the user's keyboard they actually are. Like scancodes, you also get these from SDL_EVENT_KEY_DOWN and SDL_EVENT_KEY_UP events.

/* sit in a loop forever until the user presses Escape. */
bool quit_the_app = false;
while (!quit_the_app) {
    SDL_Event e;
    while (SDL_PollEvent(&e)) {
        /* user has pressed a key? */
        if (e.type == SDL_EVENT_KEY_DOWN) {
            /* the pressed key was Escape? */
            if (e.key.key == SDLK_ESCAPE) {
                quit_the_app = true;
            }
        }
    }
}

The Chat Box

Unicode is hard! If you are composing text a string at a time ("Enter your name, adventurer!" screens or accepting sentences for a chat interface, etc) you should not be using key press events! This will never do the right thing across various keyboards and human languages around the world.

One should instead call SDL_StartTextInput, and listen for SDL_EVENT_TEXT_INPUT events. When done accepting input, call SDL_StopTextInput. This approach will let the system provide input interfaces that are familiar to the user (including popping up a virtual keyboard on mobile devices, and other UI for composing in various languages). Then the event will provide Unicode strings in UTF-8 format, which might be complete lines of text or a single character, depending on the system. You will not be able to replicate these interfaces in your application for everyone in the world, do not try to build this yourself on top of individual keypress events.

The downside of this is that a virtual keyboard, etc, might be disruptive to your game, so you need to design accordingly.

/* Set the text area and start text input */
bool text_input_complete = false;
char text[1024] = { 0 };
SDL_Rect area = { textfield.x, textfield.y, textfield.w, textfield.h };
int cursor = 0;
SDL_SetTextInputArea(window, &area, cursor);
SDL_StartTextInput(window);

while (!text_input_complete) {
    SDL_Event e;
    while (SDL_PollEvent(&e)) {
        /* user has pressed a key? */
        if (e.type == SDL_EVENT_KEY_DOWN) {
            /* the pressed key was Escape or Return? */
            if (e.key.key == SDLK_ESCAPE || e.key.key == SDLK_RETURN) {
                SDL_StopTextInput(window);
                text_input_complete = true;
            }
            /* Handle arrow keys, etc. */
        } else if (e.type == SDL_EVENT_TEXT_INPUT) {
            SDL_strlcat(text, e.text.text, sizeof(text));
        }
    }

    /* Render the text, adjusting cursor to the offset in pixels from the left edge of the textfield */
    ...

    /* Update the text input area, adjusting the cursor so IME UI shows up at the correct location. */
    SDL_SetTextInputArea(window, &area, cursor);
}

If you're writing a fullscreen game, you might want to render IME UI yourself, so you'd set SDL_HINT_IME_IMPLEMENTED_UI appropriately and handle the SDL_EVENT_TEXT_EDITING and SDL_EVENT_TEXT_EDITING_CANDIDATES events. See testime.c for an example of this.

The Text Editor

This is a special case, and if you think your game fits here, you should think very hard about how to change that before thinking about how to go this route, because often times you are just on a path to build The Chat Box, incorrectly, from scratch.

If you were writing an SDL frontend for Vim, you would need to know what keypresses-plus-modifiers produce: hitting shift-Z twice produces a different result than pressing z twice, but also you want arrow keys to do the right thing on the numpad, unless NumLock is pressed, when they should produce numbers. On top of all this, it's difficult to correlate between keypress and proper text input events and untangle them to get correct results.

In this case, you would need to handle both key events and input events as above, in addition you might want to know what the modified keycode for the event is:

bool quit_the_app = false;
while (!quit_the_app) {
    SDL_Event e;
    while (SDL_PollEvent(&e)) {
        /* user has pressed a key? */
        if (e.type == SDL_EVENT_KEY_DOWN) {
            /* Was the pressed key '$', possibly generated by Shift+4 on a US keyboard or the '$' key on the French keyboard? */
            SDL_Keycode keycode = SDL_GetKeyFromScancode(e.key.scancode, e.key.mod, false);
            if (keycode == '$') {
                /* Show me the money! */
            }
        }
    }
}

Obviously there are many keys that don't generate a character, or characters that are composed by pressing multiple keys (or navigating through IME interfaces that don't map to specific keypresses at all), so this is niche functionality and not how one should accept input in a general sense.

Showing key names to users

So you've made your game, and now you're taking the original advice about adding user-configurable keybindings, and you need to know how to show the user the key's name in your config UI, and maybe also in a "press [current keybinding] to jump" tutorial message.

For this, use SDL_GetKeyName with the SDL_Keycode you get from an event:

bool quit_the_app = false;
while (!quit_the_app) {
    SDL_Event e;
    while (SDL_PollEvent(&e)) {
        /* user has pressed a key? */
        if (e.type == SDL_EVENT_KEY_DOWN) {
            SDL_Log("Wow, you just pressed the %s key!", SDL_GetKeyName(e.key.key));
        }
    }
}

Note that SDL_GetKeyName only returns uppercase characters, which is appropriate for showing a user a "press the button with this symbol on it" message.


[ edit | delete | history | feedback | raw ]

All wiki content is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).
Wiki powered by ghwikipp.