API Gamepad pour les jeux web
L’API Gamepad apporte aux jeux web une prise en charge des manettes digne d’une console. Prenez en charge les manettes Xbox, PlayStation, Switch Pro et génériques.
1) Détecter les manettes
js
window.addEventListener('gamepadconnected', (e) => {
console.log('Manette connectée :', e.gamepad.id)
console.log('Index :', e.gamepad.index)
console.log('Boutons :', e.gamepad.buttons.length)
console.log('Axes :', e.gamepad.axes.length)
})
window.addEventListener('gamepaddisconnected', (e) => {
console.log('Manette déconnectée :', e.gamepad.id)
})2) Interroger l’état des manettes
L’état des manettes doit être interrogé régulièrement, et non géré par des événements :
js
function pollGamepads() {
const gamepads = navigator.getGamepads()
for (const gamepad of gamepads) {
if (gamepad) {
processGamepad(gamepad)
}
}
}
// À appeler dans votre boucle de jeu
function gameLoop() {
pollGamepads()
update()
render()
requestAnimationFrame(gameLoop)
}3) Mappage standard des boutons
Le mappage « standard » pour la plupart des manettes Xbox et PlayStation :
js
const BUTTONS = {
A: 0, // Croix (PS) / A (Xbox)
B: 1, // Cercle (PS) / B (Xbox)
X: 2, // Carré (PS) / X (Xbox)
Y: 3, // Triangle (PS) / Y (Xbox)
LB: 4, // L1 (PS) / LB (Xbox)
RB: 5, // R1 (PS) / RB (Xbox)
LT: 6, // L2 (PS) / LT (Xbox)
RT: 7, // R2 (PS) / RT (Xbox)
SELECT: 8, // Share (PS) / Back (Xbox)
START: 9, // Options (PS) / Start (Xbox)
L3: 10, // Appui sur le stick gauche
R3: 11, // Appui sur le stick droit
DPAD_UP: 12,
DPAD_DOWN: 13,
DPAD_LEFT: 14,
DPAD_RIGHT: 15,
HOME: 16, // Bouton PS / bouton Xbox
}
const AXES = {
LEFT_X: 0,
LEFT_Y: 1,
RIGHT_X: 2,
RIGHT_Y: 3,
}4) Lire les boutons et les sticks
js
function processGamepad(gamepad) {
// Boutons (numériques ou analogiques)
const jump = gamepad.buttons[BUTTONS.A].pressed
const shoot = gamepad.buttons[BUTTONS.X].pressed
// Gâchettes (analogiques, de 0 à 1)
const leftTrigger = gamepad.buttons[BUTTONS.LT].value
const rightTrigger = gamepad.buttons[BUTTONS.RT].value
// Sticks (analogiques, de -1 à 1)
const leftX = gamepad.axes[AXES.LEFT_X]
const leftY = gamepad.axes[AXES.LEFT_Y]
const rightX = gamepad.axes[AXES.RIGHT_X]
const rightY = gamepad.axes[AXES.RIGHT_Y]
return { jump, shoot, leftTrigger, rightTrigger, leftX, leftY, rightX, rightY }
}5) Gérer la zone morte
Les sticks restent rarement exactement à 0 :
js
function applyDeadzone(value, deadzone = 0.15) {
if (Math.abs(value) < deadzone) return 0
// Remettre à l’échelle la plage restante entre 0 et 1
const sign = Math.sign(value)
const adjusted = (Math.abs(value) - deadzone) / (1 - deadzone)
return sign * adjusted
}
function getStickInput(gamepad) {
return {
leftX: applyDeadzone(gamepad.axes[AXES.LEFT_X]),
leftY: applyDeadzone(gamepad.axes[AXES.LEFT_Y]),
rightX: applyDeadzone(gamepad.axes[AXES.RIGHT_X]),
rightY: applyDeadzone(gamepad.axes[AXES.RIGHT_Y]),
}
}6) Zone morte radiale, mieux adaptée aux déplacements en 2D
js
function applyRadialDeadzone(x, y, deadzone = 0.15) {
const magnitude = Math.sqrt(x * x + y * y)
if (magnitude < deadzone) {
return { x: 0, y: 0 }
}
const normalized = {
x: x / magnitude,
y: y / magnitude,
}
const adjusted = (magnitude - deadzone) / (1 - deadzone)
return {
x: normalized.x * adjusted,
y: normalized.y * adjusted,
}
}
// Utilisation
const stick = applyRadialDeadzone(
gamepad.axes[AXES.LEFT_X],
gamepad.axes[AXES.LEFT_Y]
)
player.vx = stick.x * player.speed
player.vy = stick.y * player.speed7) Gestionnaire de manettes complet
js
class GamepadManager {
constructor() {
this.gamepads = new Map()
this.prevState = new Map()
window.addEventListener('gamepadconnected', (e) => {
this.gamepads.set(e.gamepad.index, e.gamepad)
console.log('Connectée :', e.gamepad.id)
})
window.addEventListener('gamepaddisconnected', (e) => {
this.gamepads.delete(e.gamepad.index)
this.prevState.delete(e.gamepad.index)
})
}
poll() {
const gamepads = navigator.getGamepads()
for (const gp of gamepads) {
if (gp) this.gamepads.set(gp.index, gp)
}
}
getState(index = 0) {
const gp = this.gamepads.get(index)
if (!gp) return null
const stick = applyRadialDeadzone(gp.axes[0], gp.axes[1])
const rightStick = applyRadialDeadzone(gp.axes[2], gp.axes[3])
return {
connected: true,
leftStick: stick,
rightStick: rightStick,
// Boutons de façade
a: gp.buttons[0].pressed,
b: gp.buttons[1].pressed,
x: gp.buttons[2].pressed,
y: gp.buttons[3].pressed,
// Boutons supérieurs
lb: gp.buttons[4].pressed,
rb: gp.buttons[5].pressed,
// Gâchettes (analogiques)
lt: gp.buttons[6].value,
rt: gp.buttons[7].value,
// Croix directionnelle
dpadUp: gp.buttons[12].pressed,
dpadDown: gp.buttons[13].pressed,
dpadLeft: gp.buttons[14].pressed,
dpadRight: gp.buttons[15].pressed,
// Commandes système
select: gp.buttons[8].pressed,
start: gp.buttons[9].pressed,
}
}
isPressed(index, button) {
const gp = this.gamepads.get(index)
return gp?.buttons[button]?.pressed || false
}
wasJustPressed(index, button) {
const current = this.isPressed(index, button)
const prev = this.prevState.get(index)?.[button] || false
return current && !prev
}
endFrame() {
for (const [index, gp] of this.gamepads) {
const state = {}
for (let i = 0; i < gp.buttons.length; i++) {
state[i] = gp.buttons[i].pressed
}
this.prevState.set(index, state)
}
}
}
// Utilisation
const gamepadManager = new GamepadManager()
function update() {
gamepadManager.poll()
const state = gamepadManager.getState(0)
if (state) {
player.vx = state.leftStick.x * player.speed
player.vy = state.leftStick.y * player.speed
if (gamepadManager.wasJustPressed(0, BUTTONS.A)) {
player.jump()
}
}
gamepadManager.endFrame()
}8) Vibrations et retour haptique
js
function vibrate(gamepad, duration = 200, weakMagnitude = 0.5, strongMagnitude = 0.5) {
if (gamepad.vibrationActuator) {
gamepad.vibrationActuator.playEffect('dual-rumble', {
startDelay: 0,
duration,
weakMagnitude, // Moteur haute fréquence
strongMagnitude, // Moteur basse fréquence
})
}
}
// Utilisation
function onPlayerHit() {
const gp = navigator.getGamepads()[0]
if (gp) vibrate(gp, 150, 0.3, 0.6)
}
function onExplosion() {
const gp = navigator.getGamepads()[0]
if (gp) vibrate(gp, 300, 0.8, 1.0)
}9) Plusieurs joueurs
js
class LocalMultiplayer {
constructor(maxPlayers = 4) {
this.maxPlayers = maxPlayers
this.players = []
}
getActivePlayers() {
const gamepads = navigator.getGamepads()
const active = []
for (let i = 0; i < this.maxPlayers; i++) {
if (gamepads[i]) {
active.push({
index: i,
gamepad: gamepads[i],
state: this.getState(gamepads[i])
})
}
}
return active
}
getState(gp) {
// ... comme précédemment
}
}
// Dans la boucle de jeu
const multiplayer = new LocalMultiplayer(4)
function update() {
const players = multiplayer.getActivePlayers()
for (const { index, state } of players) {
updatePlayer(index, state)
}
}10) Solution de repli et contrôles combinés
Prenez en charge à la fois le clavier et la manette :
js
class InputManager {
constructor() {
this.keyboard = new KeyboardInput()
this.gamepad = new GamepadManager()
}
poll() {
this.gamepad.poll()
}
getInput(playerIndex = 0) {
const gpState = this.gamepad.getState(playerIndex)
const kbState = this.keyboard.getState()
// Combiner les contrôles (la manette est prioritaire pour les entrées analogiques)
return {
moveX: gpState?.leftStick.x || (kbState.left ? -1 : kbState.right ? 1 : 0),
moveY: gpState?.leftStick.y || (kbState.up ? -1 : kbState.down ? 1 : 0),
jump: gpState?.a || kbState.space,
shoot: gpState?.x || kbState.z,
pause: gpState?.start || kbState.escape,
}
}
endFrame() {
this.gamepad.endFrame()
this.keyboard.endFrame()
}
}À consulter également
- Gestion des contrôles de jeu
- Jeux web adaptés aux appareils mobiles
- Boucle de jeu Canvas 2D
- Pointer Lock pour les jeux FPS — combiner une manette avec la visée à la souris
- Conception d’un jeu en coopération — concevoir des contrôles pour plusieurs manettes en coopération locale
Ressources externes
- MDN : API Gamepad — référence complète de l’API
- MDN : utiliser l’API Gamepad — guide pas à pas
- Gamepad Tester — testez le mappage de votre manette dans le navigateur