Skip to content

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:

html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

2) Bildschirmfüllendes Canvas

css
* {
  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

js
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

js
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

js
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

js
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

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

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

js
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?
js
// 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

Externe Ressourcen