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:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">2) Canvas layar penuh
* {
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
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
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
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
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
// 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
// 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()
})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?
// 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
- Penanganan input game
- Canvas game responsif
- PWA untuk game offline
- Web Audio API untuk game — menangani pembatasan audio iOS
- Rilis game web yang dimuat dengan cepat — mengoptimalkan waktu pemuatan untuk koneksi seluler
- Menyematkan game dengan iframe — pertimbangan penyematan khusus perangkat seluler
Sumber Daya Eksternal
- MDN: Peristiwa sentuh — penanganan multisentuh
- MDN: Screen Orientation API — mengunci orientasi
- MDN: Fullscreen API — memasuki mode layar penuh di perangkat seluler
- web.dev: Desain responsif — pola responsif yang berlaku untuk UI game