From 38eee24832fe6da4f135cae455881ab97953b23a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sat, 18 Apr 2026 02:47:10 +0100 Subject: Refresh docs and re-indent --- docs/input.org | 345 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 docs/input.org (limited to 'docs/input.org') diff --git a/docs/input.org b/docs/input.org new file mode 100644 index 0000000..81ff65e --- /dev/null +++ b/docs/input.org @@ -0,0 +1,345 @@ +#+TITLE: Input + +Downstroke's input system is *action-based*. Your game never asks "is +the =W= key held?" — it asks "is the =up= action held?". A single +=input-config= record decides which raw SDL events (keyboard keys, +joystick buttons, controller buttons, analog axes) map to which +abstract action symbols, so the same update loop works on keyboard, +joystick, and game controller without changes. + +Each frame, the engine collects pending SDL events, folds them into a +fresh =input-state= record (current + previous action alists), and +stores it on the =game= struct. Your =update:= hook reads that state +through =game-input=. + +* The minimum you need + +#+begin_src scheme +(import downstroke-engine + downstroke-input + downstroke-world + downstroke-entity) + +(game-run! + (make-game + title: "Move a Square" width: 320 height: 240 + + create: (lambda (game) + (game-scene-set! game + (make-sprite-scene + entities: (list (plist->alist + (list #:type 'player + #:x 150 #:y 100 + #:width 32 #:height 32 + #:color '(100 160 255))))))) + + update: (lambda (game dt) + (let* ((scene (game-scene game)) + (input (game-input game)) + (player (car (scene-entities scene))) + (dx (cond ((input-held? input 'left) -2) + ((input-held? input 'right) 2) + (else 0)))) + (game-scene-set! game + (update-scene scene + entities: (list (entity-set player #:x + (+ (entity-ref player #:x 0) dx))))))) )) +#+end_src + +No =input-config:= keyword is passed, so =*default-input-config*= is +used: arrow keys, WASD, =j=/=z= for =a=, =k=/=x= for =b=, =return= for +=start=, =escape= for =quit=, plus a standard game-controller mapping. + +* Core concepts + +** Actions vs raw keys + +An *action* is a symbol that names something the game cares about +(=up=, =a=, =start=). A *key binding* (or button/axis binding) maps a +raw hardware event to an action. Game code only ever reads actions — +raw SDL keysyms never leak into your =update:= hook. + +The default action list, defined in =*default-input-config*=, is: + +#+begin_src scheme +'(up down left right a b start select quit) +#+end_src + +These map loosely to a generic two-button gamepad: a D-pad (=up=, +=down=, =left=, =right=), two face buttons (=a=, =b=), two system +buttons (=start=, =select=), and a synthetic =quit= action the engine +uses to terminate =game-run!= (the default loop exits when =quit= is +held). + +You are free to add or rename actions when you build a custom +=input-config= — the action list is just the set of symbols your game +agrees to recognise. + +** The default input config + +=*default-input-config*= is a =input-config= record bound at module +load time in =input.scm=. Its contents: + +*Keyboard map* (SDL keysym → action): + +| Keys | Action | +|------------------+---------| +| =w=, =up= | =up= | +| =s=, =down= | =down= | +| =a=, =left= | =left= | +| =d=, =right= | =right= | +| =j=, =z= | =a= | +| =k=, =x= | =b= | +| =return= | =start= | +| =escape= | =quit= | + +*Joystick button map* (SDL joy button id → action). These ids suit a +generic USB pad in the "SNES-ish" layout: + +| Button id | Action | +|-----------+----------| +| =0= | =a= | +| =1= | =b= | +| =7= | =start= | +| =6= | =select= | + +*Game-controller button map* (SDL =SDL_GameController= symbol → +action). This is the "Xbox-style" API SDL2 exposes for known +controllers: + +| Button | Action | +|--------------+----------| +| =a= | =a= | +| =b= | =b= | +| =start= | =start= | +| =back= | =select= | +| =dpad-up= | =up= | +| =dpad-down= | =down= | +| =dpad-left= | =left= | +| =dpad-right= | =right= | + +*Analog axis bindings.* Each binding is =(axis positive-action +negative-action)= — when the axis value exceeds the deadzone, the +positive action is held; when it drops below the negated deadzone, the +negative action is held. + +- Joystick: =(0 right left)= and =(1 down up)= (X and Y axes). +- Controller: =(left-x right left)= and =(left-y down up)= (left + analog stick). + +*Deadzone* is =8000= (SDL axis values range −32768 to 32767). + +All of these are accessible via =input-config-keyboard-map=, +=input-config-joy-button-map=, and so on, but you generally won't +touch them directly — you pass a whole replacement record via +=make-input-config= (see below). + +** Querying input each frame + +The engine calls =input-state-update= once per frame with the SDL +events it has just collected. That produces a new =input-state= record +and stores it on the game via =game-input-set!=. Your =update:= hook +reads it with =(game-input game)=: + +#+begin_src scheme +(update: (lambda (game dt) + (let ((input (game-input game))) + ...))) +#+end_src + +Three predicates live on an =input-state=: + +- =(input-held? state action)= — ~#t~ while the action is currently + active (key/button down, axis past deadzone). +- =(input-pressed? state action)= — ~#t~ for exactly one frame: the + frame on which the action transitioned from not-held to held. +- =(input-released? state action)= — ~#t~ for exactly one frame: the + frame on which the action transitioned from held to not-held. + +Press / release are derived from the record itself: =input-state= +carries both a =current= and =previous= alist of =(action . bool)= +pairs, and =input-state-update= rolls the previous snapshot forward +each frame. You do not need to maintain any input history yourself. + +A fourth convenience, =(input-any-pressed? state config)=, returns +~#t~ if /any/ action in the config's action list transitioned to +pressed this frame — useful for "press any key to continue" screens. + +The call shape throughout the demos is: + +#+begin_src scheme +(input-held? (game-input game) 'left) +(input-pressed? (game-input game) 'a) +(input-released? (game-input game) 'start) +#+end_src + +** Customising the input config + +Build a replacement config with =make-input-config= and pass it to +=make-game= via the =input-config:= keyword: + +#+begin_src scheme +(define my-input + (make-input-config + actions: '(up down left right fire pause quit) + keyboard-map: '((up . up) (w . up) + (down . down) (s . down) + (left . left) (a . left) + (right . right) (d . right) + (space . fire) + (p . pause) + (escape . quit)) + joy-button-map: '() + controller-button-map: '() + joy-axis-bindings: '() + controller-axis-bindings: '() + deadzone: 8000)) + +(make-game + title: "Custom Controls" + input-config: my-input + ...) +#+end_src + +All seven slots are required, but any of the map/binding slots can be +empty (~'()~) if you don't want that input type. The =actions= list +determines which symbols =input-any-pressed?= sweeps, and seeds the +initial =input-state= with an entry per action. + +If you want to *extend* the defaults instead of replacing them, pull +each slot off =*default-input-config*= and cons your extra entries: + +#+begin_src scheme +(define my-input + (make-input-config + actions: (input-config-actions *default-input-config*) + keyboard-map: (cons '(space . a) + (input-config-keyboard-map *default-input-config*)) + joy-button-map: (input-config-joy-button-map *default-input-config*) + controller-button-map: (input-config-controller-button-map *default-input-config*) + joy-axis-bindings: (input-config-joy-axis-bindings *default-input-config*) + controller-axis-bindings: (input-config-controller-axis-bindings *default-input-config*) + deadzone: (input-config-deadzone *default-input-config*))) +#+end_src + +* Common patterns + +** Arrow-key movement + +Straight from =demo/getting-started.scm=: test each horizontal and +vertical direction independently, pick a signed step, and write back +the updated position. + +#+begin_src scheme +(define +speed+ 2) + +(define (move-player player input) + (let* ((x (entity-ref player #:x 0)) + (y (entity-ref player #:y 0)) + (dx (cond ((input-held? input 'left) (- +speed+)) + ((input-held? input 'right) +speed+) + (else 0))) + (dy (cond ((input-held? input 'up) (- +speed+)) + ((input-held? input 'down) +speed+) + (else 0)))) + (entity-set (entity-set player #:x (+ x dx)) #:y (+ y dy)))) +#+end_src + +** Jump on press vs move while held + +The platformer distinguishes a /continuous/ action (running left/right +while =left=/=right= are held) from an /edge-triggered/ action +(jumping exactly once when =a= is first pressed): + +#+begin_src scheme +(define (player-vx input) + (cond ((input-held? input 'left) -3) + ((input-held? input 'right) 3) + (else 0))) + +(define (update-player player input) + (let* ((jump? (and (input-pressed? input 'a) + (entity-ref player #:on-ground? #f))) + (player (entity-set player #:vx (player-vx input)))) + (if jump? + (entity-set player #:ay (- *jump-force*)) + player))) +#+end_src + +Using =input-pressed?= instead of =input-held?= prevents the player +from "spamming" the jump simply by keeping =a= depressed — the action +must be released and re-pressed to fire again. + +The same pattern shows up in =demo/shmup.scm= for firing bullets: + +#+begin_src scheme +(if (input-pressed? input 'a) + (values updated (list (make-bullet ...))) + (values updated '())) +#+end_src + +** Controller + keyboard simultaneously + +The default config registers bindings for keyboard, joystick, /and/ +game controller at the same time — no configuration switch, no "input +device" concept. Whichever device fires an event first on a given +frame sets the corresponding action, and the next frame reflects it. +You get controller support for free as long as you keep the default +config (or carry its =joy-*= / =controller-*= slots forward into your +custom config). + +=game-run!= opens every connected game controller at startup, and +=handle-controller-device= opens any controller hot-plugged during the +session, so no extra wiring is needed. + +** Remapping a single action + +To let players press =space= for =start= instead of =return=, override +just the keyboard map (keeping the rest of the defaults): + +#+begin_src scheme +(define my-input + (make-input-config + actions: (input-config-actions *default-input-config*) + keyboard-map: + '((w . up) (up . up) + (s . down) (down . down) + (a . left) (left . left) + (d . right) (right . right) + (j . a) (z . a) + (k . b) (x . b) + (space . start) ;; was (return . start) + (escape . quit)) + joy-button-map: (input-config-joy-button-map *default-input-config*) + controller-button-map: (input-config-controller-button-map *default-input-config*) + joy-axis-bindings: (input-config-joy-axis-bindings *default-input-config*) + controller-axis-bindings: (input-config-controller-axis-bindings *default-input-config*) + deadzone: (input-config-deadzone *default-input-config*))) + +(make-game + title: "Remapped Start" + input-config: my-input + ...) +#+end_src + +Multiple keys can map to the same action (the defaults already do +this: both =w= and =up= trigger =up=), so another approach is to add +=(space . start)= /alongside/ the existing =(return . start)= entry +instead of replacing it. + +* Demos + +- [[file:../demo/getting-started.scm][Getting started]] (=bin/demo-getting-started=) — arrow-key movement with =input-held?=. +- [[file:../demo/platformer.scm][Platformer]] (=bin/demo-platformer=) — mixes =input-held?= for running with =input-pressed?= for jumping. +- [[file:../demo/shmup.scm][Shmup]] (=bin/demo-shmup=) — uses =input-pressed?= on =a= to fire bullets exactly once per button press. + +* See also + +- [[file:guide.org][Getting-started guide]] — the full walkthrough that + builds the minimal input example above. +- [[file:entities.org][Entities]] — the =#:input-map= entity key and + =apply-input-to-entity= hook for data-driven movement. +- [[file:physics.org][Physics]] — how velocities set by input feed into + the built-in physics pipeline. +- [[file:scenes.org][Scenes]] — how =game-input= fits alongside + =game-scene= in the =update:= hook. -- cgit v1.2.3