2D Game Engine

Zdarzenia i harmonogram

EventBus, Scheduler, Timer i Random — komunikacja między systemami, zadania z uchwytami, izolacja błędów i deterministyczna losowość.

EventBus#

Synchroniczna magistrala zdarzeń. Emisja idzie po kopii listy subskrybentów, a wyjątek jednego handlera nie blokuje pozostałych.

ts
const off = this.getEvents().on<{ score: number }>('score:changed', ({ score }) => {
  this.hud.setText(`Wynik: ${score}`)
})

this.getEvents().emit('score:changed', { score: 120 })

off()

API#

ts
on(event, handler): () => off
subscribe(event, handler): () => off
subscribeForScene(sceneId, event, handler): () => off
off(event, handler); offById(event, id)
emit(event, payload)
clearScene(sceneId); clearAll(); listenerCount(event)

Subskrypcje per scena#

ts
this.getEvents().subscribeForScene(this.id, 'player:died', () => {
  this.getCamera().shake(200, 0.01)
})

Przy shutdown() sceny wszystkie jej subskrypcje znikają automatycznie — nie ma wycieków między scenami.

Zdarzenia silnika#

engine:start, engine:stop, engine:pause, engine:resume, engine:update, scene:start, scene:stop, assets:*, audio:unlocked.

Scheduler#

Zadania z uchwytami, właścicielami i izolacją błędów:

ts
const handle = this.getScheduler().delay(2, () => {
  this.spawnWave(2)
}, { owner: this.id })

this.getScheduler().interval(0.5, () => {
  this.regenHp(1)
}, { owner: this.id, repeat: 10 })

this.getScheduler().cancel(handle)
this.getScheduler().cancelOwner(this.id)
ts
scheduler.delay(seconds, callback, options?): TaskHandle
scheduler.interval(seconds, callback, options?): TaskHandle
scheduler.cancel(handle); scheduler.cancelOwner(owner): number; scheduler.clear()
scheduler.has(id); scheduler.count(owner?); scheduler.errorCount

TaskOptions = { owner?: string; repeat?: number; onError?: (e) => void }.

Izolacja błędów

Wyjątek w zadaniu nie zatrzymuje innych zadań. Trafia do errorCount i do onError, jeśli podano. Timery sceny są anulowane przy shutdown().

Przykład: fale przeciwników#

ts
class GameScene extends Scene {
  create() {
    this.getScheduler().interval(8, () => this.spawnWave(), {
      owner: this.id,
      onError: (e) => this.getLogger().error('spawnWave failed', e)
    })
  }

  private spawnWave() {
    const count = this.getRandom().int(3, 6)
    for (let i = 0; i < count; i++) {
      this.createSprite('enemy', {
        x: this.getRandom().float(100, 1180),
        y: -40,
        tags: ['enemy']
      })
    }
  }
}

Timer#

Lżejszy wariant schedulera do prostych opóźnień:

ts
this.getTimer().delay(1.5, () => this.flashScreen())
this.getTimer().interval(0.1, () => this.tick())
this.getTimer().pending
this.getTimer().clear()

Random#

Deterministyczna losowość z seedem (domyślnie 1337):

ts
const random = this.getRandom()

random.next()
random.float(0, 100)
random.int(1, 6)
random.bool(0.3)
random.sign()
random.pick(['sword', 'axe', 'bow'])

const lootRandom = random.fork('loot')
  • Ten sam seed → ta sama sekwencja, niezależnie od maszyny.
  • fork(salt) tworzy niezależny strumień — np. osobny dla lootu i osobny dla AI, żeby nie zaburzać sobie sekwencji.
ts
const engine = createEngine({ parent: 'app', seed: 20260 })

Wskazówka

Nigdy nie używaj Math.random() w logice gry — łamie determinizm i powtarzalność zapisów. Cała losowość przechodzi przez Random.