Repository navigation
Gamepad plugin: on-screen touch controls, reported as a gamepad - #1799
Merged
Merged
Conversation
`input.addVirtualGamepad(pad, index)` puts a pad the game drives itself, such as on-screen touch controls, in a gamepad slot. The poll reads it with the pad the browser reports in that slot, so everything bound with `bindGamepad` answers to either, and `activeGamepad` / vibration see the virtual pad when the slot holds no real one. No GAMEPAD_CONNECTED is sent for it, so a game can still use that event to hide its touch controls when a real pad shows up. This is what the gamepad plugin will build on. Two fixes in the same loop, both reproduced on master first: - a pad gone while a button or the stick is held now lets go of its keys (a slot with no pad was skipped, leaving them pressed); taking out a virtual pad relies on this - a stick bound on one side lets go of its key when it comes to rest just past the centre on the unbound side (the poll returned before the release) A key is released with the same source it was pressed with, now kept on the binding, as a slot can change between its real and virtual pad. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
A new package, @melonjs/gamepad-plugin, built from the plugin template and following the in-repo plugins. It draws sticks and buttons and puts a virtual pad in a gamepad slot (input.addVirtualGamepad), so a game's bindGamepad bindings answer to the touch controls as to a real pad. - one or two analog sticks with a deadzone; the left one presses the D-pad in 8 directions. Fixed or floating (centered where the thumb lands). Keys can be pressed instead, for keyboard-only games - any number of buttons, FACE_1, FACE_2... by default or a key; a game can relabel, dim or hide them at run time - each finger followed by its own pointer id from DOM pointer events, caught in the capture phase on the canvas's parent: a touch starting on the controls never reaches the engine or the game, and a stick keeps its finger when the thumb drifts off its base - drawn after the stage, over every camera and every renderable, whatever their z, and so never inside a minimap; nothing to load, only the renderer's shapes; kept across stage changes - `enabled` (device.touch by default) and `visible`; hidden while a real pad is in use (hideOnGamepad), back when it is unplugged or the screen is touched - input.vibrateGamepad vibrates the phone through navigator.vibrate - the minimum engine version comes from peerDependencies (>= 20.12.0, the release with addVirtualGamepad) 33 browser tests on the Canvas renderer, including that touches are kept from the game and the controls are drawn over a full-screen overlay; checked by hand on WebGL. Also: README, CHANGELOG, the API docs entry, the melonjs-input skill (with symptom entries), CI test and publish wiring, and the root README plugin list. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
`pnpm -F @melonjs/gamepad-plugin visual` serves visual/, a page with the controls over a small scene, to try on a desktop or a phone (Vite serves it on the network too). Not part of the published package. - a dot the left stick moves (analog through LX/LY, digital through the D-pad), bound as a game binds a real pad - a readout of the plugin's state and of what the engine receives: the axes on GAMEPAD_UPDATE, the actions held, the active slot, the real pads, and how many taps reached the game (a tap off the controls leaves a ring) - toggles for enabled, two sticks, floating, hide on pad, dim / hide / relabel a button, a shade at a very high z, a minimap camera, phone vibration, and the renderer (?renderer=canvas|webgl|webgpu) Checked in headless Chromium with real touch events on Canvas and WebGL: two fingers on the stick and A at once reach the engine, the game gets none of those touches but gets a tap in the open, the controls stay over the shade, and the minimap draws the world without them. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
So it can pick its renderer where only the address hash reaches the page. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Plugin - touches are placed against this application's canvas from clientX/Y, so they land right on a scrolled page (globalToLocal takes page coordinates) and on a page with several applications - hidden for a real pad again when the player goes back to it after touching the screen: the pads are watched each frame while one is connected, not only on GAMEPAD_CONNECTED - the version, an initialized app and a free slot are checked before anything is hooked; a second set of controls on a slot throws even while switched off - `ready` checks isInitialized and the viewport (a destroyed app keeps both), instead of a guard that was always true - alpha and deadzone clamped with math.clamp: Canvas ignored an alpha above 1 when opacity was over 0.5 - a label keeps one Text, resized with setFont on layout and relabel - a new vibration effect stops the one still running - the control classes are exported as types; trimmed comments, a split CHANGELOG, README rows; the test page lets #hash win over ?renderer= Engine - one virtual pad cannot go in two slots; @throws lists the RangeError - the connect listeners reuse hasGamepadApi(); reworded comments and CHANGELOG entries (a one-sided stick also stuck when it came back to exactly 0) - tests: a negative-side stick back to 0, a keyboard key and a pad on one action, a remapped pad unplugged mid-press next to a virtual pad Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…he README As the debug plugin's README: the banner and badges under the title, and a screenshot of the controls (a stick held and leaning, A pressed), taken from the visual test page and shipped in the package. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
addVirtualGamepad, removeVirtualGamepad and VirtualGamepad stay in input/gamepad.ts but are @ignore @internal: out of the docs and the published types, there for the gamepad plugin only. The plugin declares the two calls itself and reaches them through one cast. Their changelog entry and skill paragraph go; the two gamepad fixes stay. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
In the API docs home page, llms.txt, and the melonjs, melonjs-plugins (with its own section and trigger words) and AGENTS skills. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…on-ao5snz # Conflicts: # packages/melonjs/CHANGELOG.md
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A new official plugin,
@melonjs/gamepad-plugin: on-screen sticks and buttons drawn over the game and read by the engine as a pad in a gamepad slot. A game'sinput.bindGamepadbindings work with them unchanged, so one set of bindings covers a real pad and a touch screen.Plugin (
packages/gamepad-plugin)FACE_1,FACE_2… or a key), which a game can relabel, dim or hide at run timez, never inside a minimap, kept across stage changesenabled: device.touch), hidden while a real pad is in use and back on the next touch;visiblelets a game word its promptsinput.vibrateGamepadvibrates the phone wherenavigator.vibrateexistspnpm -F @melonjs/gamepad-plugin visual)Engine (
packages/melonjs/src/input/gamepad.ts)@ignore @internal, so not in the docs or the published types) that puts a virtual pad in a gamepad slot; the poll reads it together with a real pad in the same slot. The plugin needs melonJS 20.12 for it, checked at registrationWiring and docs: CI test and publish jobs, root README and API docs plugin lists,
llms.txt, themelonjs,melonjs-plugins,melonjs-inputandAGENTSskills.Two design choices worth a look:
Stage.prototype.draw(restored ondestroy()), so the controls are drawn inside the frame after every camera, before the flushType of change
Checklist
pnpm lintpasses)pnpm testpasses): the gamepad, input and keyboard specs and the plugin's own suite pass. In the full suite, 2 tests failed in my sandbox: a text glyph pixel check that fails the same way on master's code, and two timeouts that pass on a rerunpnpm build)Checked by hand in headless Chromium with real touch events on Canvas and WebGL. WebGPU is not verified (no GPU adapter in my sandbox): the visual test page has a renderer picker (
#webgpu) for that.Related issues
Part of #1719. Still to do from that issue: the platformer, isometric dungeon and alex4 examples moved onto the plugin (the platformer's
entities/controls.tsremoved).Generated by Claude Code