#+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.