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:
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.
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:
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:
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:
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:
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:
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:
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.
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:
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
- MDN: KeyboardEvent.code — physische Tastenwerte für die Eingabezuordnung
- MDN: Pointer events — vereinheitlichte Maus- und Touch-Events
- MDN: Gamepad API — Controller-Unterstützung im Browser
- MDN: Touch events — Multi-Touch auf Mobilgeräten