Oyun geliştirme · Web oyun döngüsü
Girdi & Durum Makinesi: Klavye, Pointer, Gamepad
Oyun döngüsü her tikte girdi okur; DOM olayları ile oyun mantığı arasında ince bir tampon ve hafif durum makinesi (FSM) gerekir. Bu sayfa web yüzeyinde keyboard, pointer ve gamepad birleşimini anlatır: Hexovian input sistemleri derinliği veya ağ replikasyonu değildir.
Holodepth bu yaprağı «event listener nasıl eklenir» 101 olarak yazmaz. Döngü sırası ve delta Loop & zaman bütçesi yaprağındadır. Picking ve raycast Three.js · Pointer & raycast omurgasındadır (burada yalnız oyun komutu yüzeyi). Platform gecikme ve ms bütçesi Platform · FPS yaprağındadır. Gelişmiş input rebinding, action map ve konsol derinliği Hexovian sınırındadır.
Sayfayı bitirdiğinizde şu cümleyi kurabilmelisiniz: «Girdi tik başında toplanır; edge ve hold ayrılır; pointer oyun komutuna çevrilir; gamepad opsiyonel yüzeydir; FSM fazları net geçişle yaşar; menü ve oyun ayrı state'tir. Sonraki kapı entity eşlemesidir.»
Girdi katmanının ince tutulması, bu izlekteki en ucuz test edilebilirlik kazancıdır: komut sözlüğü sabitse, oyun mantığı klavye mi pad mi kullanıldığını hiç bilmeden birim testi ile doğrulanabilir. Holodepth bu ayrımı üretim öncesi bir sözleşme olarak görür.
Bu sayfanın sınırı · komşu konularla ayrım
Bu sayfa web/Three oyununda girdi yüzeyi ve hafif FSM anlatır:
- Keyboard, pointer, gamepad birleşik model
- Edge / hold / buffer
- FSM lite: menü, oyun, pause
- Döngü ile entegrasyon
Bilinçli olarak dışarıda bırakılan konular:
- rAF döngü iskeleti → Loop & zaman bütçesi
- Raycast picking derinliği → Three omurgası
- Action map, konsol rebinding → Hexovian
- Netcode input prediction, glTF
Kısa ayrım: Bu sayfa «komutlar tikte nasıl toplanır?» · Döngü yaprağı «ne zaman okunur?» · Three picking «hangi mesh?» · Hexovian «motor input katmanı».
Girdi yüzeyi: olay değil komut
DOM keydown ve pointerdown ham olaylardır;
oyun mantığı «sol», «atla», «menü» gibi komutlar okur. Holodepth: olay
dinleyicileri ince kalır; her tikte input.poll() veya
eşdeğeri komut setini üretir. Böylece mantık test edilebilir ve
cihazlar arası eşleme tek yerde toplanır.
Bu ayrımın somut faydası refactor anında görülür: tuş düzenini
WASD'den ok tuşlarına değiştirmek, tek bir eşleme
tablosunu güncellemekten ibaret kalır — update()
fonksiyonlarının hiçbiri event.code string'i görmez.
Olay yağmuru mantığa değil; tampona düşer.
Klavye: edge, hold ve çakışma
Hareket için hold (W basılı), menü için edge
(Escape bir kez) ayrılır. preventDefault yalnız oyun odaklı
tuşlarda; tarayıcı kısayollarını körlemeyin. Holodepth: aynı karede hem
edge hem hold güncellenir; önce edge tüketilir, sonra hold süzülür.
- Edge: bu tikte basıldı mı?
- Hold: basılı mı?
- Released: bu tikte bırakıldı mı?
keydown tekrarlanan (repeat) olayları tetikler; edge
tespiti bu tekrarları görmezden gelmelidir — aksi halde bir tuşa basılı
tutmak «her karede bir kez tetiklenmesi gereken» bir eylemi (ör.
zıplama) sürekli tetikler. Holodepth: event.repeat
kontrolüne güvenmek yerine kendi edge/hold durumunuzu tutun; tarayıcı
repeat davranışı standart değildir.
Pointer ve dokunma
Canvas üzerinde pointer konumu normalize edilir (-1..1 veya 0..1 ürün
kararı). Tıklama oyun komutuna çevrilir: «ateş», «seç», «sürükle».
Holodepth: picking için Three raycast omurgasına link verilir; bu sayfa
«komut üretimi» katmanında kalır. Touch için pointer events
birleşik model tercih edilir; ayrı touch dinleyicisi ikinci gerçeklik
üretir.
Çoklu dokunma (multi-touch) mobil arcade'lerde erken bir karar
gerektirir: yalnız birincil pointer mı takip edilecek, yoksa her
pointerId ayrı bir sanal joystick mi besleyecek? Holodepth:
pointer id'sini komut üretiminde saklayın; ikinci parmağın birinci
komutu «iptal» etmesi yaygın bir mobil hatadır.
Gamepad: opsiyonel ama planlı
navigator.getGamepads() her tikte poll edilir; dead zone ve
eksen eşlemesi klavye ile aynı komut sözlüğüne bağlanır. Bağlantı kopunca
state sıfırlanır; son eksen değeri «hayalet» hareket üretmesin. Holodepth:
gamepad yoksa oyun çalışır; varsa eşit vatandaş yüzeydir.
Dead zone sabiti (ör. 0,15) çubuk sürtünmesini ve sensör gürültüsünü
emer; sıfır dead zone ile «duran» bir pad hafifçe hareket komutu
sızdırabilir. Holodepth: dead zone'u komut üretim katmanında uygulayın,
her update() içinde tekrar tekrar değil — tek noktada
normalize edilmiş bir eksen değeri akış boyunca tutarlı kalır.
FSM lite: fazlar ve geçişler
Hafif durum makinesi: boot, menu,
playing, paused, gameover. Her faz
hangi komutları dinlediğini bilir; «menüde W ile zıpla» hatası FSM ile
kesilir. Holodepth tam hierarchical FSM veya behavior tree öğretmez; web
oyunu için düz geçiş tablosu yeter.
| Kaynak | Komut | Hedef |
|---|---|---|
| menu | Start (edge) | playing |
| playing | Pause (edge) | paused |
| paused | Resume (edge) | playing |
| playing | Lives === 0 | gameover |
Geçiş tablosunu kod dışında (ör. sabit bir obje) tutmak, yeni bir faz eklerken hangi durumların birbirine bağlı olduğunu tek bakışta görmenizi sağlar. Holodepth: her geçişte yan etki (ses, UI gösterme/gizleme) FSM dışında, geçişi dinleyen ayrı bir katmanda tetiklenir — FSM yalnızca «hangi fazdayım» sorusuna cevap verir.
Tampon ve tik hizası
Olaylar tik dışında gelir; ring buffer veya bayrak seti ile bir sonraki
poll()'da tüketilir. Holodepth: buffer boyutu sabit (ör. 8
edge); taşma eski komutu düşürür, sınırsız büyüme bellek riskidir.
Döngü yaprağındaki «input dilimi» burada dolar.
Buffer'ı tik başına bir kez temizlemek kritik bir kuraldır: aynı
poll() çağrısı birden fazla kez okunursa (örneğin hem FSM
hem entity katmanı tüketirse) bir edge komutu iki kez işlenebilir.
Holodepth: poll()'u döngüde tek noktadan çağırın, sonucu o
karenin tüm tüketicilerine aynı anlık görüntü olarak dağıtın.
Güvenli girdi katmanı: simüle kod
Örnekler öğretim amaçlıdır; tuş eşlemesi ürün kararıdır. Aşağıdaki
iskelet klavye ve pointer'ı aynı komut çıktısına indiren tek bir
poll() yüzeyini gösterir.
Simüle · keyboard + pointer poll.
const HELD = new Set()
const EDGES = []
const MAX_EDGES = 8
const KEY_MAP = {
KeyW: 'up', KeyA: 'left', KeyS: 'down', KeyD: 'right',
Space: 'jump', Escape: 'pause',
}
function onKeyDown(e) {
const cmd = KEY_MAP[e.code]
if (!cmd) return
if (!HELD.has(cmd)) EDGES.push(cmd)
if (EDGES.length > MAX_EDGES) EDGES.shift()
HELD.add(cmd)
if (['Space', 'ArrowUp', 'ArrowDown'].includes(e.code)) e.preventDefault()
}
function onKeyUp(e) {
const cmd = KEY_MAP[e.code]
if (cmd) HELD.delete(cmd)
}
let pointerDown = false
let pointerNorm = { x: 0, y: 0 }
export function bindInput(canvas) {
window.addEventListener('keydown', onKeyDown)
window.addEventListener('keyup', onKeyUp)
canvas.addEventListener('pointerdown', () => { pointerDown = true })
canvas.addEventListener('pointerup', () => { pointerDown = false })
canvas.addEventListener('pointermove', (e) => {
const r = canvas.getBoundingClientRect()
pointerNorm.x = ((e.clientX - r.left) / r.width) * 2 - 1
pointerNorm.y = -(((e.clientY - r.top) / r.height) * 2 - 1)
})
}
export function pollInput() {
const edges = EDGES.splice(0, EDGES.length)
return {
edges,
hold: new Set(HELD),
pointer: { down: pointerDown, ...pointerNorm },
}
}
export function unbindInput() {
window.removeEventListener('keydown', onKeyDown)
window.removeEventListener('keyup', onKeyUp)
HELD.clear()
EDGES.length = 0
}
Simüle · FSM lite + döngü köprüsü.
const STATES = ['menu', 'playing', 'paused', 'gameover']
export function createFsm(initial = 'menu') {
let state = initial
const transitions = {
menu: { start: 'playing' },
playing: { pause: 'paused', die: 'gameover' },
paused: { resume: 'playing', quit: 'menu' },
gameover: { restart: 'menu' },
}
return {
get state() { return state },
dispatch(event) {
const next = transitions[state]?.[event]
if (next && STATES.includes(next)) state = next
},
/** Faz hangi komutları işler */
accepts(cmd) {
if (state === 'menu') return ['start'].includes(cmd)
if (state === 'playing') return ['up','down','left','right','jump','pause','fire'].includes(cmd)
if (state === 'paused') return ['resume','quit'].includes(cmd)
if (state === 'gameover') return ['restart'].includes(cmd)
return false
},
}
}
/** onTick içinde: const inp = pollInput(); fsm + inp → entity komutları */
Yanlış girdi kalıpları
Bu kalıpların çoğu küçük prototipte fark edilmez; oyuncu sayısı veya cihaz çeşitliliği arttığında yüzeye çıkar.
- Mantık içinde doğrudan
keydowndinlemek - Edge ve hold ayrımı yapmamak
- FSM olmadan menü ve oyunu karıştırmak
- Sınırsız edge buffer
- Gamepad kopunca eski eksen değeri
- Aynı
poll()sonucunu birden fazla tüketiciye tekrar tekrar okutmak - Hexovian action map'i web'e taşıma beklentisi
Üç girdi profili
Üç profil aynı komut sözlüğünü paylaşır; ayrışan şey hangi cihazın birincil, hangisinin yedek olduğu ve FSM'in hangi fazda hangi girdiyi kabul ettiğidir.
-
01 · Girdi
Klavye arcade
- Yüzey
- WASD + space
- FSM
- menu / playing
- Risk
- preventDefault aşırısı
-
02 · Girdi
Pointer aim
- Yüzey
- Normalize pointer + click
- FSM
- playing only fire
- Risk
- Raycast ile karışım
-
03 · Girdi
Hibrit pad
- Yüzey
- Pad + klavye yedek
- FSM
- pause her yüzeyde
- Risk
- Çift komut çakışması
Gecikme ve bütçe
Input dilimi ms olarak küçük olmalı; ağır iş mantık katmanına taşınır.
Platform FPS ile «input → tepki» gecikmesini hissedilen jank ile
birlikte okuyun. Çok fazla preventDefault veya global
listener sayısı ana iş parçacığını yorar.
Gecikmeyi ölçerken tek bir sayı yetmez: p50 «ortalama hissi», p99 «en kötü anı» temsil eder. Holodepth: input → görsel tepki arasındaki süreyi birkaç yüz örnekle toplayın, ortalamaya değil kuyruk değerine güvenin — oyuncu p99'u hisseder, p50'yi unutur.
Holodepth perspektifleri
Komut sözlüğü tek kaynak
Klavye, pointer ve pad aynı string komutlara map edilir; entity katmanı cihaz bilmez. Yeni bir cihaz (ör. dokunmatik ekran jesti) eklemek, yalnızca eşleme tablosuna bir satır eklemek demektir.
FSM faz sınırı
Her fazın kabul listesi kısa tutulur; «her yerde her şey» bakım borcudur. Bir komutun hangi fazlarda anlamlı olduğunu tabloya yazmak, kodu okuyanın aklında tutmasından daha güvenilirdir.
Tampon, döngünün uzantısıdır
Girdi tamponu döngü yaprağındaki «input dilimi»nin somut hali; ikisini ayrı kavram sanmak sıralama hatalarına yol açar.
Teşhis maddeleri
- poll() tik başında mı?
- Edge / hold ayrıldı mı?
- Buffer sınırlı mı?
- FSM geçişleri tabloda mı?
- unbind/dispose var mı?
- Gamepad kopuşu sıfırlanıyor mu?
- Netcode veya glTF sızmış mı?
Holodepth içgörüsü
Girdi katmanı ince tutulursa entity ve fizik köprüsü test edilebilir kalır. Döngü zaman verir; FSM faz verir; komut sözlüğü cihazları birleştirir.
Sonraki kapı: entity eşlemesi
Komutlar üretildikten sonra sahne nesnelerine bağlanmalıdır. Üçüncü yaprak mesh ile oyun nesnesini hafif modelde eşler; update hook'ları döngüye takılır. Tam ECS Hexovian'da kalır.