Oyun Girdilerini İşleme
İyi bir girdi sistemi, bir oyunu başarıya da başarısızlığa da götürebilir. Gecikmeli kontroller, algılanmayan girdiler veya kafa karıştırıcı düğme eşlemeleri, oyuncular daha oyununuzun içeriğini görmeden onları bezdirir. Bu eğitimde klavye, fare, dokunma ve oyun kumandası girdilerini birleşik bir şekilde nasıl işleyeceğinizi öğreneceksiniz.
Önce deneyin:
Girdi Durumu Kalıbı
Basit yaklaşım, olaylara doğrudan tepki vermektir: kullanıcı bir tuşa bastığında bir şey yap. Bu yaklaşım sorunlara yol açar. Olaylar öngörülemeyen zamanlarda tetiklenirken oyun döngünüz sabit bir hızda çalışır. Oyuncuyu keydown işleyicisinde hareket ettirirseniz hareket hızı, işletim sistemine göre değişen tuş tekrarlama hızına bağlı olur.
Bunun yerine, oyun döngünüzün okuyacağı bir girdi durumu nesnesi tutun:
const input = {
left: false,
right: false,
up: false,
down: false,
action: false,
pointer: { x: 0, y: 0, down: false },
}Olay işleyicileri bu bayrakları ayarlar. Oyun döngüsü de bunları okur. Bu ayrıştırma her şeyi kolaylaştırır: güncelleme mantığınız, girdinin klavyeden, dokunmadan veya oyun kumandasından gelip gelmediğini önemsemez.
Klavye
Fiziksel tuş konumunu belirttiği için e.key yerine e.code kullanın. Böylece WASD, QWERTY dışındaki klavye düzenlerinde de doğru çalışır.
const keyMap = {
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
ArrowDown: 'down',
KeyA: 'left',
KeyD: 'right',
KeyW: 'up',
KeyS: 'down',
Space: 'action',
}
window.addEventListener('keydown', (e) => {
const action = keyMap[e.code]
if (action) {
input[action] = true
e.preventDefault()
}
})
window.addEventListener('keyup', (e) => {
const action = keyMap[e.code]
if (action) input[action] = false
})e.preventDefault(), ok tuşlarının sayfayı kaydırmasını engeller. Bunu yalnızca işlediğiniz tuşlar için çağırın.
Fare
Fare koordinatları ekran uzayında gelir. Bunları tuval uzayına dönüştürmeniz gerekir:
const canvas = document.getElementById('game')
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect()
input.pointer.x = e.clientX - rect.left
input.pointer.y = e.clientY - rect.top
})
canvas.addEventListener('mousedown', () => {
input.pointer.down = true
input.action = true
})
canvas.addEventListener('mouseup', () => {
input.pointer.down = false
input.action = false
})Tuvaliniz ölçeklendirilmişse (duyarlı tuval eğitimine bakın), oyun koordinatlarını elde etmek için ölçek faktörüne bölmeniz gerekir.
Dokunma
Dokunma, fareye benzer şekilde çalışır ancak tarayıcının kaydırma veya yakınlaştırma yapmasını engellemek için e.preventDefault() kullanmanız gerekir:
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
const touch = e.touches[0]
const rect = canvas.getBoundingClientRect()
input.pointer.x = touch.clientX - rect.left
input.pointer.y = touch.clientY - rect.top
input.pointer.down = true
input.action = true
}, { passive: false })
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
const touch = e.touches[0]
const rect = canvas.getBoundingClientRect()
input.pointer.x = touch.clientX - rect.left
input.pointer.y = touch.clientY - rect.top
}, { passive: false })
canvas.addEventListener('touchend', () => {
input.pointer.down = false
input.action = false
})Modern tarayıcılar performans amacıyla varsayılan olarak pasif dokunma dinleyicileri kullandığından { passive: false } gereklidir. Bu ayar olmadan preventDefault çalışmaz.
Mobil Cihazlar İçin Sanal Yön Tuşları
Mobil cihazlarda yön kontrollerine ihtiyaç duyan oyunlar için ekrana düğmeler ekleyin:
function createVirtualDpad(container) {
const dpad = document.createElement('div')
dpad.className = 'virtual-dpad'
dpad.innerHTML = `
<button data-dir="up">↑</button>
<button data-dir="left">←</button>
<button data-dir="right">→</button>
<button data-dir="down">↓</button>
`
dpad.querySelectorAll('button').forEach(btn => {
const dir = btn.dataset.dir
btn.addEventListener('touchstart', (e) => {
e.preventDefault()
input[dir] = true
})
btn.addEventListener('touchend', () => input[dir] = false)
})
container.appendChild(dpad)
}Bu düğmeleri başparmakla kullanılabilecek kadar büyük (en az 44x44 piksel) olacak şekilde biçimlendirin ve alt köşelere yerleştirin.
Oyun Kumandası
Klavye ve farenin aksine oyun kumandası durumu olay tabanlı değildir. Her karede sorgulamanız gerekir:
function pollGamepads() {
const gamepads = navigator.getGamepads()
const gp = gamepads[0]
if (gp) {
// D-pad or left stick
input.left = gp.buttons[14]?.pressed || gp.axes[0] < -0.5
input.right = gp.buttons[15]?.pressed || gp.axes[0] > 0.5
input.up = gp.buttons[12]?.pressed || gp.axes[1] < -0.5
input.down = gp.buttons[13]?.pressed || gp.axes[1] > 0.5
// A button (standard mapping)
input.action = gp.buttons[0]?.pressed
}
}Güncelleme fonksiyonunuzun başında pollGamepads() çağrısı yapın. Analog çubuklardaki 0,5 eşiği, küçük çubuk hareketlerinin girdi olarak algılanmaması için bir ölü bölge oluşturur.
Oyun kumandalarının ne zaman bağlandığını ve bağlantısının kesildiğini algılayabilirsiniz:
window.addEventListener('gamepadconnected', (e) => {
console.log('Gamepad connected:', e.gamepad.id)
})
window.addEventListener('gamepaddisconnected', (e) => {
console.log('Gamepad disconnected:', e.gamepad.id)
})Oyun Döngünüzde Girdi Kullanımı
Artık güncelleme fonksiyonunuz yalnızca girdi durumunu okur:
function update(dt) {
pollGamepads()
if (input.left) player.x -= player.speed * dt
if (input.right) player.x += player.speed * dt
if (input.up) player.y -= player.speed * dt
if (input.down) player.y += player.speed * dt
if (input.action && player.canShoot) {
player.shoot()
}
}Bu, girdinin klavyeden, dokunmadan veya oyun kumandasından gelmesinden bağımsız olarak çalışır. Oyun mantığının bunu bilmesi gerekmez.
Yeni Basıldı ve Basılı Tutuluyor Ayrımı
Bazen bir düğmenin o anda basılı tutulup tutulmadığını değil, yalnızca bu karede yeni basılıp basılmadığını algılamak istersiniz. Zıplama buna iyi bir örnektir: düğme basılı tutulduğu sürece sürekli zıplamak yerine, her basışta bir kez zıplanmasını istersiniz.
const inputPrev = { ...input }
function wasJustPressed(action) {
return input[action] && !inputPrev[action]
}
function endFrame() {
Object.assign(inputPrev, input)
}Güncelleme döngünüzün sonunda endFrame() çağrısı yapın. Ardından tek seferlik eylemler için input.action yerine wasJustPressed('action') kullanın.
Odak Kaybı
Tarayıcı penceresi odağı kaybettiğinde klavye olayları tetiklenmeyi bırakır. Oyuncu bir tuşu basılı tutuyorsa tuş basılı kalmış gibi davranır:
window.addEventListener('blur', () => {
input.left = input.right = input.up = input.down = input.action = false
})Bu olmadan oyuncu, kullanıcı Alt+Tab ile başka bir pencereye geçtiğinde hareket etmeye devam eder.
En İyi Uygulamalar
Oyuncuların farklı tercihleri olduğundan hem WASD hem de ok tuşlarını destekleyin. Tuşların yeniden atanmasına izin verin ve yapılandırmayı localStorage içinde saklayın. Mobil cihazlarda dokunmatik kontroller sağlayın. Xbox, PlayStation ve genel amaçlı oyun kumandalarının her biri biraz farklı davrandığından gerçek kumandalarla test yapın. Ayrıca odak kaybını her zaman sorunsuz biçimde yönetin.
Diğer Kaynaklar
Gamepad API'ye ayrıntılı bakış, titreşim ve birden fazla oyuncu gibi gelişmiş oyun kumandası özelliklerini ele alır.
Mobil uyumlu web oyunları, dokunmatik girdiyi daha ayrıntılı biçimde ele alır.
FPS oyunları için Pointer Lock, birinci şahıs kontrolleri için fareyi nasıl yakalayacağınızı gösterir.
Canvas 2D oyun döngüsü, eksiksiz bir oyun döngüsüne entegre edilmiş girdi durumu kalıbını gösterir.
WebSocket ile çok oyunculu oyunların temelleri, ağ üzerinden bağlı oyuncular arasında girdilerin eşitlenmesini ele alır.
Harici Kaynaklar
- MDN: KeyboardEvent.code — girdi eşleme için fiziksel tuş değerleri
- MDN: İşaretçi olayları — birleşik fare ve dokunma olayları
- MDN: Gamepad API — tarayıcıda oyun kumandası desteği
- MDN: Dokunma olayları — mobil cihazlarda çoklu dokunma yönetimi