holodepth

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.

Örnek geçişler
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 keydown dinlemek
  • 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

  1. poll() tik başında mı?
  2. Edge / hold ayrıldı mı?
  3. Buffer sınırlı mı?
  4. FSM geçişleri tabloda mı?
  5. unbind/dispose var mı?
  6. Gamepad kopuşu sıfırlanıyor mu?
  7. 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.