2D Game Engine

Architektura

Warstwy silnika, izolacja adaptera przez porty, przepływ klatki, granice modułów i zasady wydajności, które pilnują testy.

Warstwy#

text
┌───────────────┐   ┌──────────────────────────────┐   ┌──────────────────┐
│  Kod gry      │   │            Engine            │   │     Adapter      │
│  Sceny        │──▶│  Config + Time + EventBus    │──▶│ PhaserRenderLayer│──▶ Phaser.Game
│  Komponenty   │   │  SceneManager  ObjectIndex   │   │ PhaserPhysics    │──▶ Arcade
│  Obiekty      │   │  AssetManager  InputManager  │   │ PhaserCamera     │──▶ Camera
└───────────────┘   │  PhysicsManager CameraManager│   │ PhaserInput      │──▶ Keyboard
                    │  AudioManager   TweenManager │   │ PhaserAudio      │──▶ WebAudio
                    └──────────────┬───────────────┘   │ PhaserAssets     │──▶ Loader
                                   │                   └──────────────────┘
                     core/ports.ts (6 interfejsów)             │
                                                               ▼
                                                      ┌──────────────────┐
                                                      │ WASM (opcjonalnie)│
                                                      └──────────────────┘
KatalogZna PhaseraZawartość
corenieEngine, Scene, GameObject, Component, ObjectIndex, Time, porty
componentsnieTransform
animationnieEasing (28 krzywych), Tween, TweenManager, sekwencje
audionieAudioManager, kanały, SoundHandle, kolejka autoplay
systemsnieEventBus, AssetManager, InputManager, PhysicsManager, CameraManager, Logger
debugnieLogBuffer, LogPanel, DebugViews, Profiler, Diagnostics
pluginsniePluginHost, rejestry, manifest, parser zakresów wersji
adapterstakPhaserAdapter + backendy i warstwa renderu

Izolacja adaptera#

Rdzeń nie importuje Phasera. Pilnują tego dwa mechanizmy:

  1. Reguła ESLint no-restricted-imports.
  2. Test tests/unit/PublicApi.test.ts, który skanuje źródła i wywala się na import Phasera lub katalogu adapters/ w core/.

Cały dostęp do runtime idzie przez sześć interfejsów z core/ports.ts:

ts
interface RuntimeAdapter {
  render: RenderPort
  physics: PhysicsPort
  camera: CameraPort
  input: InputPort
  audio: AudioPort
  assets: AssetBackend
}

Nowy backend = sześć nowych implementacji. Metody opcjonalne są wykrywane przez supports* i wołane z ?? false / ?? null, więc minimalny adapter nie wywala się w runtime.

GameObject to encja domenowa

GameObject trzyma tagi, grupy, transform lokalny, hierarchię i komponenty oraz deskryptory render i body — nigdy instancję Phasera. Renderer dostaje deskryptor, nie obiekt sceny.

Przepływ klatki#

Kolejność faz internalUpdate sceny:

text
applyPhysicsPositions        odczyt pozycji z fizyki
→ tweenManager.update        tweeny mutują zwykłe właściwości
→ komponenty input           faza wejścia
→ Scene.update(dt)           logika gry
→ komponenty behavior        zachowania
→ komponenty physics         ciała
→ komponenty animation       klatki animacji
→ komponenty render          przygotowanie renderu
→ kolejka pendingDestroy     usuwanie obiektów
→ syncRender                 tylko obiekty brudne
→ debug                      overlay, panel, profiler

Cykl życia obiektu#

text
createObject → render.create → aktywacja w drzewie → aktualizacje
→ destroyObject → potomkowie liść-pierwszy → component.onDestroy
→ usunięcie z indeksu → render.destroy

Zasoby i zapisy#

  • Assety: declareAssets → AssetManifest (id, duplikaty, referencje, budżet rozmiaru) → AssetBackend.load → statusy idle/queued/loading/ready/failed → refcount właściciela zwalniany w shutdown.
  • Zapisy: validateProject → exportProject (koperta + suma kontrolna) → ProjectRepository (tmp → odczyt zwrotny → backup → commit) lub PlayerSaveRepository → SaveStore.

SaveStore to pięć metod (read, write, remove, keys, clear). SqlSaveStore przyjmuje dowolny obiekt z query(sql, params) — dzięki temu w bundlu gry nie ma pg ani mysql2.

Wydajność#

  • Brak alokacji w gorącej pętli poza jedną kopią listy subskrybentów w EventBus.emit i przebudową listy komponentów po zmianie struktury.
  • Render dostaje tylko obiekty brudne — scena z 79 obiektami wysyła w klatce zwykle 1–3.
  • Odczyt world jest O(1) po przeliczeniu; zmiana transformacji unieważnia tylko poddrzewo.
  • Aktualizacja komponentów: 0,72 ms na klatkę dla 20 000 obiektów, czyli ok. 0,07 ms dla 2 000.
  • Profiler jest wyłączony domyślnie — wtedy begin/end zwracają się natychmiast.

Więcej w Wydajność i benchmarki.

Decyzje architektoniczne#

Każda decyzja, której cofnięcie kosztowałoby więcej niż tydzień pracy, ma swój rekord ADR: izolacja adaptera, wersjonowany JSON, brak ECS, opcjonalny WASM, brak SPA fallback. Pełna lista: Decyzje architektoniczne.