Mobil uyumlu web oyunları
Mobil cihazlar en büyük oyun platformudur. Birkaç düzenlemeyle web oyununuzun telefonlarda ve tabletlerde harika çalışmasını sağlayabilirsiniz.
1) Görüntü alanı ayarları
Yakınlaştırmayı önleyin ve doğru ölçeklendirmeyi sağlayın:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">2) Tam ekran 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%;
}Arayüzü height: 100% yerine görüntü alanı yüksekliğine göre boyutlandırıyorsanız vh yerine dinamik görüntü alanı birimleri olan dvh, svh ve lvh birimlerini tercih edin. Bu birimler, mobil tarayıcının daralıp genişleyen araç çubuğunu hesaba katar; 100vh ise bunu göz ardı eder (iOS'te 100vh, araç çubuğunun gizli olduğu yüksekliği temsil ettiğinden düzeninizin alt kısmı kırpılır). Bu birimler Baseline Widely Available durumuna ulaşmıştır ve Chrome/Edge 108+, Firefox 101+ ile Safari/iOS Safari 15.4+ tarafından desteklenmektedir; bu da kullanıcıların yaklaşık %92'sine karşılık gelir.
3) Dokunma olaylarını işleme
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)
}
})Oyununuzun fare veya ekran kalemiyle de çalışması gerekiyorsa dokunma olayları yerine Pointer Events (pointerdown/pointermove/pointerup) kullanmayı değerlendirin. MDN artık bunları cihazdan bağımsız tek giriş modeli olarak öneriyor: tek bir kod yolu dokunma, fare ve kalem girişlerini işlerken çoklu dokunma da pointerId üzerinden çalışmaya devam eder (yukarıda kullanılan touch.identifier karşılığıdır). Yalnızca dokunmatik giriş kullanan oyunlar için dokunma olayları hâlâ tamamen uygundur.
4) Sanal 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) Dokunmatik düğmeler
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) Ekran yönünü işleme
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) Performans optimizasyonu
// 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) Pil kullanımına ilişkin hususlar
// 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()
})Önemli bir sınırlama vardır: Fullscreen API, iPhone'daki genel öğeler için çalışmaz. iOS Safari tam ekran özelliğini yalnızca yerel video öğeleri için standart dışı webkitEnterFullscreen aracılığıyla sunar; bu nedenle documentElement.requestFullscreen() iPhone'da reddedilir. Yukarıdaki try/catch bunu sorunsuz biçimde işler. Bu nedenle oyununuzu iPhone'da gerçek tam ekran olmadan oynanabilecek şekilde tasarlayın ve tam ekranı, desteklendiği iPad, Android ve masaüstü cihazlarda ek bir iyileştirme olarak değerlendirin. screen.orientation.lock() çağrısı da iPhone'da aynı sınırlamaya sahiptir, dolayısıyla buna bağımlı olmayın.
10) Test kontrol listesi
Yayınlamadan önce:
- Dokunma tepkiselliği — Yalnızca emülatörlerde değil, gerçek cihazlarda test edin
- Çeşitli ekran boyutları — Telefon, tablet, iPad Pro
- Her iki ekran yönü — Ya da tek bir yöne kilitleyin
- Düşük donanımlı cihazlar — 2-3 yıllık telefonlarda test edin
- Yavaş ağlar — 3G simülasyonunda test edin
- iOS Safari — Ses ve tam ekranla ilgili kendine özgü davranışları vardır
- Android Chrome — En yaygın tarayıcıdır
- Kesintiler — Telefon aramaları, bildirimler
- Bellek baskısı — Arka plan sekmesinden döndükten sonra toparlanıyor mu?
// 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)
}
}İlgili içerikler
- Oyun girişlerini işleme
- Duyarlı oyun canvas'ı
- Çevrimdışı oyunlar için PWA
- Oyunlar için Web Audio API — iOS ses kısıtlamalarını işleme
- Hızlı yüklenen bir web oyunu yayınlama — mobil bağlantılar için yükleme sürelerini optimize etme
- Oyunları iframe ile yerleştirme — mobil cihazlara özgü yerleştirme hususları
Harici kaynaklar
- MDN: Dokunma olayları — çoklu dokunmayı işleme
- MDN: Screen Orientation API — ekran yönünü kilitleme
- MDN: Fullscreen API — mobil cihazlarda tam ekrana geçme
- web.dev: Duyarlı tasarım — oyun arayüzlerine uygulanabilen duyarlı tasarım kalıpları