gamepad-node
Version:
Browser Gamepad API implementation for Node.js with native SDL2 bindings
153 lines (101 loc) • 5.9 kB
Markdown
[](https://www.npmjs.com/package/gamepad-node)
[](https://github.com/monteslu/gamepad-node/actions/workflows/ci.yml)
[](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.
- 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);
```
```javascript
const manager = installNavigatorShim();
manager.on('gamepadconnected', (event) => {
console.log('Connected:', event.gamepad.id);
});
manager.on('gamepaddisconnected', (event) => {
console.log('Disconnected:', event.gamepad.id);
});
```
```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
});
}
```
```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.
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