Engine-to-game service registries
Small single-slot registries keep generic engine code from importing the active game.
Current seams
GameplayMapLauncher: engine console to browser game/lobby launch;NetworkConnector: engine UI to the selected game network client;GameFrameHook: engine camera controllers to the game's per-frame work, so a freefly or spectator camera can tick the network and draw remote players without importing the game.
The first two expose register, get and clear; GameFrameHook uses setGameFrameHook(hook) and clears by passing null. The application composition root owns registration:
// From src/main.ts
import { registerGameplayMapLauncher } from './engine/console/GameplayMapLauncher';
import { registerNetworkConnector } from './engine/ui/components/NetworkConnector';
registerGameplayMapLauncher(fpsMapLauncher);
registerNetworkConnector(fpsNetworkConnector);The engine consumer must handle a missing adapter and report a useful error rather than importing an FPS fallback.
Test hygiene
afterEach(() => {
clearGameplayMapLauncher();
clearNetworkConnector();
setGameFrameHook(null);
});Use the clear functions from the same registry modules. Registrations are process-global singleton state.
When to use a registry
Use this pattern when:
- there is one selected implementation;
- game code provides it and engine code consumes it;
- no dependency graph or phased initialization is needed;
- bootstrap can wire it explicitly.
Use ServiceLocator for engine-owned services with initialization order, scene phases, and coordinated disposal. Use EventBus for events with zero or multiple listeners. Use direct dependencies inside one subsystem.
Dependency direction
game/bootstrap -> register adapter -> engine interface
engine consumer -> get adapter -> call interfaceThe interface module stays game-agnostic. Never make the interface implementation silently reach back into a hard-coded game module.
