docs: add singleton components, commands, and behaviour trees
This commit is contained in:
parent
4182bc1578
commit
ef9abf03c6
152
USAGE.md
152
USAGE.md
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue