UNPKG

gamepad-node

Version:

Browser Gamepad API implementation for Node.js with native SDL2 bindings

153 lines (101 loc) 5.9 kB
# gamepad-node [![npm version](https://img.shields.io/npm/v/gamepad-node.svg)](https://www.npmjs.com/package/gamepad-node) [![CI](https://github.com/monteslu/gamepad-node/actions/workflows/ci.yml/badge.svg)](https://github.com/monteslu/gamepad-node/actions/workflows/ci.yml) [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) W3C Gamepad API for Node.js using SDL2. Works exactly like the browser API, but better - every controller gets `mapping: "standard"`, not just the handful browsers recognize. ## Features - Browser-compatible API - `navigator.getGamepads()` works exactly like in browsers - Every controller gets `mapping: "standard"` - not just the 20-30 browsers recognize - **Positional button mapping** - buttons mapped by physical position (N/S/E/W), not labels (A/B/X/Y) - 2100+ controllers via SDL2's community database - 321 more via EmulationStation configs (Knulli + Batocera - position-aware!) - Generic fallback for everything else - Hot-plug support with connect/disconnect events - Vibration/rumble support with automatic hardware detection - CLI tester with positional labels (N/S/E/W) - Zero config - SDL2 downloads automatically ## Why this exists Browsers only give `mapping: "standard"` to about 20-30 controllers. Everyone else gets unpredictable button mappings and has to implement config screens. That sucks for game developers. This library ensures **every controller** gets standard mappings. Your game code stays simple. ## Install ```bash npm install gamepad-node ``` SDL2 is installed automatically by @kmamal/sdl. No compilation, no config. ## Usage ```javascript import { installNavigatorShim } from 'gamepad-node'; installNavigatorShim(); // Same API as browsers setInterval(() => { const gamepads = navigator.getGamepads(); for (const gamepad of gamepads) { if (!gamepad) continue; if (gamepad.buttons[0].pressed) { console.log('A button pressed'); } const leftStickX = gamepad.axes[0]; const leftStickY = gamepad.axes[1]; } }, 16); ``` ### Events ```javascript const manager = installNavigatorShim(); manager.on('gamepadconnected', (event) => { console.log('Connected:', event.gamepad.id); }); manager.on('gamepaddisconnected', (event) => { console.log('Disconnected:', event.gamepad.id); }); ``` ### Rumble ```javascript const gamepad = navigator.getGamepads()[0]; // vibrationActuator is null if controller doesn't support rumble if (gamepad?.vibrationActuator) { await gamepad.vibrationActuator.playEffect('dual-rumble', { duration: 200, strongMagnitude: 1.0, weakMagnitude: 0.5 }); } ``` ## Test your controllers ```bash npx gamepad-node ``` Shows all buttons, triggers, sticks, and d-pad in real-time. Face buttons labeled **N/S/E/W** (North/South/East/West) for positional clarity. Press R to test rumble (if supported). ## How it works Four-tier fallback system with **positional mapping** priority: 1. **SDL_GameController with rumble** - Keep as-is for rumble support 2. **SDL_GameController without rumble + db.json match** - Force joystick mode for position-aware EmulationStation mappings 3. **EmulationStation database** (321 controllers) - Position-based remapping using community configs 4. **Fallback** - Generic Xbox 360 / PS4 style mapping ### Why positional mapping matters The W3C Gamepad API spec defines buttons by **physical position** (0=bottom, 1=right, 2=left, 3=top), not by label. But manufacturers print different labels at the same positions: - **Xbox**: South=A, East=B, West=X, North=Y - **Nintendo/8BitDo**: South=B, East=A, West=Y, North=X SDL's mapping database uses **label-based** matching (maps "A button"), which breaks for Nintendo-layout controllers. EmulationStation's database uses **position-based** matching (maps "south button"), which works universally. When possible, we use position-aware mappings from EmulationStation. This ensures button 0 is always the **bottom** button, regardless of what letter is printed on it. See [docs/CONTROLLER_VS_JOYSTICK.md](./docs/CONTROLLER_VS_JOYSTICK.md) for technical details, or [docs/MAPPED_CONTROLLERS.md](./docs/MAPPED_CONTROLLERS.md) for the full controller list. ## Platform support Works on macOS (Intel + Apple Silicon), Linux (x64 + arm64), and Windows (x64). SDL2 binaries are downloaded automatically. ## Why "better than browsers"? Most browsers only recognize about 20-30 controllers for standard mapping. Try plugging in a Logitech Precision or some retro USB adapter - you'll get `mapping: ""` and buttons all over the place. This library gives **every controller** standard mappings using position-aware databases. Your game works with anything, zero config required. **Bonus:** We also correctly detect rumble support. Browsers often expose `vibrationActuator` even when hardware doesn't support it - we set it to `null` if rumble isn't available. ## Development Pure JavaScript on top of @kmamal/sdl, no build step. Run `npm install` and you're good. ```bash npm test # Basic test npm run test:events # Events & rumble npm run test:unit # Unit tests npx gamepad-node # Interactive tester ``` ## Terminal Gaming I'm building this as part of a terminal gaming platform. Combine gamepad-node with webaudio-node and some clever half-block rendering, and you can make full games that run via `npx`. Check out [docs/TERMINAL_GAMING_PLATFORM.md](./docs/TERMINAL_GAMING_PLATFORM.md) if that sounds interesting. ## Credits Built on [@kmamal's SDL2 bindings](https://github.com/kmamal/node-sdl), which made this whole thing possible. Also using controller databases from [SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB), [Knulli](https://knulli.org/), and [Batocera](https://batocera.org/). ## License ISC