Skip to content

Engine modules

Every host subsystem is an EngineModule:

ts
import type { EngineContext, EngineModule } from '@aosengine/core';

/** What other modules and game code get from `engine.get('spin')`. */
interface SpinService {
  /** Radians turned so far. */
  angle: number;
}

/**
 * A module that just turns.
 *
 * @returns The module, ready to pass to `createEngine`.
 */
export function spin(): EngineModule {
  const service: SpinService = { angle: 0 };
  return {
    id: 'spin',
    order: 100,
    init: (ctx: EngineContext) => {
      console.log(ctx.caps.webgpu);
      return service; // published as engine.get('spin')
    },
    fixedUpdate: (dt) => {
      service.angle += dt;
    },
    dispose: () => {
      service.angle = 0;
    },
  };
}

createEngine({ modules: [input(), physics(), audio(), splat()] }) registers them; the engine drives them. A module owns its own resources and releases them in dispose; the engine never reaches inside one.

Order

Modules run sorted by order (default 0), then by registration order. init runs in that order, dispose in reverse. Rough convention:

orderWho
-100input
0physics
100gameplay, the wasm sandbox
200rendering helpers, splats, overlays

Because init is awaited one module at a time, a module may call ctx.get('physics') for anything registered ahead of it.

Services and declaration merging

A module publishes a service by returning it from init, or by calling ctx.registerService(id, service). engine.get(id) is typed through the EngineServices interface, which core deliberately leaves empty. Each package merges its own entry in:

ts
import type { PhysicsService } from './service.js';

declare module '@aosengine/core' {
  interface EngineServices {
    physics: PhysicsService;
  }
}

Importing the package is then enough for engine.get('physics') to be typed — no cast at the call site, and core never has to know the package exists. The same pattern extends EngineEventMap for typed events.

Per-frame hooks

All optional; a module is only called for the hooks it implements.

HookWhen
beginFrame()Before the frame's fixed steps
fixedUpdate(dt)Once per fixed step, dt always 1/60
update(dt, alpha)Once per frame, before rendering
endFrame()After rendering

None of them may allocate.

See also