Skip to content

Eingaben in Spielen

Gute Eingabeverarbeitung entscheidet über ein Spiel. Träge Steuerung, verschluckte Eingaben oder verwirrende Tastenbelegungen frustrieren Spieler, bevor sie überhaupt den Inhalt deines Spiels sehen. Dieses Tutorial zeigt, wie du Tastatur, Maus, Touch und Gamepad einheitlich behandelst.

Probier es zuerst aus:

Das Muster des Eingabezustands

Der naive Ansatz ist, direkt auf Events zu reagieren: Wenn der Nutzer eine Taste drückt, mach etwas. Das macht Ärger. Events feuern zu unvorhersehbaren Zeitpunkten, während deine Spielschleife mit fester Rate läuft. Bewegst du den Spieler im keydown-Handler, hängt die Bewegungsgeschwindigkeit von der Tastenwiederholrate ab, und die unterscheidet sich je nach Betriebssystem.

Pflege stattdessen ein Objekt mit dem Eingabezustand, das deine Spielschleife liest:

js
const input = {
  left: false,
  right: false,
  up: false,
  down: false,
  action: false,
  pointer: { x: 0, y: 0, down: false },
}

Event-Handler setzen diese Flags. Die Spielschleife liest sie. Diese Entkopplung macht alles einfacher: Deiner Update-Logik ist es egal, ob die Eingabe von Tastatur, Touch oder Gamepad kam.

Tastatur

Nutz e.code statt e.key, weil es sich auf die physische Tastenposition bezieht. WASD funktioniert damit auch auf Nicht-QWERTY-Layouts korrekt.

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
})

Das e.preventDefault() verhindert, dass die Pfeiltasten die Seite scrollen. Ruf es nur für Tasten auf, die du selbst behandelst.

Maus

Mauskoordinaten kommen im Bildschirmraum an. Du musst sie in den Canvas-Raum umrechnen:

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
})

Ist dein Canvas skaliert (siehe das Tutorial zum responsiven Canvas), musst du durch den Skalierungsfaktor teilen, um Spielkoordinaten zu bekommen.

Touch

Touch funktioniert ähnlich wie die Maus, du brauchst aber e.preventDefault(), damit der Browser nicht scrollt oder zoomt:

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
})

Das { passive: false } ist nötig, weil moderne Browser Touch-Listener aus Performancegründen standardmäßig als passiv behandeln. Ohne das wirkt preventDefault nicht.

Virtuelles Steuerkreuz für Mobilgeräte

Für Spiele, die auf Mobilgeräten Richtungssteuerung brauchen, ergänzt du Bildschirmtasten:

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)
}

Gestalte diese Tasten groß genug für Daumen (mindestens 44x44 Pixel) und platziere sie in den unteren Ecken.

Gamepad

Anders als bei Tastatur und Maus kommt der Gamepad-Zustand nicht als Event. Du musst ihn in jedem Frame abfragen:

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
  }
}

Ruf pollGamepads() am Anfang deiner Update-Funktion auf. Die Schwelle von 0,5 auf den Analogsticks erzeugt eine Totzone, damit kleine Stickbewegungen nicht als Eingabe zählen.

Du kannst erkennen, wenn Gamepads verbunden und getrennt werden:

js
window.addEventListener('gamepadconnected', (e) => {
  console.log('Gamepad connected:', e.gamepad.id)
})

window.addEventListener('gamepaddisconnected', (e) => {
  console.log('Gamepad disconnected:', e.gamepad.id)
})

Eingaben in deiner Spielschleife nutzen

Jetzt liest deine Update-Funktion einfach den Eingabezustand:

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()
  }
}

Das funktioniert unabhängig davon, ob die Eingabe von Tastatur, Touch oder Gamepad kam. Die Spiellogik muss es nicht wissen.

Gerade gedrückt gegen gehalten

Manchmal willst du erkennen, ob eine Taste in diesem Frame gerade gedrückt wurde, und nicht, ob sie gerade gehalten wird. Springen ist ein gutes Beispiel: Du willst einen Sprung pro Druck, kein Dauerspringen, solange die Taste gehalten wird.

js
const inputPrev = { ...input }

function wasJustPressed(action) {
  return input[action] && !inputPrev[action]
}

function endFrame() {
  Object.assign(inputPrev, input)
}

Ruf endFrame() am Ende deiner Update-Schleife auf. Nutz dann wasJustPressed('action') statt input.action für einmalige Aktionen.

Fokusverlust

Verliert das Browserfenster den Fokus, hören Tastatur-Events auf zu feuern. Hielt der Spieler gerade eine Taste, bleibt sie hängen:

js
window.addEventListener('blur', () => {
  input.left = input.right = input.up = input.down = input.action = false
})

Ohne das läuft die Figur weiter, wenn der Spieler per Alt-Tab wegwechselt.

Best Practices

Unterstütze WASD und Pfeiltasten, denn Spieler haben Vorlieben. Erlaube Neubelegung und leg die Konfiguration in localStorage ab. Biete auf Mobilgeräten Touch-Steuerung an. Teste mit echten Gamepads, weil Xbox-, PlayStation- und generische Controller sich alle leicht unterschiedlich verhalten. Und behandle Fokusverlust immer sauber.

Mehr Ressourcen

Gamepad-API im Detail behandelt fortgeschrittene Gamepad-Funktionen wie Vibration und mehrere Spieler.

Mobilfreundliche Web-Spiele geht ausführlicher auf Touch-Eingaben ein.

Pointer Lock für Shooter zeigt, wie du die Maus für Ego-Steuerung einfängst.

Canvas-2D-Spielschleife zeigt das Muster des Eingabezustands eingebaut in eine vollständige Spielschleife.

WebSocket-Multiplayer-Grundlagen behandelt das Synchronisieren von Eingaben über vernetzte Spieler hinweg.

Externe Ressourcen