2D Game Engine

WebAssembly

Opcjonalny most TS↔WASM dla ciężkiego compute. Instalacja Emscripten, kompilacja C++23, zasady granicy i graceful fallback.

Kiedy WASM ma sens#

WASM jest opcjonalny. Most jest przygotowany, ale dodawaj moduł tylko wtedy, gdy benchmark TS vs WASM wykaże mierzalną korzyść (ADR 0004).

Typowe kandydaci: pathfinding A*, generowanie terenu, symulacje cząsteczek, ciężka geometria.

Zasady granicy TS↔WASM#

  1. Jedno wywołanie z dużą paczką danych > wiele drobnych wywołań. Przekroczenie granicy kosztuje.
  2. Nie ujawniaj wewnętrznych klas C++. Cienka fasada extern "C" lub embind.
  3. required: false nie wywala gry. Brak pliku WASM = graceful fallback do TS.

Instalacja Emscripten#

bash
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install latest
./emsdk/emsdk activate latest
source ./emsdk/emsdk_env.sh

Kompilacja#

bash
emcmake cmake -S native/core -B native/core/build-wasm
cmake --build native/core/build-wasm
ls native/core/build-wasm/*.wasm

Skopiuj artefakty do playgroundu:

bash
cp native/core/build-wasm/core.* apps/playground/public/wasm/

Użycie w silniku#

ts
const engine = createEngine({
  parent: 'app',
  width: 800,
  height: 600,
  wasm: { wasmUrl: '/wasm/core.wasm', required: false }
})

await engine.getWasm().load()

if (engine.getWasm().isReady()) {
  const path = engine.getWasm().call<Vec2i[]>('findPathAStar', grid, start, goal)
}

API mostu#

ts
engine.getWasm().isReady()
engine.getWasm().getState()
engine.getWasm().getError()
engine.getWasm().getInstance()
engine.getWasm().call<T>(name, ...args): T | null
engine.getWasm().reset()

Przykład: pathfinding A*#

TS przekazuje płaską tablicę blocked (Uint8Array), C++ zwraca wektor pozycji:

ts
const grid = {
  width: 64,
  height: 64,
  blocked: new Uint8Array(4096)
}

const path = engine.getWasm().call<Vec2i[]>(
  'findPathAStar',
  grid,
  { x: 0, y: 0 },
  { x: 63, y: 63 }
)

if (path) {
  for (const step of path) {
    enemy.moveTo(step.x * TILE, step.y * TILE)
  }
}

Implementacja po stronie C++ żyje w native/core/include/pathfinding.hpp. Testuj algorytm natywnie i w przeglądarce (native/core/tests).

Graceful fallback#

ts
async function findPath(grid: Grid, from: Vec2i, to: Vec2i): Promise<Vec2i[]> {
  const wasm = engine.getWasm()
  if (wasm.isReady()) {
    const result = wasm.call<Vec2i[]>('findPathAStar', grid, from, to)
    if (result) return result
  }
  return findPathAStarTS(grid, from, to)
}

Mierz, nie zgaduj

„WASM będzie szybszy” to hipoteza, nie fakt. Dla małych danych narzut kopiowania przez granicę potrafi zjeść cały zysk. Uruchom npm run bench dla obu wariantów i dopiero wtedy decyduj.