Gestion des entrées de jeu
Une bonne gestion des entrées peut faire toute la différence dans un jeu. Des commandes qui réagissent avec retard, des entrées ignorées ou des associations de boutons déroutantes frustrent les joueurs avant même qu'ils ne découvrent le contenu de votre jeu. Ce tutoriel explique comment gérer le clavier, la souris, le tactile et les manettes de manière unifiée.
Essayez d'abord :
Le modèle d'état des entrées
L'approche naïve consiste à réagir directement aux événements : lorsque l'utilisateur appuie sur une touche, effectuer une action. Cela pose problème. Les événements se déclenchent à des moments imprévisibles, tandis que votre boucle de jeu s'exécute à une fréquence fixe. Si vous déplacez le joueur dans le gestionnaire keydown, la vitesse de déplacement dépend de la fréquence de répétition des touches, qui varie selon le système d'exploitation.
À la place, conservez un objet représentant l'état des entrées, que votre boucle de jeu peut consulter :
const input = {
left: false,
right: false,
up: false,
down: false,
action: false,
pointer: { x: 0, y: 0, down: false },
}Les gestionnaires d'événements définissent ces indicateurs. La boucle de jeu les consulte. Ce découplage simplifie tout : votre logique de mise à jour n'a pas besoin de savoir si l'entrée provient du clavier, du tactile ou d'une manette.
Clavier
Utilisez e.code plutôt que e.key, car il correspond à la position physique de la touche. Ainsi, les commandes WASD fonctionnent correctement sur les dispositions de clavier autres que QWERTY.
const keyMap = {
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
ArrowDown: 'down',
KeyA: 'left',
KeyD: 'right',
KeyW: 'up',
KeyS: 'down',
Space: 'action',
}
window.addEventListener('keydown', (e) => {
const action = keyMap[e.code]
if (action) {
input[action] = true
e.preventDefault()
}
})
window.addEventListener('keyup', (e) => {
const action = keyMap[e.code]
if (action) input[action] = false
})La méthode e.preventDefault() empêche les touches fléchées de faire défiler la page. Appelez-la uniquement pour les touches que vous gérez.
Souris
Les coordonnées de la souris sont exprimées dans l'espace de l'écran. Vous devez les convertir dans l'espace du canevas :
const canvas = document.getElementById('game')
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect()
input.pointer.x = e.clientX - rect.left
input.pointer.y = e.clientY - rect.top
})
canvas.addEventListener('mousedown', () => {
input.pointer.down = true
input.action = true
})
canvas.addEventListener('mouseup', () => {
input.pointer.down = false
input.action = false
})Si votre canevas est redimensionné (consultez le tutoriel sur le canevas adaptatif), vous devrez diviser les valeurs par le facteur d'échelle afin d'obtenir les coordonnées du jeu.
Tactile
Le tactile fonctionne de manière similaire à la souris, mais vous devez utiliser e.preventDefault() pour empêcher le navigateur de faire défiler ou de zoomer la page :
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
const touch = e.touches[0]
const rect = canvas.getBoundingClientRect()
input.pointer.x = touch.clientX - rect.left
input.pointer.y = touch.clientY - rect.top
input.pointer.down = true
input.action = true
}, { passive: false })
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
const touch = e.touches[0]
const rect = canvas.getBoundingClientRect()
input.pointer.x = touch.clientX - rect.left
input.pointer.y = touch.clientY - rect.top
}, { passive: false })
canvas.addEventListener('touchend', () => {
input.pointer.down = false
input.action = false
})L'option { passive: false } est nécessaire, car les navigateurs modernes utilisent par défaut des écouteurs tactiles passifs pour améliorer les performances. Sans elle, preventDefault ne fonctionnera pas.
Croix directionnelle virtuelle pour mobile
Pour les jeux qui nécessitent des commandes directionnelles sur mobile, ajoutez des boutons à l'écran :
function createVirtualDpad(container) {
const dpad = document.createElement('div')
dpad.className = 'virtual-dpad'
dpad.innerHTML = `
<button data-dir="up">↑</button>
<button data-dir="left">←</button>
<button data-dir="right">→</button>
<button data-dir="down">↓</button>
`
dpad.querySelectorAll('button').forEach(btn => {
const dir = btn.dataset.dir
btn.addEventListener('touchstart', (e) => {
e.preventDefault()
input[dir] = true
})
btn.addEventListener('touchend', () => input[dir] = false)
})
container.appendChild(dpad)
}Donnez à ces boutons une taille suffisante pour être facilement utilisés avec les pouces (au moins 44x44 pixels) et placez-les dans les coins inférieurs.
Manette
Contrairement au clavier et à la souris, l'état d'une manette n'est pas piloté par des événements. Vous devez l'interroger à chaque image :
function pollGamepads() {
const gamepads = navigator.getGamepads()
const gp = gamepads[0]
if (gp) {
// D-pad or left stick
input.left = gp.buttons[14]?.pressed || gp.axes[0] < -0.5
input.right = gp.buttons[15]?.pressed || gp.axes[0] > 0.5
input.up = gp.buttons[12]?.pressed || gp.axes[1] < -0.5
input.down = gp.buttons[13]?.pressed || gp.axes[1] > 0.5
// A button (standard mapping)
input.action = gp.buttons[0]?.pressed
}
}Appelez pollGamepads() au début de votre fonction de mise à jour. Le seuil de 0,5 appliqué aux sticks analogiques crée une zone morte afin que leurs petits mouvements ne soient pas enregistrés comme des entrées.
Vous pouvez détecter la connexion et la déconnexion des manettes :
window.addEventListener('gamepadconnected', (e) => {
console.log('Gamepad connected:', e.gamepad.id)
})
window.addEventListener('gamepaddisconnected', (e) => {
console.log('Gamepad disconnected:', e.gamepad.id)
})Utiliser les entrées dans votre boucle de jeu
Votre fonction de mise à jour n'a désormais plus qu'à consulter l'état des entrées :
function update(dt) {
pollGamepads()
if (input.left) player.x -= player.speed * dt
if (input.right) player.x += player.speed * dt
if (input.up) player.y -= player.speed * dt
if (input.down) player.y += player.speed * dt
if (input.action && player.canShoot) {
player.shoot()
}
}Cela fonctionne que l'entrée provienne du clavier, du tactile ou d'une manette. La logique du jeu n'a pas besoin de le savoir.
Appui initial ou maintien
Vous souhaitez parfois détecter si un bouton vient d'être pressé pendant l'image actuelle, plutôt que de savoir s'il est maintenu enfoncé. Le saut en est un bon exemple : vous voulez un saut par appui, et non des sauts continus tant que le bouton reste enfoncé.
const inputPrev = { ...input }
function wasJustPressed(action) {
return input[action] && !inputPrev[action]
}
function endFrame() {
Object.assign(inputPrev, input)
}Appelez endFrame() à la fin de votre boucle de mise à jour. Utilisez ensuite wasJustPressed('action') à la place de input.action pour les actions qui ne doivent être déclenchées qu'une seule fois.
Perte de focus
Lorsque la fenêtre du navigateur perd le focus, les événements du clavier cessent de se déclencher. Si le joueur maintenait une touche enfoncée, celle-ci reste alors considérée comme active :
window.addEventListener('blur', () => {
input.left = input.right = input.up = input.down = input.action = false
})Sans cela, le joueur continue de se déplacer lorsqu'il utilise Alt+Tab pour changer de fenêtre.
Bonnes pratiques
Prenez en charge à la fois les touches WASD et les touches fléchées, car chaque joueur a ses préférences. Permettez de réassigner les commandes et enregistrez la configuration dans localStorage. Proposez des commandes tactiles sur les appareils mobiles. Testez avec de vraies manettes, car les manettes Xbox, PlayStation et génériques se comportent toutes de façon légèrement différente. Enfin, gérez toujours correctement la perte de focus.
Ressources complémentaires
Présentation approfondie de l'API Gamepad présente des fonctionnalités avancées des manettes, comme les vibrations et la prise en charge de plusieurs joueurs.
Jeux web adaptés aux mobiles présente les entrées tactiles plus en détail.
Pointer Lock pour les jeux FPS explique comment capturer la souris pour les commandes à la première personne.
Boucle de jeu Canvas 2D montre comment intégrer le modèle d'état des entrées dans une boucle de jeu complète.
Principes de base du multijoueur avec WebSocket explique comment synchroniser les entrées entre des joueurs en réseau.
Ressources externes
- MDN : KeyboardEvent.code — valeurs des touches physiques pour l'association des entrées
- MDN : événements de pointeur — événements unifiés pour la souris et le tactile
- MDN : API Gamepad — prise en charge des manettes dans le navigateur
- MDN : événements tactiles — gestion du multipoint sur mobile