diff --git a/USAGE.md b/USAGE.md index da1f3d2..e782004 100644 --- a/USAGE.md +++ b/USAGE.md @@ -10,12 +10,15 @@ Entity-Component-System with an observable-style API for TypeScript. Built for g - [API](#api) - [World](#world) - [Components](#components) + - [Singleton Components](#singleton-components) - [Queries](#queries) - [Observable Queries](#observable-queries) - [Change Tracking](#change-tracking) - [Relationships](#relationships) - [Events](#events) - [Serialization](#serialization) + - [Commands](#commands) + - [Behaviour Trees](#behaviour-trees) - [TypeScript Inference](#typescript-inference) ## Installation @@ -132,6 +135,33 @@ Get returns a **live mutable reference** — no defensive copies. Mutations are --- +### Singleton Components + +For global state (score, board, config) that doesn't need per-entity tracking. A single internal entity is created lazily and reused for all singleton components. + +```ts +const Score = defineComponent("score", { points: 0, level: 1 }); + +// Add (auto-creates the backing entity on first call) +world.addSingleton(Score); + +// Get / set / check — no entity argument needed +world.getSingleton(Score).points += 100; +world.hasSingleton(Score); // true +world.setSingleton(Score, { points: 0, level: 2 }); + +// Try-get +const s = world.tryGetSingleton(Score); // Score | undefined + +// Mark dirty for change tracking +world.markDirtySingleton(Score); + +// Remove (destroys the backing entity if it becomes bare) +world.removeSingleton(Score); +``` + +--- + ### Queries Create filters with `query()`. Chain `.without()` to exclude components. @@ -324,6 +354,128 @@ world2.get(e2, Health); // { current: 75, max: 100 } --- +### Commands + +Decouple input from game logic. Define command components, register handlers, spawn command entities from input — the `CommandQueue` drains and dispatches them each frame. + +```ts +import { CommandQueue } from "ecs-observable/commands"; + +const MoveLeft = defineComponent("moveLeft", {}); +const MoveRight = defineComponent("moveRight", {}); + +const queue = new CommandQueue(world); + +// Register handlers +queue.handle(MoveLeft, () => { + player.x -= 1; +}); + +queue.handle(MoveRight, () => { + player.x += 1; +}); + +// Input → spawn command entities +onKey("ArrowLeft", () => { + const cmd = world.spawn(); + world.add(cmd, MoveLeft); +}); + +// Each frame — drain and dispatch +queue.execute(); +``` + +Command entities are automatically destroyed after processing if they become bare. + +--- + +### Behaviour Trees + +Behaviour trees control game flow by composing tasks into a tree. Each node in the tree is an ECS entity with a `Task` component. Parent-child relationships are `ChildOf` edges. This means you can query, observe, and serialize the tree just like any other ECS data. + +`buildTree()` takes a declarative definition and materializes it into entities, returning a fully-wired `TaskRunner`. + +```ts +import { buildTree, Cancel } from "ecs-observable/bt"; +``` + +#### Leaf patterns + +**One-shot** — just return. Implicit success. +```ts +{ kind: "leaf", run: () => { doWork(); } } +``` + +**Fail** — throw any error. +```ts +{ kind: "leaf", run: () => { throw new Error("bad"); } } +``` + +**Cancel** — throw the `Cancel` symbol. +```ts +{ kind: "leaf", run: () => { throw Cancel; } } +``` + +**Ongoing** — generator function. Each `yield` suspends until next tick. The yielded value is the delay in ms (or nothing for next frame). Completion = success. +```ts +{ kind: "leaf", *run() { + while (true) { + const dt: number = yield; // delta time from runner.tick(dt) + timer.accumulator += dt; + if (timer.accumulator >= timer.interval) { + // ... act ... + } + } +} } +``` + +#### Composite nodes + +```ts +{ kind: "sequential", children: [a, b, c] } // left-to-right, all must succeed +{ kind: "selector", children: [a, b, c] } // left-to-right, first success wins +{ kind: "parallel", children: [a, b, c] } // all at once, all must succeed +{ kind: "random", children: [a, b, c] } // pick one child each activation +{ kind: "repeat", child: a } // decorator — re-run child forever +``` + +#### Full example + +```ts +const runner = buildTree(world, { + kind: "parallel", + children: [ + { + kind: "leaf", + *run() { + while (true) { + const dt: number = yield; + updatePhysics(dt); + } + }, + }, + { + kind: "repeat", + child: { + kind: "sequential", + children: [ + { kind: "leaf", run: () => { handleInput(); } }, + { kind: "leaf", run: () => { render(); } }, + ], + }, + }, + ], +}); + +// Kick off +runner.schedule((runner as any).root); + +// Game loop +setInterval(() => runner.tick(16), 16); +``` + +--- + ## TypeScript Inference Components infer their type from the defaults object — no separate type declaration needed.