2D Game Engine

Wydajność i benchmarki

Zasady gorącej pętli, dirty sync, benchmarki Node z progami regresji i raport rozmiaru bundla. Liczby z pomiarów, nie z obietnic.

Zasady gorącej pętli#

  • Brak alokacji poza jedną kopią listy subskrybentów w EventBus.emit i przebudową listy komponentów po zmianie struktury.
  • Dirty sync — render dostaje tylko obiekty brudne. Scena z 79 obiektami wysyła w klatce zwykle 1–3.
  • Odczyt world O(1) po przeliczeniu; zmiana transformacji unieważnia tylko poddrzewo.
  • Profiler domyślnie wyłączony — begin/end zwracają się natychmiast.

Zmierzone liczby#

PomiarWynik
Aktualizacja komponentów, 20 000 obiektów0,72 ms/klatkę
Aktualizacja komponentów, 2 000 obiektów~0,07 ms/klatkę
Obiekty brudne w klatce (scena 79 obiektów)zwykle 1–3
Benchmarki Node17 przypadków
Próg regresji bench:check40%

Bez obietnic sprzętowych

Liczby bezwzględne zależą od maszyny, dlatego bench:check porównuje wyniki z baseline na tej samej maszynie i używa progów względnych. Silnik nie publikuje limitów sprzętowych, których nie zmierzył.

Benchmarki Node#

bash
npm run bench
npm run bench:check
npm run bench:baseline

17 przypadków w kategoriach: objects, queries, collisions, tilemap, assets. Wyniki trafiają do benchmarks/latest.json, a bench:check porównuje je z benchmarks/baseline.json z progiem 40% — kod wyjścia 1 nadaje się do CI.

ts
import { describe, bench } from 'vitest'

describe('queries', () => {
  bench('findByTag', () => {
    scene.findByTag('enemy')
  })
})

Raport rozmiaru bundla#

bash
npm run bundle:report
npm run bundle:check
npm run bundle:baseline

dist/release.json zawiera rozmiar, gzip, brotli i SHA-256 każdego pliku. Progi:

ZakresPróg
Suma gzip katalogu+10% względem baseline
Pojedynczy plik > 16 kB+35%
playground.html (single-file)+5%

bundle:check kończy się kodem 1 przy przekroczeniu — działa jako bramka CI bez dodatkowego parsowania.

Checklist wydajnościowy dla gry#

  1. Mierz, zanim optymalizujesz. Włącz profiler i znajdź najdroższe znaczniki.
  2. Trzymaj logikę w komponentach z właściwą fazą. input przed update, fizyka w fixedUpdate.
  3. Nie alokuj w pętli. Współdziel wektory, tablice i deskryptory; używaj puli obiektów dla pocisków.
  4. Tagi zamiast skanowania. findByTag korzysta z indeksu, find skanuje wszystko.
  5. Ogranicz dirty sync. Zmieniaj tylko to, co naprawdę się rusza; HUD z setScrollFactor(0).
  6. Assety z budżetem. sizeReport(limits) i diagnose() wyłapią nieużywane i za ciężkie pliki.
  7. WASM tylko z pomiarem. Porównaj TS vs WASM na realnych danych.

Przykład: pula pocisków#

ts
class BulletPool {
  private pool: GameObject[] = []

  constructor(private scene: Scene, size = 64) {
    for (let i = 0; i < size; i++) {
      const bullet = scene.createRect(6, 6, { x: -100, y: -100, fill: '#ffd75e' })
      bullet.setActive(false).setVisible(false)
      this.pool.push(bullet)
    }
  }

  fire(x: number, y: number, vx: number) {
    const bullet = this.pool.find((b) => !b.active)
    if (!bullet) return
    bullet.setPosition(x, y).setActive(true).setVisible(true)
    bullet.add(new TimedDestroy(2))
  }
}

Zero alokacji w trakcie strzelania — obiekty powstają raz, przy stworzeniu puli.