Skip to content

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 :

js
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.

js
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 :

js
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 :

js
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 :

js
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 :

js
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 :

js
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 :

js
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é.

js
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 :

js
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