Skip to content

Game web yang ramah perangkat seluler

Perangkat seluler adalah platform game terbesar. Beberapa penyesuaian dapat membuat game web Anda berfungsi dengan sangat baik di ponsel dan tablet.

1) Penyiapan viewport

Cegah zoom dan pastikan penskalaan yang tepat:

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

2) Canvas layar penuh

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

Jika Anda menentukan ukuran UI berdasarkan tinggi viewport, bukan height: 100%, gunakan unit viewport dinamis dvh, svh, dan lvh daripada vh. Unit ini memperhitungkan bilah alat browser seluler yang dapat menyusut dan mengembang, sedangkan 100vh mengabaikannya (di iOS, 100vh adalah tinggi saat bilah alat tersembunyi, sehingga bagian bawah tata letak Anda terpotong). Unit-unit ini telah mencapai status Baseline Widely Available dan didukung di Chrome/Edge 108+, Firefox 101+, serta Safari/iOS Safari 15.4+, atau sekitar 92% pengguna.

3) Penanganan peristiwa sentuh

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

Jika game Anda juga perlu berfungsi dengan mouse atau stilus, pertimbangkan Pointer Events (pointerdown/pointermove/pointerup) sebagai pengganti peristiwa sentuh. MDN kini merekomendasikannya sebagai satu model input yang tidak bergantung pada perangkat: satu alur kode menangani sentuhan, mouse, dan pena, sementara multisentuh tetap berfungsi melalui pointerId (padanan touch.identifier yang digunakan di atas). Peristiwa sentuh tetap sangat cocok untuk game yang hanya menggunakan sentuhan.

4) Joystick virtual

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) Tombol sentuh

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) Penanganan orientasi

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) Optimalisasi performa

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) Pertimbangan baterai

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

Satu hal yang perlu diperhatikan: Fullscreen API tidak berfungsi untuk elemen umum di iPhone. Safari iOS hanya menyediakan mode layar penuh untuk elemen video native (melalui webkitEnterFullscreen yang nonstandar), sehingga documentElement.requestFullscreen() akan ditolak di iPhone. Blok try/catch di atas menanganinya dengan baik. Karena itu, rancang game Anda agar tetap dapat dimainkan tanpa layar penuh sebenarnya di iPhone, dan perlakukan layar penuh sebagai peningkatan di iPad, Android, serta desktop yang mendukungnya. Pemanggilan screen.orientation.lock() memiliki keterbatasan yang sama di iPhone, jadi jangan bergantung padanya.

10) Daftar periksa pengujian

Sebelum merilis:

  • Respons sentuhan — Uji di perangkat nyata, bukan hanya emulator
  • Berbagai ukuran layar — Ponsel, tablet, iPad Pro
  • Kedua orientasi — Atau kunci ke salah satunya
  • Perangkat kelas bawah — Uji di ponsel berusia 2–3 tahun
  • Jaringan lambat — Uji dengan simulasi 3G
  • Safari iOS — Memiliki keunikan terkait audio dan layar penuh
  • Chrome Android — Browser yang paling umum
  • Gangguan — Panggilan telepon, notifikasi
  • Tekanan memori — Apakah game pulih setelah tab berada di latar belakang?
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)
  }
}

Terkait

Sumber Daya Eksternal