aboutsummaryrefslogtreecommitdiff
path: root/docs/input.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/input.org')
-rw-r--r--docs/input.org345
1 files changed, 345 insertions, 0 deletions
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.