Mobilfreundliche Web-Spiele
Mobil ist die größte Gaming-Plattform. Mit ein paar Anpassungen läuft dein Web-Spiel auch auf Handys und Tablets richtig gut.
1) Viewport einrichten
Zoom verhindern und saubere Skalierung sicherstellen:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">2) Bildschirmfüllendes Canvas
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
touch-action: none; /* Prevent browser gestures */
}
#game {
display: block;
width: 100%;
height: 100%;
}Wenn du UI an der Viewport-Höhe statt an height: 100% ausrichtest, nimm die dynamischen Viewport-Einheiten dvh, svh und lvh statt vh. Sie berücksichtigen die ein- und ausfahrende Werkzeugleiste mobiler Browser, die 100vh ignoriert (unter iOS ist 100vh die Höhe bei ausgeblendeter Leiste, der untere Teil deines Layouts wird also abgeschnitten). Diese Einheiten haben Baseline Widely Available erreicht und werden in Chrome/Edge ab 108, Firefox ab 101 und Safari/iOS Safari ab 15.4 unterstützt, also von rund 92 % der Nutzer.
3) Touch-Events behandeln
const canvas = document.getElementById('game')
// Prevent default touch behaviors
canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false })
canvas.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false })
// Track touches
const touches = new Map()
canvas.addEventListener('touchstart', (e) => {
for (const touch of e.changedTouches) {
touches.set(touch.identifier, {
x: touch.clientX,
y: touch.clientY,
startX: touch.clientX,
startY: touch.clientY,
})
}
})
canvas.addEventListener('touchmove', (e) => {
for (const touch of e.changedTouches) {
const t = touches.get(touch.identifier)
if (t) {
t.x = touch.clientX
t.y = touch.clientY
}
}
})
canvas.addEventListener('touchend', (e) => {
for (const touch of e.changedTouches) {
touches.delete(touch.identifier)
}
})Soll dein Spiel auch mit Maus oder Stift funktionieren, zieh Pointer Events (pointerdown/pointermove/pointerup) statt Touch-Events in Betracht. MDN empfiehlt sie inzwischen als einheitliches, geräteunabhängiges Eingabemodell: Ein Codepfad deckt Touch, Maus und Stift ab, und Multi-Touch funktioniert weiterhin über pointerId (das Gegenstück zum oben genutzten touch.identifier). Für reine Touch-Spiele sind Touch-Events aber völlig in Ordnung.
4) Virtueller Joystick
class VirtualJoystick {
constructor(x, y, radius) {
this.baseX = x
this.baseY = y
this.radius = radius
this.knobRadius = radius * 0.4
this.active = false
this.touchId = null
this.dx = 0
this.dy = 0
}
handleTouchStart(touch) {
const dist = Math.hypot(touch.clientX - this.baseX, touch.clientY - this.baseY)
if (dist < this.radius) {
this.active = true
this.touchId = touch.identifier
this.updatePosition(touch.clientX, touch.clientY)
}
}
handleTouchMove(touch) {
if (touch.identifier === this.touchId) {
this.updatePosition(touch.clientX, touch.clientY)
}
}
handleTouchEnd(touch) {
if (touch.identifier === this.touchId) {
this.active = false
this.touchId = null
this.dx = 0
this.dy = 0
}
}
updatePosition(x, y) {
let dx = x - this.baseX
let dy = y - this.baseY
const dist = Math.hypot(dx, dy)
if (dist > this.radius) {
dx = (dx / dist) * this.radius
dy = (dy / dist) * this.radius
}
this.dx = dx / this.radius
this.dy = dy / this.radius
}
draw(ctx) {
// Base circle
ctx.beginPath()
ctx.arc(this.baseX, this.baseY, this.radius, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'
ctx.fill()
// Knob
const knobX = this.baseX + this.dx * this.radius
const knobY = this.baseY + this.dy * this.radius
ctx.beginPath()
ctx.arc(knobX, knobY, this.knobRadius, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
ctx.fill()
}
getInput() {
return { x: this.dx, y: this.dy }
}
}5) Touch-Buttons
class TouchButton {
constructor(x, y, radius, label) {
this.x = x
this.y = y
this.radius = radius
this.label = label
this.pressed = false
this.touchId = null
}
contains(px, py) {
return Math.hypot(px - this.x, py - this.y) < this.radius
}
handleTouchStart(touch) {
if (this.contains(touch.clientX, touch.clientY)) {
this.pressed = true
this.touchId = touch.identifier
}
}
handleTouchEnd(touch) {
if (touch.identifier === this.touchId) {
this.pressed = false
this.touchId = null
}
}
draw(ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
ctx.fillStyle = this.pressed ? 'rgba(255, 255, 255, 0.5)' : 'rgba(255, 255, 255, 0.2)'
ctx.fill()
ctx.font = 'bold 24px sans-serif'
ctx.fillStyle = '#fff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(this.label, this.x, this.y)
}
}6) Ausrichtung behandeln
function checkOrientation() {
const isLandscape = window.innerWidth > window.innerHeight
if (!isLandscape) {
showRotatePrompt()
} else {
hideRotatePrompt()
resizeCanvas()
}
}
window.addEventListener('resize', checkOrientation)
window.addEventListener('orientationchange', checkOrientation)
function showRotatePrompt() {
document.getElementById('rotate-prompt').style.display = 'flex'
}
function hideRotatePrompt() {
document.getElementById('rotate-prompt').style.display = 'none'
}7) Performance optimieren
// Reduce resolution on low-end devices
function getDeviceScale() {
const dpr = window.devicePixelRatio || 1
const isLowEnd = navigator.hardwareConcurrency <= 2
return isLowEnd ? Math.min(dpr, 1.5) : Math.min(dpr, 2)
}
// Throttle expensive operations
let lastHeavyUpdate = 0
function maybeDoHeavyWork(timestamp) {
if (timestamp - lastHeavyUpdate > 100) { // 10 fps for heavy stuff
doExpensiveCalculations()
lastHeavyUpdate = timestamp
}
}
// Use lower quality assets
function getAssetQuality() {
const memory = navigator.deviceMemory || 4
if (memory <= 2) return 'low'
if (memory <= 4) return 'medium'
return 'high'
}8) Rücksicht auf den Akku
// Check battery and reduce effects
async function checkBattery() {
if ('getBattery' in navigator) {
const battery = await navigator.getBattery()
if (battery.level < 0.2 && !battery.charging) {
enableBatterySaver()
}
battery.addEventListener('levelchange', () => {
if (battery.level < 0.2 && !battery.charging) {
enableBatterySaver()
}
})
}
}
function enableBatterySaver() {
game.particleCount = Math.floor(game.particleCount * 0.5)
game.targetFps = 30
console.log('Battery saver enabled')
}9) Fullscreen-API
async function requestFullscreen() {
const elem = document.documentElement
try {
if (elem.requestFullscreen) {
await elem.requestFullscreen()
} else if (elem.webkitRequestFullscreen) {
await elem.webkitRequestFullscreen()
}
// Lock orientation
if (screen.orientation?.lock) {
await screen.orientation.lock('landscape')
}
} catch (err) {
console.warn('Fullscreen failed:', err)
}
}
// Call on user interaction
document.getElementById('play-btn').addEventListener('click', () => {
requestFullscreen()
startGame()
})Ein Vorbehalt: Die Fullscreen-API funktioniert auf dem iPhone nicht für allgemeine Elemente. iOS Safari bietet Vollbild nur für native Video-Elemente (über das nicht standardisierte webkitEnterFullscreen), documentElement.requestFullscreen() schlägt auf dem iPhone also fehl. Das try/catch oben fängt das sauber ab, entwirf dein Spiel also so, dass es auf dem iPhone auch ohne echtes Vollbild spielbar ist, und behandle Vollbild als Zugabe auf iPad, Android und Desktop, wo es unterstützt wird. Der Aufruf screen.orientation.lock() hat auf dem iPhone dieselbe Einschränkung, verlass dich also nicht darauf.
10) Testcheckliste
Vor dem Ausliefern:
- Touch-Reaktion — auf echten Geräten testen, nicht nur in Emulatoren
- Verschiedene Bildschirmgrößen — Handy, Tablet, iPad Pro
- Beide Ausrichtungen — oder auf eine festlegen
- Schwache Geräte — auf 2 bis 3 Jahre alten Handys testen
- Langsame Netze — mit 3G-Simulation testen
- Safari unter iOS — hat Eigenheiten bei Audio und Vollbild
- Chrome unter Android — der häufigste Browser
- Unterbrechungen — Anrufe, Benachrichtigungen
- Speicherdruck — erholt es sich, nachdem der Tab im Hintergrund war?
// Debug touch visualization
function drawTouchDebug(ctx) {
for (const [id, touch] of touches) {
ctx.beginPath()
ctx.arc(touch.x, touch.y, 30, 0, Math.PI * 2)
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.fill()
ctx.fillStyle = '#fff'
ctx.fillText(id.toString(), touch.x, touch.y)
}
}Verwandt
- Eingaben in Spielen
- Responsives Spiel-Canvas
- PWA für Offline-Spiele
- Web-Audio-API für Spiele — mit iOS-Audiobeschränkungen umgehen
- Ein Web-Spiel ausliefern, das schnell lädt — Ladezeiten für mobile Verbindungen optimieren
- Spiele per Iframe einbetten — was beim Einbetten auf Mobilgeräten zu beachten ist
Externe Ressourcen
- MDN: Touch events — Multi-Touch behandeln
- MDN: Screen Orientation API — Ausrichtung festlegen
- MDN: Fullscreen API — Vollbild auf Mobilgeräten
- web.dev: Responsive design — responsive Muster, die auch für Spiel-UI gelten