Skip to content

Quick Start

This guide walks you through building a minimal application using the Engine class and ECS. By the end, you will have created an engine, defined components, spawned entities, and run a system.

Hello Engine

The fastest way to start is with createEngine() and a preset. The engine manages the game loop, time, and scheduling for you.

1. Create an Engine

typescript
import { createEngine, Game2DPreset } from '@web-engine-dev/engine';

// Create an engine with the 2D game preset (60 FPS, fixed timestep)
const engine = createEngine(Game2DPreset);

Available presets: Game2DPreset, Game3DPreset, MinimalPreset, HighPerformancePreset, TurnBasedPreset, MobilePreset, HeadlessPreset.

Or use the shorthand factory:

typescript
import { create2DEngine } from '@web-engine-dev/engine';

const engine = create2DEngine();

2. Define Components

Components are pure data containers with a typed schema:

typescript
import { defineComponent, defineTag } from '@web-engine-dev/ecs';

// Data components with typed fields
const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
const Velocity = defineComponent('Velocity', { x: 'f32', y: 'f32' });

// Tags are zero-size marker components
const IsPlayer = defineTag('IsPlayer');

Available field types: i8, i16, i32, u8, u16, u32, f32, f64, bool.

3. Spawn Entities

Use the engine's world to create entities:

typescript
const world = engine.world;

// Spawn the player
const player = world.spawn();
world.insert(player, Position, { x: 0, y: 0 });
world.insert(player, Velocity, { x: 2, y: 1 });
world.insert(player, IsPlayer, {});

// Spawn some enemies
for (let i = 0; i < 100; i++) {
  const enemy = world.spawn();
  world.insert(enemy, Position, {
    x: Math.random() * 100,
    y: Math.random() * 100,
  });
  world.insert(enemy, Velocity, {
    x: (Math.random() - 0.5) * 2,
    y: (Math.random() - 0.5) * 2,
  });
}

4. Query and Run Systems

Build queries to match entities by component pattern, then define systems that process them:

typescript
import { queryBuilder, defineSystem, CoreSchedule } from '@web-engine-dev/ecs';

const movables = queryBuilder().with(Position, Velocity).build();

const movementSystem = defineSystem({
  name: 'Movement',
  fn: (world) => {
    // Access the engine's time manager for frame delta
    const time = engine.timeManager.time;
    const dt = time.scaledDelta;

    for (const { entity, components: [pos, vel] } of world.run(movables)) {
      pos.x += vel.x * dt;
      pos.y += vel.y * dt;

      // CRITICAL: Write changes back to storage
      world.insert(entity, Position, pos);
    }
  },
});

// Register the system on the Update schedule
engine.scheduler.addSystem(CoreSchedule.Update, movementSystem);

CRITICAL: Component Data is Copied

world.get() and query iteration return copies of component data, not references. The ECS uses SoA (Struct of Arrays) columnar storage internally. You must call world.insert() to persist any changes.

typescript
// BUG: Changes are silently lost
const pos = world.get(entity, Position);
pos.x += 10; // Modifies local copy only!

// CORRECT: Write changes back
const pos = world.get(entity, Position);
pos.x += 10;
world.insert(entity, Position, pos); // Persists to storage

Resources are different -- world.getResource() returns the actual stored object, so mutations persist automatically.

5. Start the Engine

typescript
// start() calls init() automatically, then runs the game loop
await engine.start();

That's it. The engine handles requestAnimationFrame, time management, and system execution.

Putting It All Together

typescript
import {
  createEngine,
  Game2DPreset,
  defineComponent,
  defineTag,
  defineSystem,
  queryBuilder,
  CoreSchedule,
} from '@web-engine-dev/engine';

// Components
const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
const Velocity = defineComponent('Velocity', { x: 'f32', y: 'f32' });
const IsPlayer = defineTag('IsPlayer');

// Query
const movables = queryBuilder().with(Position, Velocity).build();

// System
const movementSystem = defineSystem({
  name: 'Movement',
  fn: (world) => {
    for (const { entity, components: [pos, vel] } of world.run(movables)) {
      pos.x += vel.x;
      pos.y += vel.y;
      world.insert(entity, Position, pos);
    }
  },
});

// Engine with callbacks
const engine = createEngine(Game2DPreset, {
  onInit(engine) {
    engine.scheduler.addSystem(CoreSchedule.Update, movementSystem);

    // Spawn player
    const player = engine.world.spawn();
    engine.world.insert(player, Position, { x: 0, y: 0 });
    engine.world.insert(player, Velocity, { x: 2, y: 1 });
    engine.world.insert(player, IsPlayer, {});
  },
  onUpdate(engine) {
    // Read data every frame (optional -- systems handle most logic)
    const pos = engine.world.get(
      [...engine.world.entities()][0]!,
      Position,
    );
    if (pos) {
      console.log(`Player at (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`);
    }
  },
});

await engine.start();

Engine Lifecycle Callbacks

The createEngine() function accepts lifecycle callbacks for hooking into the engine loop:

CallbackWhen it runs
onInitOnce, during engine.init()
onFrameStartStart of every frame
onFixedUpdateFixed timestep updates (physics)
onUpdateEvery frame (variable timestep)
onLateUpdateAfter all updates
onFrameEndEnd of every frame
onPause / onResumeWhen the engine is paused/resumed
onStopWhen the engine is stopped
onDisposeDuring cleanup

Using Plugins

The engine supports plugins for composing functionality:

typescript
import {
  create2DEngine,
  createInputPlugin,
  createRenderPlugin,
  createPhysics2DPlugin,
} from '@web-engine-dev/engine';

const canvas = document.querySelector('canvas')!;

const engine = create2DEngine();
engine.addPlugin(createRenderPlugin({ canvas }));
engine.addPlugin(createInputPlugin());
engine.addPlugin(createPhysics2DPlugin());

await engine.start();

Built-in plugins: RenderPlugin, InputPlugin, AudioPlugin, Physics2DPlugin, Physics3DPlugin, AnimationPlugin, SpritePlugin, ParticlePlugin, ScriptingPlugin.

Going Deeper: Raw ECS

If you need full control without the Engine class, you can use the ECS directly:

typescript
import {
  createWorld,
  defineComponent,
  queryBuilder,
  Scheduler,
  CoreSchedule,
  defineSystem,
} from '@web-engine-dev/ecs';

const Position = defineComponent('Position', { x: 'f32', y: 'f32' });

const world = createWorld();
const scheduler = new Scheduler();

const entity = world.spawn();
world.insert(entity, Position, { x: 0, y: 0 });

const printSystem = defineSystem({
  name: 'Print',
  fn: (w) => {
    const query = queryBuilder().with(Position).build();
    for (const { components: [pos] } of w.run(query)) {
      console.log(`(${pos.x}, ${pos.y})`);
    }
  },
});

scheduler.addSystem(CoreSchedule.Update, printSystem);
scheduler.run(world, CoreSchedule.Update, 0);

This gives you complete control over the game loop, time management, and scheduling -- but you manage all of it yourself.

Next Steps

Proprietary software. All rights reserved.