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
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:
import { create2DEngine } from '@web-engine-dev/engine';
const engine = create2DEngine();2. Define Components
Components are pure data containers with a typed schema:
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:
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:
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.
// 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 storageResources are different -- world.getResource() returns the actual stored object, so mutations persist automatically.
5. Start the Engine
// 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
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:
| Callback | When it runs |
|---|---|
onInit | Once, during engine.init() |
onFrameStart | Start of every frame |
onFixedUpdate | Fixed timestep updates (physics) |
onUpdate | Every frame (variable timestep) |
onLateUpdate | After all updates |
onFrameEnd | End of every frame |
onPause / onResume | When the engine is paused/resumed |
onStop | When the engine is stopped |
onDispose | During cleanup |
Using Plugins
The engine supports plugins for composing functionality:
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:
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
- Project Setup -- Set up a complete project with Vite and TypeScript
- First Game -- Build a playable game with rendering
- ECS Concepts -- Deep dive into the Entity Component System
- API Reference -- Full API documentation