docs: add singleton components, commands, and behaviour trees

This commit is contained in:
hypercross 2026-06-02 00:38:26 +08:00
parent 4182bc1578
commit ef9abf03c6
1 changed files with 152 additions and 0 deletions

152
USAGE.md
View File

@ -10,12 +10,15 @@ Entity-Component-System with an observable-style API for TypeScript. Built for g
- [API](#api) - [API](#api)
- [World](#world) - [World](#world)
- [Components](#components) - [Components](#components)
- [Singleton Components](#singleton-components)
- [Queries](#queries) - [Queries](#queries)
- [Observable Queries](#observable-queries) - [Observable Queries](#observable-queries)
- [Change Tracking](#change-tracking) - [Change Tracking](#change-tracking)
- [Relationships](#relationships) - [Relationships](#relationships)
- [Events](#events) - [Events](#events)
- [Serialization](#serialization) - [Serialization](#serialization)
- [Commands](#commands)
- [Behaviour Trees](#behaviour-trees)
- [TypeScript Inference](#typescript-inference) - [TypeScript Inference](#typescript-inference)
## Installation ## 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 ### Queries
Create filters with `query()`. Chain `.without()` to exclude components. 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 ## TypeScript Inference
Components infer their type from the defaults object — no separate type declaration needed. Components infer their type from the defaults object — no separate type declaration needed.