352 lines
16 KiB
Markdown
352 lines
16 KiB
Markdown
|
|
# 15. Input
|
|||
|
|
|
|||
|
|
libakgl does not have an input system so much as a *binding* system. SDL3 owns events,
|
|||
|
|
keycodes, keymods, gamepad enumeration and text composition, and it documents all of them
|
|||
|
|
in the [SDL3 wiki](https://wiki.libsdl.org/SDL3/) — start with
|
|||
|
|
[SDL_Event](https://wiki.libsdl.org/SDL3/SDL_Event),
|
|||
|
|
[SDL_Keycode](https://wiki.libsdl.org/SDL3/SDL_Keycode) and
|
|||
|
|
[CategoryGamepad](https://wiki.libsdl.org/SDL3/CategoryGamepad). Nothing here replaces any
|
|||
|
|
of it.
|
|||
|
|
|
|||
|
|
What libakgl adds is two things: a table that says *"when event X arrives from device Y
|
|||
|
|
carrying key or button Z, call this handler on this actor"*, and a small ring buffer of
|
|||
|
|
keystrokes for a caller who wants to ask "is there a key waiting" without owning the event
|
|||
|
|
loop.
|
|||
|
|
|
|||
|
|
## Input is pushed, not polled
|
|||
|
|
|
|||
|
|
**The host pumps every SDL event into `akgl_controller_handle_event`.** There is no
|
|||
|
|
`akgl_input_update()` that reads the keyboard state for you, and there is no internal event
|
|||
|
|
loop. Whatever drains the SDL event queue — your `while ( SDL_PollEvent(&e) )`, or SDL's
|
|||
|
|
`SDL_AppEvent` callback — hands each event over, unconditionally.
|
|||
|
|
|
|||
|
|
Unconditionally is the point: **an event nothing binds is not an error.** It returns
|
|||
|
|
success having done nothing, which is what makes the blanket call correct and keeps the
|
|||
|
|
host from having to know which events matter.
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#include <SDL3/SDL.h>
|
|||
|
|
#include <akgl/actor.h>
|
|||
|
|
#include <akgl/controller.h>
|
|||
|
|
#include <akgl/error.h>
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
* Bind WASD on top of control map 0, which akgl_controller_default has already
|
|||
|
|
* filled with the four arrow keys and the four D-pad directions.
|
|||
|
|
*
|
|||
|
|
* Bindings are appended and scanned in push order, and the first match stops
|
|||
|
|
* the scan -- so these fire only for keys the eight defaults do not claim.
|
|||
|
|
* There is no way to remove one; zeroing akgl_controlmaps[id] is the only reset.
|
|||
|
|
*/
|
|||
|
|
akerr_ErrorContext AKERR_NOIGNORE *bind_wasd(int mapid, SDL_KeyboardID kbid)
|
|||
|
|
{
|
|||
|
|
akgl_Control control;
|
|||
|
|
PREPARE_ERROR(errctx);
|
|||
|
|
|
|||
|
|
PASS(errctx, akgl_controller_default(mapid, "player", kbid, 0));
|
|||
|
|
|
|||
|
|
/* Copied in by value, so one stack local does for every push. */
|
|||
|
|
SDL_memset(&control, 0x00, sizeof(akgl_Control));
|
|||
|
|
control.event_on = SDL_EVENT_KEY_DOWN;
|
|||
|
|
control.event_off = SDL_EVENT_KEY_UP;
|
|||
|
|
|
|||
|
|
control.key = SDLK_A;
|
|||
|
|
control.handler_on = &akgl_actor_cmhf_left_on;
|
|||
|
|
control.handler_off = &akgl_actor_cmhf_left_off;
|
|||
|
|
PASS(errctx, akgl_controller_pushmap(mapid, &control));
|
|||
|
|
|
|||
|
|
control.key = SDLK_D;
|
|||
|
|
control.handler_on = &akgl_actor_cmhf_right_on;
|
|||
|
|
control.handler_off = &akgl_actor_cmhf_right_off;
|
|||
|
|
PASS(errctx, akgl_controller_pushmap(mapid, &control));
|
|||
|
|
|
|||
|
|
SUCCEED_RETURN(errctx);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Every event, unconditionally. An event nothing binds is success, not a
|
|||
|
|
* failure, which is what makes the unconditional call correct. */
|
|||
|
|
akerr_ErrorContext AKERR_NOIGNORE *pump(void *appstate, SDL_Event *event)
|
|||
|
|
{
|
|||
|
|
PREPARE_ERROR(errctx);
|
|||
|
|
PASS(errctx, akgl_controller_handle_event(appstate, event));
|
|||
|
|
SUCCEED_RETURN(errctx);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Two independent paths out of one call
|
|||
|
|
|
|||
|
|
`akgl_controller_handle_event` does two unrelated things with the event it is given, and
|
|||
|
|
**neither one suppresses the other**:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
akgl_controller_handle_event(appstate, event)
|
|||
|
|
|
|
|||
|
|
+----------------------+----------------------+
|
|||
|
|
| |
|
|||
|
|
(a) the keystroke ring, FIRST (b) the control maps, SECOND
|
|||
|
|
| |
|
|||
|
|
KEY_DOWN -> push {key, mod, ""} for map 0..7 (target != NULL):
|
|||
|
|
TEXT_INPUT -> attach text to the for control 0..31, in push order:
|
|||
|
|
newest press, or push event type == event_on/off ?
|
|||
|
|
a text-only entry device id == kbid / jsid ?
|
|||
|
|
| key/button == this binding ?
|
|||
|
|
| -> call the handler and
|
|||
|
|
akgl_controller_poll_key() STOP the entire scan
|
|||
|
|
akgl_controller_poll_keystroke()
|
|||
|
|
akgl_controller_flush_keys()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The ring is filled **first**, before the maps are consulted, and *whether or not* a map
|
|||
|
|
claims the key. A key that drives an actor is still a key an embedded interpreter polling
|
|||
|
|
with `akgl_controller_poll_key()` wants to see.
|
|||
|
|
|
|||
|
|
### The control-map scan
|
|||
|
|
|
|||
|
|
```c excerpt=include/akgl/controller.h
|
|||
|
|
/** @brief How many control maps exist -- effectively the local player limit. */
|
|||
|
|
#define AKGL_MAX_CONTROL_MAPS 8
|
|||
|
|
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
|
|||
|
|
#define AKGL_MAX_CONTROLS 32
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Eight maps means up to eight locally controlled players, each on its own keyboard and
|
|||
|
|
gamepad. A map with a `NULL` `target` is skipped entirely — that is how an unused slot is
|
|||
|
|
spelled.
|
|||
|
|
|
|||
|
|
**Maps are scanned in index order, bindings within a map in push order, and the first
|
|||
|
|
match wins and stops the whole scan.** A key bound in two maps only ever fires in the
|
|||
|
|
lower-numbered one. A key bound twice in one map only fires on the earlier push. This is
|
|||
|
|
what makes `akgl_controller_default` followed by your own pushes behave sanely: the
|
|||
|
|
defaults are already in slots 0–7 and your additions land after them.
|
|||
|
|
|
|||
|
|
A match requires **all three** of: the event type equals the binding's `event_on` or
|
|||
|
|
`event_off`; the device id on the event equals the map's `kbid` (keyboard events) or `jsid`
|
|||
|
|
(gamepad events); and the key or button equals the binding's. The device-id half is what
|
|||
|
|
keeps two players on two keyboards apart, and it is exact — `akgl_controller_list_keyboards`
|
|||
|
|
logs every attached keyboard and its id, which is how you find the number to pass.
|
|||
|
|
|
|||
|
|
There is no way to remove a binding. `akgl_controller_pushmap` appends and that is the
|
|||
|
|
whole API; the only reset is zeroing `akgl_controlmaps[id]` yourself. **Calling
|
|||
|
|
`akgl_controller_default` twice on one map leaves sixteen bindings**, and the first eight
|
|||
|
|
are the ones that fire.
|
|||
|
|
|
|||
|
|
### The keystroke ring
|
|||
|
|
|
|||
|
|
```c excerpt=src/controller.c
|
|||
|
|
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
|
|||
|
|
{
|
|||
|
|
int tail = 0;
|
|||
|
|
|
|||
|
|
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
|||
|
|
keybuffer_awaiting_text = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
|||
|
|
keybuffer[tail].key = key;
|
|||
|
|
keybuffer[tail].mod = mod;
|
|||
|
|
keybuffer[tail].text[0] = '\0';
|
|||
|
|
keybuffer_count += 1;
|
|||
|
|
keybuffer_awaiting_text = true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Thirty-two entries (`AKGL_CONTROLLER_KEY_BUFFER`), one fixed array in the library's data
|
|||
|
|
segment, **one ring for the whole process** — it is a file-scope static, not per map and
|
|||
|
|
not per actor.
|
|||
|
|
|
|||
|
|
**A full buffer drops the newest keystroke, not the oldest.** A caller reading a line of
|
|||
|
|
input wants the characters that were typed first; overwriting the head would hand it the
|
|||
|
|
tail of what the user typed and silently lose the beginning.
|
|||
|
|
|
|||
|
|
Two pollers drain the same ring, and a keystroke taken by one is not waiting for the
|
|||
|
|
other:
|
|||
|
|
|
|||
|
|
| Call | Hands back | Discards |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `akgl_controller_poll_key(&keycode, &available)` | The SDL keycode, as an `int` | Entries carrying only composed text and no keycode — there is no key to report |
|
|||
|
|
| `akgl_controller_poll_keystroke(&dest, &available)` | `key`, `mod` and the composed `text` | Nothing |
|
|||
|
|
| `akgl_controller_flush_keys()` | — | Everything waiting |
|
|||
|
|
|
|||
|
|
An empty ring is success, not a failure: `available` comes back `false` and the caller
|
|||
|
|
polls again next frame. **Check `available`, not the keycode against 0** — a text-only
|
|||
|
|
entry legitimately has a keycode of 0.
|
|||
|
|
|
|||
|
|
**The `text` field needs `SDL_StartTextInput()` on your window.** SDL reports one keystroke
|
|||
|
|
as two events — the key going down, and then the character it composed to, if it composed
|
|||
|
|
to anything — and it only sends the second while text input is started. Without that call
|
|||
|
|
`key` and `mod` still arrive and `text` is always the empty string. That is not a libakgl
|
|||
|
|
limitation to work around; **it is the only correct way to get a character out of SDL**, and
|
|||
|
|
it is what makes a keyboard layout, a compose key, a dead key and an IME commit work. `"`,
|
|||
|
|
`!`, `(`, `)`, `:` and `;` are all unreachable from a keycode alone, and so is every
|
|||
|
|
lower-case letter. See
|
|||
|
|
[SDL_StartTextInput](https://wiki.libsdl.org/SDL3/SDL_StartTextInput).
|
|||
|
|
|
|||
|
|
An entry with composed text and **no** keycode is not a degenerate case: an input method or
|
|||
|
|
a dead key finishes a character with no key press of its own, and a line editor wants that
|
|||
|
|
character. `akgl_controller_poll_keystroke` reports it; `akgl_controller_poll_key` cannot,
|
|||
|
|
and drops it on the way past.
|
|||
|
|
|
|||
|
|
Text longer than `AKGL_CONTROLLER_KEYSTROKE_TEXT` (8 bytes) — an IME committing a whole
|
|||
|
|
word at once — is truncated **on a code point boundary**, never through the middle of a
|
|||
|
|
UTF-8 sequence.
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#include <SDL3/SDL.h>
|
|||
|
|
#include <akgl/controller.h>
|
|||
|
|
#include <akgl/error.h>
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
* Drain the keystroke ring into a line buffer, the way an embedded interpreter
|
|||
|
|
* or a name-entry screen wants it.
|
|||
|
|
*
|
|||
|
|
* The `text` field is only populated while SDL text input is started on the
|
|||
|
|
* window, so a caller that wants punctuation, lower case, dead keys or an IME
|
|||
|
|
* commit calls SDL_StartTextInput() first. Without it `key` and `mod` still
|
|||
|
|
* arrive and `text` is always empty.
|
|||
|
|
*/
|
|||
|
|
akerr_ErrorContext AKERR_NOIGNORE *read_line(char *dest, size_t destsize, bool *done)
|
|||
|
|
{
|
|||
|
|
akgl_Keystroke stroke;
|
|||
|
|
bool available = false;
|
|||
|
|
size_t used = 0;
|
|||
|
|
PREPARE_ERROR(errctx);
|
|||
|
|
|
|||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
|||
|
|
FAIL_ZERO_RETURN(errctx, done, AKERR_NULLPOINTER, "done");
|
|||
|
|
|
|||
|
|
used = SDL_strlen(dest);
|
|||
|
|
*done = false;
|
|||
|
|
|
|||
|
|
/* The ATTEMPT block goes outside the loop: CATCH reports failure by
|
|||
|
|
* break-ing, which would bind to the loop instead of the block. */
|
|||
|
|
ATTEMPT {
|
|||
|
|
for ( ;; ) {
|
|||
|
|
CATCH(errctx, akgl_controller_poll_keystroke(&stroke, &available));
|
|||
|
|
if ( available == false ) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
if ( stroke.key == SDLK_RETURN ) {
|
|||
|
|
*done = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
if ( (stroke.text[0] != '\0') && ((used + 1) < destsize) ) {
|
|||
|
|
dest[used] = stroke.text[0];
|
|||
|
|
used += 1;
|
|||
|
|
dest[used] = '\0';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} CLEANUP {
|
|||
|
|
} PROCESS(errctx) {
|
|||
|
|
} FINISH(errctx, true);
|
|||
|
|
|
|||
|
|
SUCCEED_RETURN(errctx);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The ring is filled on whichever thread pumps events and **is not synchronized**. Poll it
|
|||
|
|
from that thread.
|
|||
|
|
|
|||
|
|
## The default bindings
|
|||
|
|
|
|||
|
|
`akgl_controller_default(controlmapid, actorname, kbid, jsid)` points a map at the actor
|
|||
|
|
registered under `actorname` and pushes **eight** bindings — the four arrow keys and the
|
|||
|
|
four D-pad directions — each wired to the matching `akgl_actor_cmhf_*_on`/`_off` pair:
|
|||
|
|
|
|||
|
|
| Key | Gamepad button | `handler_on` / `handler_off` |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `SDLK_DOWN` | `SDL_GAMEPAD_BUTTON_DPAD_DOWN` | `akgl_actor_cmhf_down_on` / `_down_off` |
|
|||
|
|
| `SDLK_UP` | `SDL_GAMEPAD_BUTTON_DPAD_UP` | `akgl_actor_cmhf_up_on` / `_up_off` |
|
|||
|
|
| `SDLK_LEFT` | `SDL_GAMEPAD_BUTTON_DPAD_LEFT` | `akgl_actor_cmhf_left_on` / `_left_off` |
|
|||
|
|
| `SDLK_RIGHT` | `SDL_GAMEPAD_BUTTON_DPAD_RIGHT` | `akgl_actor_cmhf_right_on` / `_right_off` |
|
|||
|
|
|
|||
|
|
It is the "just give me something that works" path. A game wanting different keys builds
|
|||
|
|
its own bindings with `akgl_controller_pushmap`, as in the WASD example above.
|
|||
|
|
|
|||
|
|
**The default bindings cannot produce a diagonal.** Every `akgl_actor_cmhf_*_on` handler
|
|||
|
|
clears `AKGL_ACTOR_STATE_MOVING_ALL` before setting its own bit, so pressing Left while Up
|
|||
|
|
is held cancels Up rather than adding to it. That is fine for a four-way JRPG and wrong for
|
|||
|
|
anything that wants eight-way movement — and it means the thrust-vector ellipse cap in
|
|||
|
|
[Chapter 14](14-physics.md) is unreachable through these handlers. Eight-way movement wants
|
|||
|
|
your own handlers that `AKGL_BITMASK_ADD` the movement bit without clearing the others.
|
|||
|
|
|
|||
|
|
`akgl_controller_handle_button_down` / `_up` are a different thing again, and are **not**
|
|||
|
|
control-map bindings: they look the actor up by the literal name `"player"` in
|
|||
|
|
`AKGL_REGISTRY_ACTOR` rather than being told which actor to act on, so they only ever drive
|
|||
|
|
one. The `akgl_actor_cmhf_*` handlers are the general form; see
|
|||
|
|
[Chapter 12](12-actors.md).
|
|||
|
|
|
|||
|
|
## Gamepads
|
|||
|
|
|
|||
|
|
**SDL delivers no button events from a gamepad nobody has opened.** `akgl_game_init` calls
|
|||
|
|
`akgl_controller_open_gamepads()` for the devices present at startup; a device plugged in
|
|||
|
|
mid-session arrives as `SDL_EVENT_GAMEPAD_ADDED`, and `akgl_controller_handle_added` opens
|
|||
|
|
it. Route that event (and `SDL_EVENT_GAMEPAD_REMOVED`, to `akgl_controller_handle_removed`)
|
|||
|
|
or hot-plugged pads stay silent. No gamepads attached at all is success, not an error.
|
|||
|
|
|
|||
|
|
`akgl_controller_handle_added` logs the mapping it found, deliberately: **a controller with
|
|||
|
|
no entry in the mapping database produces no button events at all**, which is otherwise
|
|||
|
|
indistinguishable from a broken binding.
|
|||
|
|
|
|||
|
|
Which is why libakgl ships a database. `include/akgl/SDL_GameControllerDB.h` carries
|
|||
|
|
`AKGL_SDL_GAMECONTROLLER_DB_LEN` (2255) mappings from the community
|
|||
|
|
[SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB) project, and
|
|||
|
|
`akgl_game_init` feeds every one to `SDL_AddGamepadMapping` before opening anything.
|
|||
|
|
|
|||
|
|
That header is **generated, and tracked on purpose**. It is the offline fallback that keeps
|
|||
|
|
the library buildable when upstream is renamed, rate-limited or unreachable. Never
|
|||
|
|
hand-edit it; regeneration is `cmake --build build --target controllerdb` and nothing else
|
|||
|
|
runs the script. The full contract, including why a failed fetch used to destroy the very
|
|||
|
|
file it exists to protect, is in `AGENTS.md` under "Generated and Vendored Sources".
|
|||
|
|
|
|||
|
|
## Known defects
|
|||
|
|
|
|||
|
|
**A matched binding's handler pointer is not checked.** `akgl_controller_handle_event`
|
|||
|
|
calls `curcontrol->handler_on(...)` the moment the event type, device id and key all match,
|
|||
|
|
with no `NULL` test. A control pushed with a `NULL` `handler_on` or `handler_off` crashes
|
|||
|
|
when its event arrives — and since `akgl_controller_pushmap` copies whatever it is given,
|
|||
|
|
a `memset`-zeroed `akgl_Control` with only `event_on` filled in is a loaded gun. **Set both
|
|||
|
|
handlers on every binding you push**, even if one of them does nothing. There is no
|
|||
|
|
diagnostic for this at any layer.
|
|||
|
|
|
|||
|
|
**`appstate` is required and never read.** Every entry point in this subsystem —
|
|||
|
|
`akgl_controller_handle_event`, `handle_button_down`, `handle_button_up`, `handle_added`,
|
|||
|
|
`handle_removed` — raises `AKERR_NULLPOINTER` on a `NULL` `appstate` and then never looks
|
|||
|
|
at it. Pass any non-`NULL` pointer if your program has no app state.
|
|||
|
|
|
|||
|
|
**The analogue-axis, mouse and pen fields are declared and never consulted.**
|
|||
|
|
`akgl_Control::axis`, `axis_range_min` and `axis_range_max`, and `akgl_ControlMap::mouseid`
|
|||
|
|
and `penid`, are in the structs and `akgl_controller_handle_event` does not read any of
|
|||
|
|
them. Setting them has no effect; analogue sticks, mice and pens do not reach the control
|
|||
|
|
maps at all today. Handle those events yourself before calling
|
|||
|
|
`akgl_controller_handle_event`.
|
|||
|
|
|
|||
|
|
**Key auto-repeat is not filtered.** The ring push tests only `event->type ==
|
|||
|
|
SDL_EVENT_KEY_DOWN` and does not consult `event->key.repeat`, so a held key fills the
|
|||
|
|
32-entry ring at the platform's repeat rate. Check `repeat` in your event pump if you are
|
|||
|
|
polling for discrete presses.
|
|||
|
|
|
|||
|
|
Two defects this chapter used to carry are **fixed** and are recorded here so nobody
|
|||
|
|
re-derives them from the header prose:
|
|||
|
|
|
|||
|
|
- **A negative `controlmapid` is rejected.** Both `akgl_controller_pushmap` and
|
|||
|
|
`akgl_controller_default` check `controlmapid < 0` as well as the upper bound and raise
|
|||
|
|
`AKERR_OUTOFBOUNDS`. `tests/controller.c` passes `-1` and `-4096` to each. The
|
|||
|
|
`@warning` blocks in `include/akgl/controller.h` saying only the upper bound is checked
|
|||
|
|
are stale; `TODO.md`, "Known and still open" item 11.
|
|||
|
|
- **The `akgl_controller_handle_*` functions exist.** `controller.h` once declared four
|
|||
|
|
names that were defined under different spellings, so they linked nowhere. Fixed in
|
|||
|
|
0.5.0, and `scripts/check_api_surface.sh` runs as the `api_surface` test to stop that
|
|||
|
|
class of drift coming back.
|
|||
|
|
|
|||
|
|
## Statuses
|
|||
|
|
|
|||
|
|
The ones this subsystem raises, with their libakgl meanings in
|
|||
|
|
[Chapter 4](04-errors.md):
|
|||
|
|
|
|||
|
|
| Status | When |
|
|||
|
|
|---|---|
|
|||
|
|
| `AKERR_NULLPOINTER` | A `NULL` `appstate`, `event`, `control`, or poller destination; SDL cannot enumerate or open a device; no actor registered as `"player"` |
|
|||
|
|
| `AKERR_OUTOFBOUNDS` | `controlmapid` outside `0..AKGL_MAX_CONTROL_MAPS - 1`, or the map already holds `AKGL_MAX_CONTROLS` bindings |
|
|||
|
|
| `AKGL_ERR_REGISTRY` | `akgl_controller_default` could not find `actorname` — usually because the actor has not been created yet |
|
|||
|
|
| anything | Whatever the matched binding's handler raises, propagated unchanged |
|