# Input and camera ## One action enum Branch on device at the mapping layer only. Gameplay code must never ask which device is connected. ```rust enum Action { MoveX(f32), MoveY(f32), Jump, Fire, Interact, Pause } ``` Sources: `RawInput` and `ControlSource` in `makepad-game-blocks`; `GameInputState` / `GamepadState` from `makepad_platform::event::game_input`; `DriveInput` for vehicles, with `drive_input_for(world, player)` in `makepad-game-session` for the networked case. ## Per-device rules **Keyboard** — normalise diagonals (`(1,1)` normalised, or diagonal movement is 41% faster; this ships more often than it should). Support held keys via state, not key-repeat events. **Gamepad** — radial deadzone ~0.15 (not per-axis, which produces a square deadzone and cross-shaped drift), rescale the remainder to full range so the first degree past the deadzone is not a jump to 0.15. Triggers are analog; use the range. **Touch** — no hover, no right-click, and the thumb covers what it presses. Virtual sticks are relative to first-touch, not to a fixed screen point. Minimum 44 px targets. Every action needs a touch path or a mobile claim is false. **XR** — see `apps/arcade/src/xr_input.rs` and `xr/`. Controller pose is not a gamepad axis: motion comes from the rig, aim from the hand. Snap-turn by default (smooth turning causes sickness); never move the camera without player input; keep HUD off the near plane — world- or wrist-anchored, not face-locked. ## Buffering and forgiveness Store timestamps, not booleans: ```rust if action_pressed(Action::Jump) { jump_pressed_at = now; } let buffered = now - jump_pressed_at < 0.12; let coyote = now - left_ground_at < 0.10; if buffered && (on_floor || coyote) { do_jump(); jump_pressed_at = -1.0; } ``` Clear the buffer on use, or one press yields two jumps. ## Camera `CameraRig` in `makepad-game-render` owns camera state; `scene_state(..)` builds the `SceneState3D` and `set_pass_camera(cx, &pass, &scene)` applies it. Script route: `camera`, `cam_dist`, `cam_yaw`, `cam_pitch`, `cam_fov`, `cam_shake`, `cam_dragging`, `chase`, `face`. | Rig | Use | Notes | | --- | --- | --- | | Chase | vehicles, third-person action | lag on position, lead on velocity | | Orbit | sandbox, inspection | drag to orbit, wheel to zoom (the `arcade_view` pattern) | | Fixed / isometric | strategy, puzzle | no lag; readability over drama | | First person | shooter, XR | no smoothing on look, ever | **Lag and lookahead.** Position lags the target by ~0.1–0.2 s; the look point leads by velocity × ~0.3 s. Lag alone feels sluggish, lookahead alone feels twitchy; together they feel intentional. **FOV.** 60° desktop third-person, 75–90° first-person. Widen with speed (a few degrees) for a sense of acceleration — this is nearly free and very effective. In XR, FOV belongs to the headset: never set it. **Collision.** Sphere-cast from target to camera and pull in on a hit, then ease back out slowly. Snapping out is more noticeable than snapping in. **Shake.** Trauma-based: add trauma on impact, decay it, and use `trauma²` for amplitude so small hits stay subtle. Cap total displacement, and never shake in XR. ## Screen-space aiming Convert a pointer to a world ray through the scene camera, then `cast_ray` with `raycast_filter()`. Filter out the player's own capsule. ## Testing input `makepad-test` drives the real thing: `press_key`, `press_key_with_modifiers`, `type_text`, `touch_down`, plus `wait_for_log_contains` to synchronise. Test the mapping layer's output (the action stream) in a unit test, and the end-to-end path in one driven test — not every action end-to-end, which is slow and proves the same thing repeatedly.