# Virtual controls On-screen touch controls for keyboard-only games, provided by the shell. **Status:** prototype, enabled for Malaga only. --- ## The problem A game written for desktop reads arrow keys and space. On a phone there is no keyboard, so the game renders and then does nothing. Three of our five games are in this state. The correct fix is native touch support, which lives in the game and is the developer's job. But that is a per-developer, per-game effort, and until it happens the game is dead weight on mobile. ## The mechanism The shell draws a control pad over the game iframe and turns presses into synthetic keyboard events delivered into the game document. This works because games are same-origin and the iframe carries `allow-same-origin`, so the shell can reach `iframe.contentWindow`. p5 registers its key listeners on that window and reads `event.which` to populate the key state that `keyIsDown()` queries. It never inspects `isTrusted`, so a synthetic event is indistinguishable from a real one. One wrinkle: `keyCode` and `which` are legacy read-only properties that the `KeyboardEvent` constructor ignores, yet they are exactly what input code reads. They have to be redefined on the instance after construction. Events are dispatched on the game's `document.body` rather than the window, so they propagate the same way a real key event does and reach listeners at either level. ## The game is never modified This is the important part. Everything runs in the shell. `public/games//` stays byte-for-byte as the developer shipped it. The one thing we depend on is the key mapping, and **we never guess it** — it is declared in the catalogue entry: ```js virtualControls: { left: [ { label: "◀", ariaLabel: "Sinistra", key: "ArrowLeft", code: "ArrowLeft", keyCode: 37, mode: "hold" }, { label: "▶", ariaLabel: "Destra", key: "ArrowRight", code: "ArrowRight", keyCode: 39, mode: "hold" } ], right: [ { label: "FUOCO", ariaLabel: "Spara", key: " ", code: "Space", keyCode: 32, mode: "hold", wide: true } ] } ``` Omit `virtualControls` and no pad is shown. Controls only render on touch-first devices (`pointer: coarse`). ### Button modes | Mode | Behaviour | Use when | |---|---|---| | `hold` | keydown on press, keyup on release | The game polls key state (`keyIsDown`) | | `tap` | keydown then keyup after 40ms | One discrete action per press | | `repeat` | keydown re-fired while held (`repeatMs`, default 180) | The game fires on the discrete `keyPressed` callback and should autofire | Malaga polls with its own cooldown, so `hold` on fire is correct. Asteroiz shoots from `keyPressed`, so it will need `repeat`. --- ## This is a fallback, not a standard A pad drawn over a canvas is worse than controls designed for touch. It covers part of the play area, it has no analogue precision, and it cannot express gestures. Native touch support therefore stays a requirement in [`SUBMISSION-RULES.md`](submission-rules.md). Virtual controls exist so that an otherwise good desktop game is playable on a phone while its developer does the real work — not so that the real work can be skipped. There is a genuine incentive risk here: a fallback that works reduces the pressure to build the proper thing. Games with native touch should be favoured in grid placement, and virtual controls should never be presented to developers as "mobile support, done". ## Known limitations **Depends on `allow-same-origin`.** Once third-party games run under a stricter sandbox, direct dispatch stops working. The replacement is SDK-mediated input over `postMessage`, where the SDK inside the iframe generates the events on its own window. `VirtualControls.mount()` / `unmount()` is the whole public API precisely so that swap stays internal. **Buttons cover the canvas.** No layout negotiation with the game exists. **No analogue input.** Fine for arrow-key games, wrong for anything wanting a continuous axis. **Games with three simultaneous inputs are awkward.** Asteroiz needs rotate, thrust and fire together; four buttons on a phone screen is playable but not good. ## Testing Synthetic dispatch is verified against p5's actual key-handling code — the listener registration and the `_downKeys[e.which]` assignment were read out of `p5.min.js`, not assumed. Multi-touch, independent release and key isolation all behave correctly under jsdom. Real-device testing on iOS Safari and Android Chrome is still required before this leaves prototype status.