Repository navigation
Examples: the platformer's touch controls come from the gamepad plugin - #1800
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
The platformer registers @melonjs/gamepad-plugin (a stick and an A button) and drops its own entities/controls.ts. The player's existing gamepad bindings drive it: the stick walks (D-pad and LX), A jumps (FACE_1). It shows on touch screens, and on a desktop while the debug panel is open, as the old joypad did. The examples depend on the plugin through the workspace, keep it out of Vite's pre-bundling like the other plugins, and the docs workflow builds it before building the examples site. Part of #1719. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…anvas On a phone, a long press on the stick or a button started a text selection over the canvas, the iOS image menu or the Android context menu: cancelling the pointer events does not stop those. While the controls listen, the canvas gets user-select: none, -webkit-touch-callout: none and a transparent tap highlight (put back on destroy, in reverse as user-select and its -webkit- form alias), a touchstart that lands on a control is cancelled, and the context menu is blocked while the controls show. Touches elsewhere are left alone. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
The stick read against its whole base, while the thumb drawn under the finger travels about half that, so the thumb lagged the finger and the D-pad (past half the reading) only pressed with the finger most of the way to the rim. It now reads against the thumb's travel (`reach`), and the thumb is drawn under the finger as far as it goes. New stick option `dpadThreshold` (0.5 by default) sets how far the stick leans before it presses the D-pad and its keys. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…ress - `dpadThreshold` is measured on how far the thumb has travelled, so the default 0.5 presses the D-pad with the thumb half way out; it was measured after the deadzone, which put it at about 55% - `haptics` (10 ms by default, 0 for none): a button press, or a finger sliding onto the next button, gives a short vibration for the feel of a real button, where navigator.vibrate exists (Android, not iOS). It is skipped while the game plays a rumble, which it would cut short Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
A touch only counts as the player's own once it ends, and browsers refuse a vibration before that, logging an error. The tick is skipped while navigator.userActivation says no tap has happened yet. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Any browser with navigator.vibrate: Android browsers, and apps that provide it from their web view (the Claude iOS app does); not Safari or the other iPhone browsers, which all use WebKit. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
18 tasks
5 of 10 tasks
obiot
added a commit
that referenced
this pull request
Oct 30, 2026
Also lists what came in #1800: dpadThreshold, no selection on a long press, and the haptics option. Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru Co-authored-by: Claude <[email protected]>
8 of 10 tasks
obiot
added a commit
that referenced
this pull request
Nov 12, 2026
…nvas, proportional minimap (#1855) * Canvas meshes: textures, depth order, palette shading, no seams The Canvas renderer's drawMesh, measured against WebGL on the examples: - A mesh with baked material colors (vertexColors, from an MTL) was always solid-filled, so a textured OBJ + MTL model came out as a white silhouette. The texture is drawn, multiplied by each triangle's color and the tint, as the GPU renderers do; only a mesh with no texture (the 1x1 white fallback) is filled. - The painter's sort took the largest projected Z as the farthest, but projectVertices puts the near plane at +zScale: meshes drew their far side over their near side. It now sorts on each triangle's farthest point, smallest Z first. The back-face test was right, and is now covered. - Palette models (all 568 triangles of a Kenney animal) map each face onto a line of one texture. Such a triangle was filled with the one texel under its first vertex; it is now filled with a gradient of the texels along its UV line, as the GPU interpolates it, and a point is one color. A UV area under a quarter texel counts as degenerate, instead of exactly zero, so near-zero ones no longer smear. - Opaque triangles also stroke their edge in their own fill, covering the faint seam two antialiased neighbours leave. - Texture pixels are read once per image and cached, instead of one getImageData readback per triangle per frame, which froze the ground shadows example on Canvas. Tests: canvas-mesh.spec.js draws and reads back, including an OBJ + MTL palette model through the loader and a colored cube through the real projection; each fix fails its test when reverted. Skills updated where they described the old Canvas behaviour. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx * Examples: 2D-camera meshes run on Canvas; Camera3d ones say they cannot - The 3D Mesh and Multi-material OBJ examples draw their meshes under a 2D camera, which the Canvas renderer supports, at about 60 fps; they no longer refuse it. - Billboard, Instanced Forest, glTF Scene, glTF Animated Model, Ground Shadows and Night City need Camera3d, which needs a depth buffer. On Canvas they showed an empty sky (and Instanced Forest ran at about 1 fps); they now stop with their existing "needs WebGL" message, as AfterBurner, Earth Defender, Jungle Rabbit and Material Textures do. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx * Examples: proportional minimap; the matter platformer uses the gamepad plugin - Both platformers' minimap is 18% of the canvas width (about the old 180 px at the default size), zoomed to the whole level, and resized through Camera2d#setViewport on CANVAS_ONRESIZE, which an orientation change also emits. In a portrait window it no longer covers the audio and fullscreen buttons. - The matter platformer's touch controls come from @melonjs/gamepad-plugin, as the built-in platformer's have since #1800: its own VirtualJoypad (controls.ts, removed) was laid out for landscape and covered the jump button in portrait. The player's gamepad bindings drive it unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx * Docs: meshes target the GPU backends, Canvas is a last resort - Mesh, InstancedMesh, CanvasRenderer#drawMesh, the video.CANVAS setting, the README, DOC_README and the 3D and renderer-backends skills now say it the same way: 3D is built for WebGL 2 and WebGPU; a mesh under a 2D camera still draws on Canvas as a last-resort fallback (CPU-projected, unlit, one texture, slower with every triangle), and a Camera3d scene does not draw there at all. Mesh and the 3D skill gain an example of checking supportsDepthBuffer after init(). - Two Mesh comments still described the old Canvas solid fill. - Seven class descriptions were not on the API site: a helper or a typedef had been inserted between each doc comment and its class (Mesh since #1662), so TypeDoc showed Mesh, Light3d and Trigger with Renderable's description and InstancedMesh, Color, Noise and ShaderEffect with none. Each comment is back on its class; TypeDoc's warnings are unchanged (208 before and after). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
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
The platformer example now gets its touch controls from
@melonjs/gamepad-plugin(#1799) and drops its ownentities/controls.ts(240 lines).createGame.tsregisters the plugin with a stick and an A buttonLX), A jumps (FACE_1)play.tssetsenabled)Wiring:
packages/examples/package.json: the plugin as a workspace dependency, like the other pluginspackages/examples/vite.config.ts: the plugin kept out of Vite's pre-bundling, like the debug and tiled-inflate plugins, so a rebuild is picked up.github/workflows/docs.yml: the plugin built before the examples sitePlugin changes found while trying it on a phone (
packages/gamepad-plugin):user-select: none,-webkit-touch-callout: noneand a transparent tap highlight (put back ondestroy()), atouchstartthat lands on a control is cancelled, and the context menu is blocked while the controls showreach), the thumb is drawn under the finger, and a new stick optiondpadThreshold(0.5 by default: half the thumb's travel) sets when the D-pad presseshaptics(10 ms by default, 0 for none) vibrates the phone on each button press, where the browser supports it. It is skipped while the game plays a rumble, and before the page's first completed tap, when browsers refuse itTests cover each (45 plugin tests).
Checked on the examples dev server in headless Chromium emulating a phone, with real touch events: the controls show over the level and not in the minimap; the thumb a little past half way walks the player; holding the stick while tapping A jumps him over the crate and the water; letting go off the stick, or a cancelled touch, stops him; the sound button still gets its tap; each A press asks for one 10 ms vibration. No console errors. Checked on a phone by hand: long press, rotation, stick + repeated A taps, and the button vibration.
The diff against master is these files. The commit list also shows #1799's commits: the branch was reused, and they were recorded with an
oursmerge that adds no content (that branch tip and the squash commit have identical trees).Type of change
Checklist
pnpm lintpasses)pnpm testpasses)pnpm build)Related issues
Part of #1719. Still to do: the isometric dungeon and alex4 examples, and
platformer-matter, which has its own copy ofcontrols.ts.🤖 Generated with Claude Code
https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru