@vworlds/vecs 1.0.25 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -778
- package/dist/component.d.ts +1 -1
- package/dist/component.js +1 -1
- package/dist/component.js.map +1 -1
- package/dist/component_meta.d.ts +5 -1
- package/dist/component_meta.js +10 -0
- package/dist/component_meta.js.map +1 -1
- package/dist/dsl.d.ts +2 -2
- package/dist/entity/entity.base.d.ts +2 -18
- package/dist/entity/entity.base.js +2 -20
- package/dist/entity/entity.base.js.map +1 -1
- package/dist/entity/entity.components.d.ts +1 -0
- package/dist/entity/entity.components.js +50 -0
- package/dist/entity/entity.components.js.map +1 -1
- package/dist/entity/entity.lifecycle.d.ts +3 -3
- package/dist/entity/entity.lifecycle.js +6 -9
- package/dist/entity/entity.lifecycle.js.map +1 -1
- package/dist/entity/entity.relationships.js +2 -2
- package/dist/entity/entity.relationships.js.map +1 -1
- package/dist/filter.d.ts +1 -0
- package/dist/filter.js +3 -2
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +3 -2
- package/dist/inject.js.map +1 -1
- package/dist/modules/implements.d.ts +14 -0
- package/dist/modules/implements.js +98 -0
- package/dist/modules/implements.js.map +1 -0
- package/dist/package.json +1 -4
- package/dist/query/callbacks.d.ts +6 -2
- package/dist/query/callbacks.js +5 -2
- package/dist/query/callbacks.js.map +1 -1
- package/dist/query/query.d.ts +14 -1
- package/dist/query/query.js +26 -15
- package/dist/query/query.js.map +1 -1
- package/dist/system.d.ts +5 -4
- package/dist/system.js +17 -6
- package/dist/system.js.map +1 -1
- package/dist/util/array_map.d.ts +70 -12
- package/dist/util/array_map.js +113 -26
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/bitset.js +0 -17
- package/dist/util/bitset.js.map +1 -1
- package/dist/util/events.d.ts +42 -12
- package/dist/util/events.js +94 -43
- package/dist/util/events.js.map +1 -1
- package/dist/util/ordered_set.js +43 -19
- package/dist/util/ordered_set.js.map +1 -1
- package/dist/world/world.deferred.js +2 -0
- package/dist/world/world.deferred.js.map +1 -1
- package/dist/world/world.entities.d.ts +8 -1
- package/dist/world/world.entities.js +25 -6
- package/dist/world/world.entities.js.map +1 -1
- package/dist/world/world.js +8 -1
- package/dist/world/world.js.map +1 -1
- package/dist/world/world.queries.js +6 -1
- package/dist/world/world.queries.js.map +1 -1
- package/dist/world/world.storage.d.ts +2 -2
- package/dist/world/world.storage.js +6 -3
- package/dist/world/world.storage.js.map +1 -1
- package/docs/README.md +50 -0
- package/docs/components.md +267 -0
- package/docs/concepts.md +86 -0
- package/docs/design-guide.md +506 -0
- package/docs/entities.md +175 -0
- package/docs/execution-model.md +173 -0
- package/docs/getting-started.md +215 -0
- package/docs/glossary.md +113 -0
- package/docs/modules.md +108 -0
- package/docs/queries-and-filters.md +187 -0
- package/docs/relationships.md +148 -0
- package/docs/systems.md +311 -0
- package/docs/utilities.md +139 -0
- package/package.json +1 -4
package/docs/entities.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Entities
|
|
2
|
+
|
|
3
|
+
Create, look up, mutate, and destroy entities, and understand how the entity API behaves under
|
|
4
|
+
deferred mutation.
|
|
5
|
+
|
|
6
|
+
## What an entity is
|
|
7
|
+
|
|
8
|
+
An `Entity` (`lib/vecs/src/entity/entity.ts`) is a unique numeric id (`eid`) owned by a `World`,
|
|
9
|
+
with an arbitrary set of component instances attached. Never instantiate `Entity` directly —
|
|
10
|
+
create entities through the world.
|
|
11
|
+
|
|
12
|
+
## Creating and looking up entities
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// New entity with an auto-assigned id (from the "entity" pool, 1000+ by default):
|
|
16
|
+
const e = world.entity();
|
|
17
|
+
|
|
18
|
+
// Look up by id (throws if not found):
|
|
19
|
+
const found = world.entity(42);
|
|
20
|
+
|
|
21
|
+
// Optional lookup — returns undefined instead of throwing:
|
|
22
|
+
const maybe = world.getEntity(42);
|
|
23
|
+
|
|
24
|
+
// Externally assigned id (e.g. from a server); creates the entity if absent:
|
|
25
|
+
const net = world.getOrCreateEntity(serverId, (newEntity) => {
|
|
26
|
+
tracked.add(newEntity);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Named entities, via the built-in Identity component:
|
|
30
|
+
world.entity("player"); // looks up by name, creating the entity if absent
|
|
31
|
+
world.entity("player").name === "player"; // true
|
|
32
|
+
world.getEntity("player"); // lookup only — undefined when absent
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`world.entity(ref)` also accepts a registered component class (returning its component entity) or
|
|
36
|
+
an `Entity` (validating it belongs to this world). Names are maintained by `IdentityModule`:
|
|
37
|
+
setting `entity.name` writes the `Identity` component, and duplicate names are last-writer-wins.
|
|
38
|
+
|
|
39
|
+
`world.entities` is a read-only view of every live entity keyed by id, and `world.clearAllEntities()`
|
|
40
|
+
destroys **every** entity in the world — ordinary entities first, then component entities once
|
|
41
|
+
their carriers are gone. It is a full wipe (systems and phases are backed by entities too); use it
|
|
42
|
+
for end-of-session teardown, not selective resets.
|
|
43
|
+
|
|
44
|
+
## The component API
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
e.add(Position); // attach with default field values (idempotent)
|
|
48
|
+
e.set(Position, { x: 100 }); // attach if needed, assign props, fire onSet/update
|
|
49
|
+
e.get(Position); // Readonly<Position> | undefined
|
|
50
|
+
e.has(Position); // boolean
|
|
51
|
+
e.remove(Position); // detach; fires exit callbacks, then onRemove
|
|
52
|
+
e.modified(Position); // mark changed after in-place mutation
|
|
53
|
+
e.destroy(); // remove everything and unregister the entity
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
All mutators return the entity for chaining (`remove` and `destroy` return `void`). Component
|
|
57
|
+
references can be a registered class, a numeric component id, or an entity used as a component
|
|
58
|
+
key.
|
|
59
|
+
|
|
60
|
+
### `set` vs `add` vs `modified`
|
|
61
|
+
|
|
62
|
+
- `add(C)` attaches with defaults and fires `onAdd` only. It is idempotent and does **not** fire
|
|
63
|
+
`onSet`.
|
|
64
|
+
- `set(C, props)` attaches if needed (firing `onAdd`), copies `props` onto the instance, and fires
|
|
65
|
+
`onSet` plus `update` callbacks.
|
|
66
|
+
- `modified(C)` fires `onSet`/`update` for data you already mutated in place. Repeated calls are
|
|
67
|
+
coalesced until the world routes the notification.
|
|
68
|
+
|
|
69
|
+
### `getMut`: mutating in place
|
|
70
|
+
|
|
71
|
+
`get` returns a `Readonly` view. To mutate, use `getMut`, which marks the component modified for
|
|
72
|
+
you:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Inside a system / forEach / defer block (deferred mode) — returns the mutable instance:
|
|
76
|
+
const vel = e.getMut(Velocity)!;
|
|
77
|
+
vel.vx += 1;
|
|
78
|
+
|
|
79
|
+
// Outside deferred mode you must use the callback form, so the modified
|
|
80
|
+
// notification fires after your mutation:
|
|
81
|
+
e.getMut(Velocity, (vel) => {
|
|
82
|
+
vel.vx += 1;
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Calling `getMut(C)` without a callback outside deferred mode throws. `getMut` also throws for
|
|
87
|
+
relationship components (retarget with `set` instead — see
|
|
88
|
+
[Relationships](./relationships.md#retargeting)) and for `Implements` interface aliases.
|
|
89
|
+
|
|
90
|
+
### `attach`: storing an existing instance
|
|
91
|
+
|
|
92
|
+
`attach` stores the exact object you pass, replacing any previous instance of that component
|
|
93
|
+
class, and fires events as a `set` would:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const shared = new Position();
|
|
97
|
+
e.attach(shared);
|
|
98
|
+
e.get(Position) === shared; // true
|
|
99
|
+
|
|
100
|
+
// Store under a base class key instead of the instance's own constructor:
|
|
101
|
+
e.attach(BaseShape, concreteShape);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The resolved class must already be registered in the entity's world. Use `attach` when component
|
|
105
|
+
ownership is intentionally shared with caller code or another object graph; code receiving the
|
|
106
|
+
component through vecs callbacks should use the entity vecs passes, not assume a 1:1 mapping.
|
|
107
|
+
|
|
108
|
+
### Deferred semantics
|
|
109
|
+
|
|
110
|
+
Inside a system body, a `forEach`, or a `world.defer` block, all of the mutators above enqueue
|
|
111
|
+
commands instead of applying immediately, and reads return the pre-mutation state until the queue
|
|
112
|
+
drains. The exact read-after-write rules are tabulated in
|
|
113
|
+
[Execution model — deferred mutation](./execution-model.md#deferred-mutation-and-flush-boundaries).
|
|
114
|
+
|
|
115
|
+
Deferred entity creation is idempotent within one deferred scope: if `world.entity()` or
|
|
116
|
+
`getOrCreateEntity(eid)` creates a new entity, `getEntity(eid)`, `entity(eid)`, and later
|
|
117
|
+
`getOrCreateEntity(eid)` calls before the drain return that same pending entity. Collection-style
|
|
118
|
+
reads such as `world.entities` and queries still do not see the entity until the queued creation
|
|
119
|
+
drains.
|
|
120
|
+
|
|
121
|
+
## Destroying entities
|
|
122
|
+
|
|
123
|
+
`e.destroy()` removes every component (firing each `onRemove` hook and query `exit`), emits the
|
|
124
|
+
entity's `"destroy"` event just before teardown, unregisters the entity from the world, and
|
|
125
|
+
applies incoming relationship cleanup policies (see
|
|
126
|
+
[Relationships](./relationships.md#cleanup-when-a-target-dies)). After destruction the entity must
|
|
127
|
+
not be used.
|
|
128
|
+
|
|
129
|
+
If the entity is itself in use as a component key, `destroy` honors `meta.onDelete` — by default
|
|
130
|
+
it throws while carriers remain ([Components — cleanup policies](./components.md#cleanup-policies-destroying-a-component-entity)).
|
|
131
|
+
|
|
132
|
+
## Entities as component keys
|
|
133
|
+
|
|
134
|
+
Any entity can serve as a component key: `other.add(keyEntity)` attaches a marker component whose
|
|
135
|
+
type id is the key entity's eid. The key entity's `meta` (created lazily) carries the same hook
|
|
136
|
+
and policy surface as a registered class, and `keyEntity.onAdd/onSet/onRemove(...)` register hooks
|
|
137
|
+
directly. This is how tag-like, runtime-created component types work without declaring a class.
|
|
138
|
+
|
|
139
|
+
## Relationships, in brief
|
|
140
|
+
|
|
141
|
+
`e.parent(Rel)` returns the target of a relationship component, `e.children(Rel)` the set of
|
|
142
|
+
entities targeting `e` through it, and `e.childOf(parent)` is shorthand for
|
|
143
|
+
`e.set(ChildOf, { target: parent })`. The full model — retargeting, cleanup cascades, traversal —
|
|
144
|
+
is in [Relationships](./relationships.md).
|
|
145
|
+
|
|
146
|
+
## Reference
|
|
147
|
+
|
|
148
|
+
`lib/vecs/src/entity/` (split across `entity.base.ts`, `entity.components.ts`,
|
|
149
|
+
`entity.lifecycle.ts`, `entity.relationships.ts`):
|
|
150
|
+
|
|
151
|
+
| Property / Method | Description |
|
|
152
|
+
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
153
|
+
| `eid` | Unique numeric entity id. |
|
|
154
|
+
| `world` | The `World` that owns this entity. |
|
|
155
|
+
| `name` | Identity name (getter/setter backed by the `Identity` component); `undefined` when unnamed. |
|
|
156
|
+
| `componentBitmask` | `Bitset` of component type ids attached. Used for archetype matching. |
|
|
157
|
+
| `components` | Read-only `ArrayMap` view of attached instances keyed by type id (`forEach`, `get`, `has`, `size`). |
|
|
158
|
+
| `empty` | `true` when no components are attached. |
|
|
159
|
+
| `properties` | Free-form `Map<string, any>` for module-level bookkeeping. |
|
|
160
|
+
| `meta` | `ComponentMeta` for using this entity as a component key (created lazily). |
|
|
161
|
+
| `instanceCount()` | Number of entities currently carrying this entity's eid as a component. |
|
|
162
|
+
| `add(ref)` | Attach a component with default values (idempotent). Returns `this`. |
|
|
163
|
+
| `attach(instance)` / `attach(Class, instance)` | Store an existing instance, replacing any previous one; fires set-side events. Returns `this`. |
|
|
164
|
+
| `set(ref, props)` | Attach if needed and assign `props`; fires `onSet` and `update` callbacks. Returns `this`. |
|
|
165
|
+
| `get(ref)` | Read-only component lookup, or `undefined`. |
|
|
166
|
+
| `getMut(ref)` / `getMut(ref, callback)` | Mutable lookup that marks the component modified. Callback form required outside deferred mode. |
|
|
167
|
+
| `has(ref)` | `true` when the component is attached. |
|
|
168
|
+
| `modified(ref)` | Queue an `onSet`/`update` notification; coalesced while pending. Returns `this`. |
|
|
169
|
+
| `remove(ref)` | Detach a component; routes `exit`, then fires `onRemove`. |
|
|
170
|
+
| `destroy()` | Remove all components, unregister, and apply incoming relationship cleanup. |
|
|
171
|
+
| `parent(rel)` | Target entity of relationship `rel`, or `undefined`. |
|
|
172
|
+
| `children(rel)` | `ReadonlySet<Entity>` of entities targeting this one through `rel`. |
|
|
173
|
+
| `childOf(parent)` | Shorthand for `set(ChildOf, { target: parent })`. Returns `this`. |
|
|
174
|
+
| `onAdd/onSet/onRemove(handler)` | Register hooks for this entity used as a component key. Return `this`. |
|
|
175
|
+
| `toString()` | Returns `"EntityN"`. |
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Execution Model
|
|
2
|
+
|
|
3
|
+
How `world.progress` runs your systems: the phase spine, ordering guarantees, deferred mutation,
|
|
4
|
+
and reactive event routing. Every design trick in vecs follows from the rules on this page.
|
|
5
|
+
|
|
6
|
+
## One tick, anatomically
|
|
7
|
+
|
|
8
|
+
`world.progress(now, delta)` (`lib/vecs/src/world/world.pipeline.ts`) does, in order:
|
|
9
|
+
|
|
10
|
+
1. **`beginFrame(delta)`** — flush any pending top-level commands, rebuild the flat pipeline if
|
|
11
|
+
the pipeline query changed (this is when newly created systems are inserted), and evaluate
|
|
12
|
+
every registered tick source once (see [Systems — cadence](./systems.md#cadence-interval-rate-and-tick-sources)).
|
|
13
|
+
2. **For each system in pipeline order:** run the system, then `flush()` the world's command
|
|
14
|
+
queue.
|
|
15
|
+
3. **`endFrame()`** — close the frame.
|
|
16
|
+
|
|
17
|
+
`now` and `delta` are both milliseconds. You can also call `beginFrame` / `endFrame` yourself and
|
|
18
|
+
run systems manually, but `progress` is the normal driver.
|
|
19
|
+
|
|
20
|
+
## Phases: the pipeline spine
|
|
21
|
+
|
|
22
|
+
Systems are grouped by **phase**, and phases run in a fixed order. The eight built-in phases are
|
|
23
|
+
plain entities, created by `PipelineModule` and chained with the `DependsOn` relationship
|
|
24
|
+
(`lib/vecs/src/modules/pipeline.ts`). The exported constants are their entity names:
|
|
25
|
+
|
|
26
|
+
| Constant | Entity name | Use it for |
|
|
27
|
+
| ------------- | ------------------ | ----------------------------------------------------------------------------------- |
|
|
28
|
+
| `ON_LOAD` | `vecs::OnLoad` | Load data _into_ the ECS: inputs, network snapshots, external reads. |
|
|
29
|
+
| `POST_LOAD` | `vecs::PostLoad` | Process raw loaded data: turn key presses into high-level actions. |
|
|
30
|
+
| `PRE_UPDATE` | `vecs::PreUpdate` | Final prep before game logic: clean up last frame, prepare state gameplay will use. |
|
|
31
|
+
| `ON_UPDATE` | `vecs::OnUpdate` | **The default phase.** Main gameplay/simulation logic. |
|
|
32
|
+
| `ON_VALIDATE` | `vecs::OnValidate` | Validate state after updates: collision detection, constraint checks. |
|
|
33
|
+
| `POST_UPDATE` | `vecs::PostUpdate` | Apply corrections from validation: resolve collisions, fix up state. |
|
|
34
|
+
| `PRE_STORE` | `vecs::PreStore` | Prepare data for output: compute render state once all logic is done. |
|
|
35
|
+
| `ON_STORE` | `vecs::OnStore` | Store/emit the final frame: rendering, writing output, sending snapshots. |
|
|
36
|
+
|
|
37
|
+
`BUILTIN_PIPELINE_PHASES` exports the eight names in order. Map your work onto this taxonomy
|
|
38
|
+
rather than inventing phases: input belongs in `ON_LOAD`/`POST_LOAD`, gameplay in `ON_UPDATE`,
|
|
39
|
+
validation and correction in `ON_VALIDATE`/`POST_UPDATE`, output in `PRE_STORE`/`ON_STORE`.
|
|
40
|
+
|
|
41
|
+
### Assigning a system to a phase
|
|
42
|
+
|
|
43
|
+
Every system defaults to `ON_UPDATE`. Call `.phase(...)` only to run elsewhere:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { ON_STORE } from "@vworlds/vecs";
|
|
47
|
+
|
|
48
|
+
world.system("Render").phase(ON_STORE).with(Sprite, Position).each([Sprite, Position], draw);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`.phase(anchor)` accepts a phase name or an entity. It sets `DependsOn` on the system's backing
|
|
52
|
+
entity (replacing any previous target) and tags the anchor with `Phase`, creating the anchor
|
|
53
|
+
entity if the name is new. **A system lives in exactly one phase.** If a concern needs work in two
|
|
54
|
+
phases, split it into two systems.
|
|
55
|
+
|
|
56
|
+
### Ordering guarantees
|
|
57
|
+
|
|
58
|
+
1. **Across phases, order is guaranteed by the phase spine.** A system in `PRE_STORE` always runs
|
|
59
|
+
before any system in `ON_STORE`, regardless of when either was created.
|
|
60
|
+
2. **Within a phase, systems run in creation order** (entity-id order — the order `world.system`
|
|
61
|
+
was called). An umbrella module controls this by the order it loads sub-modules.
|
|
62
|
+
3. **Never rely on registration order for cross-module correctness.** When two independent
|
|
63
|
+
features must order against each other, express it with phases.
|
|
64
|
+
|
|
65
|
+
### The pipeline is a query
|
|
66
|
+
|
|
67
|
+
The pipeline itself is an ordered query over system entities. The default, installed by
|
|
68
|
+
`PipelineModule`:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
world.setPipeline({
|
|
72
|
+
with: { all: [System, { target: [DependsOn, Phase] }] },
|
|
73
|
+
cascade: DependsOn,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It matches every `System` whose backing entity `DependsOn` a `Phase`, ordered depth-first along
|
|
78
|
+
the `DependsOn` cascade. `world.setPipeline(spec)` replaces it; the world always appends `System`
|
|
79
|
+
to the spec so the result flattens to systems.
|
|
80
|
+
|
|
81
|
+
### Custom phases: `insertPhaseAfter`
|
|
82
|
+
|
|
83
|
+
`insertPhaseAfter(phase, anchor)` splices an existing `Phase` entity into the spine directly after
|
|
84
|
+
`anchor`, retargeting the anchor's downstream phases onto the new one:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { insertPhaseAfter, ON_UPDATE, Phase } from "@vworlds/vecs";
|
|
88
|
+
|
|
89
|
+
const physicsStep = world.entity("physics-step").add(Phase);
|
|
90
|
+
insertPhaseAfter(physicsStep, world.entity(ON_UPDATE));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Reserve custom phases for engine-level modules that genuinely need their own spine (the physics
|
|
94
|
+
engine inserts its step phases after `ON_UPDATE`). Application and render systems should use the
|
|
95
|
+
built-in taxonomy.
|
|
96
|
+
|
|
97
|
+
## Deferred mutation and flush boundaries
|
|
98
|
+
|
|
99
|
+
Inside a system body, a `Query.forEach` / `Filter.forEach` iteration, or any `world.defer(...)`
|
|
100
|
+
block, the world is in **deferred mode**: entity mutations (`add` / `attach` / `set` / `remove` /
|
|
101
|
+
`destroy` / `modified`, and `world.entity()` creation) are queued as commands instead of applied
|
|
102
|
+
inline. The queue drains when the scope that opened deferral ends.
|
|
103
|
+
|
|
104
|
+
For the pipeline this means: **a structural change made by system A is applied in the flush
|
|
105
|
+
immediately after A finishes, so it is visible to every later system in the same tick** — later in
|
|
106
|
+
the same phase or in a later phase — but not to A itself mid-iteration. This single fact enables
|
|
107
|
+
teardown-before-create ordering, the companion-component lifecycle, and same-tick reconciliation
|
|
108
|
+
(see the [Design guide](./design-guide.md)).
|
|
109
|
+
|
|
110
|
+
### What reads see while deferred
|
|
111
|
+
|
|
112
|
+
While a mutation is queued but not yet applied, reads return the pre-mutation state:
|
|
113
|
+
|
|
114
|
+
| After calling… | `entity.get(C)` / `entity.getMut(C)` returns… |
|
|
115
|
+
| ------------------------- | ---------------------------------------------------------- |
|
|
116
|
+
| `entity.add(C)` | `undefined` — no instance has been created yet. |
|
|
117
|
+
| `entity.attach(instance)` | `undefined`, if `C` was absent before. |
|
|
118
|
+
| `entity.set(C, props)` | The **previous** value (or `undefined` if `C` was absent). |
|
|
119
|
+
| `entity.remove(C)` | Still the component instance. |
|
|
120
|
+
|
|
121
|
+
Outside any deferred scope (top-level user code), the same calls execute inline and effects are
|
|
122
|
+
visible immediately.
|
|
123
|
+
|
|
124
|
+
### Controlling deferral yourself
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
world.deferred; // true while the world is in deferred mode
|
|
128
|
+
world.defer(() => { ... }); // run a block in deferred mode, drain on exit
|
|
129
|
+
world.beginDefer(); // open a scope manually…
|
|
130
|
+
world.endDefer(); // …close it (drains at the outermost scope)
|
|
131
|
+
world.flush(); // drain queued top-level commands now
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Scopes nest: a `Filter.forEach` inside a system inherits the system's scope and does not drain on
|
|
135
|
+
exit. `flush()` is a no-op while any scope is open; it also builds queries created since the last
|
|
136
|
+
flush.
|
|
137
|
+
|
|
138
|
+
## Reactive routing
|
|
139
|
+
|
|
140
|
+
When the world applies a command, it routes events to everything that watches the affected
|
|
141
|
+
component:
|
|
142
|
+
|
|
143
|
+
- **Hooks** (`onAdd` / `onSet` / `onRemove`) fire synchronously at the moment the command is
|
|
144
|
+
applied — inline outside deferred mode, or during the drain.
|
|
145
|
+
- **Standalone `Query` callbacks** (`enter` / `update` / `exit`) also fire synchronously during
|
|
146
|
+
routing.
|
|
147
|
+
- **Systems** queue events into an ordered **inbox** instead, and replay the inbox at the top of
|
|
148
|
+
their next run (`lib/vecs/src/system.ts`). So systems react to change without scanning, and they
|
|
149
|
+
react _in their own phase slot_, in event arrival order.
|
|
150
|
+
|
|
151
|
+
Three routing rules worth memorizing:
|
|
152
|
+
|
|
153
|
+
- **`update(C)` fires on entry.** When an entity enters a system, the system pushes an `update`
|
|
154
|
+
event for every watched component already present. A single `.update(C, ...)` therefore
|
|
155
|
+
initializes on entry _and_ reacts to changes — don't write a duplicate `enter`.
|
|
156
|
+
- **`exit` reads a snapshot.** Components injected into an `exit` callback are captured when the
|
|
157
|
+
exit is routed, so they are still resolvable even though they are being (or have been) removed.
|
|
158
|
+
- **In-place mutation needs `modified`.** Assigning to component fields directly notifies no one.
|
|
159
|
+
Call `entity.modified(C)` (or use `set` / `getMut`) to route the change.
|
|
160
|
+
|
|
161
|
+
## Why the patterns work
|
|
162
|
+
|
|
163
|
+
Putting the rules together:
|
|
164
|
+
|
|
165
|
+
- _Teardown before create:_ an `exit`-driven teardown system in `PRE_STORE` runs before an
|
|
166
|
+
`enter`-driven creation system in `ON_STORE`; the teardown's `remove` is flushed between them,
|
|
167
|
+
so a type swap tears down the old companion and builds the new one in the same tick.
|
|
168
|
+
- _React, don't scan:_ `update(C)` touches nothing on idle entities, where `each` would visit all
|
|
169
|
+
of them every tick.
|
|
170
|
+
- _Same-tick reconciliation:_ a system that writes a component early in the tick is guaranteed
|
|
171
|
+
that later phases see the new value.
|
|
172
|
+
|
|
173
|
+
The [Design guide](./design-guide.md) develops each of these into a full pattern.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
Build a small simulation from scratch: create a world, register components, write two systems, and
|
|
4
|
+
drive the loop with `world.progress`.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @vworlds/vecs
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package ships TypeScript types; no extra tooling is required.
|
|
13
|
+
|
|
14
|
+
## 1. Create a world
|
|
15
|
+
|
|
16
|
+
A `World` owns every entity, registered component, query, system, and the update pipeline. Create
|
|
17
|
+
one per game session:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { World } from "@vworlds/vecs";
|
|
21
|
+
|
|
22
|
+
const world = new World();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The constructor installs the built-in modules — identity names, interface aliases, singletons,
|
|
26
|
+
relationships, and the eight-phase pipeline — so the world is ready to use immediately (see
|
|
27
|
+
[Modules](./modules.md)).
|
|
28
|
+
|
|
29
|
+
## 2. Define and register components
|
|
30
|
+
|
|
31
|
+
Components are plain data classes: field initializers, a no-argument constructor, and no behavior.
|
|
32
|
+
Register each class with `world.component` before using it:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
class Position {
|
|
36
|
+
x = 0;
|
|
37
|
+
y = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Velocity {
|
|
41
|
+
vx = 0;
|
|
42
|
+
vy = 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class Health {
|
|
46
|
+
hp = 100;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
world.component(Position);
|
|
50
|
+
world.component(Velocity);
|
|
51
|
+
world.component(Health);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
There is no automatic registration — using an unregistered class as a component throws. See
|
|
55
|
+
[Components](./components.md) for ids, hooks, and policies.
|
|
56
|
+
|
|
57
|
+
## 3. Add a system
|
|
58
|
+
|
|
59
|
+
Systems declare which entities they care about with `.with(...)` and attach behavior. `each` runs
|
|
60
|
+
once per matched entity on every tick:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
world
|
|
64
|
+
.system("Move")
|
|
65
|
+
.with(Position, Velocity)
|
|
66
|
+
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
67
|
+
pos.x += vel.vx;
|
|
68
|
+
pos.y += vel.vy;
|
|
69
|
+
e.modified(Position); // notify watchers that Position changed
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Two details to internalize early:
|
|
74
|
+
|
|
75
|
+
- The components listed in `.with` are guaranteed present, so `pos` and `vel` are non-nullable in
|
|
76
|
+
the callback tuple.
|
|
77
|
+
- Mutating fields directly does not notify anyone. Call `e.modified(Position)` so hooks, `update`
|
|
78
|
+
callbacks, and reactive queries see the change. See [Systems](./systems.md).
|
|
79
|
+
|
|
80
|
+
## 4. Add a reactive system in a later phase
|
|
81
|
+
|
|
82
|
+
Systems run in the world's phase pipeline and default to the `vecs::OnUpdate` phase. Place
|
|
83
|
+
follow-up work in a later phase — here, despawning dead entities during validation:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { ON_VALIDATE } from "@vworlds/vecs";
|
|
87
|
+
|
|
88
|
+
world
|
|
89
|
+
.system("Reap")
|
|
90
|
+
.phase(ON_VALIDATE)
|
|
91
|
+
.with(Health)
|
|
92
|
+
.update(Health, (entity, health) => {
|
|
93
|
+
if (health.hp <= 0) {
|
|
94
|
+
entity.destroy();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`update(Health, ...)` fires only when `Health` is set or marked modified on a tracked entity — and
|
|
100
|
+
once on entry for each watched component already present — so this system does no work for idle
|
|
101
|
+
entities. The phase taxonomy and ordering rules are in
|
|
102
|
+
[Execution model](./execution-model.md).
|
|
103
|
+
|
|
104
|
+
## 5. Observe lifecycle with hooks
|
|
105
|
+
|
|
106
|
+
For a per-component callback that is not worth a whole system, register hooks on the component
|
|
107
|
+
entity:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
world
|
|
111
|
+
.component(Health)
|
|
112
|
+
.onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
|
|
113
|
+
.onRemove((entity) => console.log(`entity ${entity.eid} died`));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`onAdd` fires when the component is first attached, `onRemove` when it is detached or its entity is
|
|
117
|
+
destroyed, and `onSet` whenever its data is set or marked modified.
|
|
118
|
+
|
|
119
|
+
## 6. Spawn entities
|
|
120
|
+
|
|
121
|
+
`world.entity()` creates an entity; `set` attaches a component and assigns data in one call.
|
|
122
|
+
Calls chain:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 7. Drive the loop
|
|
129
|
+
|
|
130
|
+
`world.progress(now, delta)` runs one tick: every system in phase order, with structural changes
|
|
131
|
+
flushed after each system. Both arguments are milliseconds — `now` is your absolute clock, `delta`
|
|
132
|
+
the time since the previous tick:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
let now = 0;
|
|
136
|
+
for (let tick = 0; tick < 5; tick++) {
|
|
137
|
+
now += 16;
|
|
138
|
+
world.progress(now, 16);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In a real game, call `world.progress` from your frame loop (`requestAnimationFrame`, a fixed-step
|
|
143
|
+
server loop, etc.).
|
|
144
|
+
|
|
145
|
+
## Complete program
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { ON_VALIDATE, World } from "@vworlds/vecs";
|
|
149
|
+
|
|
150
|
+
class Position {
|
|
151
|
+
x = 0;
|
|
152
|
+
y = 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class Velocity {
|
|
156
|
+
vx = 0;
|
|
157
|
+
vy = 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class Health {
|
|
161
|
+
hp = 100;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const world = new World();
|
|
165
|
+
|
|
166
|
+
world.component(Position);
|
|
167
|
+
world.component(Velocity);
|
|
168
|
+
world
|
|
169
|
+
.component(Health)
|
|
170
|
+
.onAdd((entity, h) => console.log(`entity ${entity.eid} spawned with hp=${h.hp}`))
|
|
171
|
+
.onRemove((entity) => console.log(`entity ${entity.eid} died`));
|
|
172
|
+
|
|
173
|
+
world
|
|
174
|
+
.system("Move")
|
|
175
|
+
.with(Position, Velocity)
|
|
176
|
+
.each([Position, Velocity], (e, [pos, vel]) => {
|
|
177
|
+
pos.x += vel.vx;
|
|
178
|
+
pos.y += vel.vy;
|
|
179
|
+
e.modified(Position);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
world
|
|
183
|
+
.system("Drain")
|
|
184
|
+
.with(Health)
|
|
185
|
+
.each([Health], (e, [health]) => {
|
|
186
|
+
health.hp -= 1;
|
|
187
|
+
e.modified(Health);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
world
|
|
191
|
+
.system("Reap")
|
|
192
|
+
.phase(ON_VALIDATE)
|
|
193
|
+
.with(Health)
|
|
194
|
+
.update(Health, (entity, health) => {
|
|
195
|
+
if (health.hp <= 0) {
|
|
196
|
+
entity.destroy();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
world.entity().set(Position, { x: 0, y: 0 }).set(Velocity, { vx: 5, vy: 0 }).set(Health, { hp: 3 });
|
|
201
|
+
|
|
202
|
+
let now = 0;
|
|
203
|
+
for (let tick = 0; tick < 5; tick++) {
|
|
204
|
+
now += 16;
|
|
205
|
+
world.progress(now, 16);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Run it and watch the entity spawn, drain, and die after three ticks.
|
|
210
|
+
|
|
211
|
+
## Where to go next
|
|
212
|
+
|
|
213
|
+
- [Concepts](./concepts.md) — the mental model behind what you just wrote.
|
|
214
|
+
- [Execution model](./execution-model.md) — why the phase placement and `modified` calls work.
|
|
215
|
+
- [Design guide](./design-guide.md) — the idioms to reach for as your design grows.
|
package/docs/glossary.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
ECS and vecs terminology, in alphabetical order. Each entry links to the guide that develops it.
|
|
4
|
+
|
|
5
|
+
**Archetype** — the set of component type ids attached to an entity, represented as the entity's
|
|
6
|
+
`componentBitmask` (a [`Bitset`](./utilities.md#bitset)). Query matching tests
|
|
7
|
+
archetypes with fast bitwise subset checks.
|
|
8
|
+
|
|
9
|
+
**Backfill** — when a tracked query or system is built, entities that already match are routed
|
|
10
|
+
through `enter` so the tracked set starts complete.
|
|
11
|
+
[Queries and filters](./queries-and-filters.md#lifecycle-build-backfill-destroy).
|
|
12
|
+
|
|
13
|
+
**Bitset** — a compact, growable set of non-negative integers backed by 32-bit words; used for
|
|
14
|
+
archetypes and watchlists, and exported for bit-flag component fields.
|
|
15
|
+
[Utilities](./utilities.md#bitset).
|
|
16
|
+
|
|
17
|
+
**Cascade (query)** — depth-first ordering of a query's members along a `Traversable`
|
|
18
|
+
relationship, via `{ with: ..., cascade: Rel }`.
|
|
19
|
+
[Relationships](./relationships.md#traversal-order-traversable-and-cascade).
|
|
20
|
+
|
|
21
|
+
**Cleanup policy** — what happens to dependent entities on destruction: `onDelete` (component
|
|
22
|
+
entity destroyed → carriers throw/strip/die) and `onDeleteTarget` (relationship target destroyed →
|
|
23
|
+
sources strip/die). [Components](./components.md#cleanup-policies-destroying-a-component-entity),
|
|
24
|
+
[Relationships](./relationships.md#cleanup-when-a-target-dies).
|
|
25
|
+
|
|
26
|
+
**Companion (wrapper) component** — a local, non-networked component holding a non-ECS resource
|
|
27
|
+
(render object, WASM handle) so the entity's component set drives the resource lifecycle.
|
|
28
|
+
[Components](./components.md#companion-components-for-non-ecs-resources).
|
|
29
|
+
|
|
30
|
+
**Component** — a registered plain data class with a no-argument constructor; pure data, no
|
|
31
|
+
behavior. [Components](./components.md).
|
|
32
|
+
|
|
33
|
+
**Component entity** — the entity backing a registered component class; returned by
|
|
34
|
+
`world.component(C)` and carrying the class's hooks and `ComponentMeta`.
|
|
35
|
+
[Components](./components.md#registration-worldcomponent).
|
|
36
|
+
|
|
37
|
+
**Deferred mode / flush** — inside systems, `forEach`, and `defer` blocks, entity mutations are
|
|
38
|
+
queued as commands; `flush` drains the queue at the scope boundary (after each system in the
|
|
39
|
+
pipeline). [Execution model](./execution-model.md#deferred-mutation-and-flush-boundaries).
|
|
40
|
+
|
|
41
|
+
**Entity** — a unique numeric id (`eid`) owned by a world, with a set of components attached.
|
|
42
|
+
[Entities](./entities.md).
|
|
43
|
+
|
|
44
|
+
**Exclusive components** — a declared group where at most one member may exist on an entity;
|
|
45
|
+
setting one removes the others. [Components](./components.md#exclusive-component-groups).
|
|
46
|
+
|
|
47
|
+
**Events** — a minimal typed event emitter helper exported for package-level events. It is not an
|
|
48
|
+
entity lifecycle API. [Utilities](./utilities.md#events).
|
|
49
|
+
|
|
50
|
+
**Filter** — a non-reactive, one-shot predicate that scans world entities on each `forEach`.
|
|
51
|
+
[Queries and filters](./queries-and-filters.md#filters-one-shot-scans).
|
|
52
|
+
|
|
53
|
+
**Hook** — an `onAdd` / `onSet` / `onRemove` callback registered per component class, fired
|
|
54
|
+
synchronously at the mutation site. [Components](./components.md#hooks-onadd-onset-onremove).
|
|
55
|
+
|
|
56
|
+
**Implements / interface alias** — a declaration that a concrete component is queryable through
|
|
57
|
+
abstract/interface component types; aliases are read-only views of the same instance.
|
|
58
|
+
[Components](./components.md#interface-aliases-with-implements).
|
|
59
|
+
|
|
60
|
+
**Inbox** — a system's ordered queue of routed `enter` / `exit` / `update` events, replayed at the
|
|
61
|
+
top of the system's run. [Execution model](./execution-model.md#reactive-routing).
|
|
62
|
+
|
|
63
|
+
**Injection** — resolving component instances per entity into a typed callback tuple, including
|
|
64
|
+
`target` (relationship target's components) and `down` (fan-out over children) markers.
|
|
65
|
+
[Systems](./systems.md#component-injection).
|
|
66
|
+
|
|
67
|
+
**Iter** — an opt-in reusable cursor passed to callbacks instead of the bare entity, exposing the
|
|
68
|
+
visited entity and the source entity of each injected slot.
|
|
69
|
+
[Systems](./systems.md#the-iter-cursor).
|
|
70
|
+
|
|
71
|
+
**Module** — a packaging unit (`class M extends Module<Config>`) whose `init()` registers
|
|
72
|
+
components, systems, and policy once per world. [Modules](./modules.md).
|
|
73
|
+
|
|
74
|
+
**Phase** — an entity used as a pipeline ordering anchor (tagged with the `Phase` component);
|
|
75
|
+
systems attach to exactly one phase via `DependsOn`.
|
|
76
|
+
[Execution model](./execution-model.md#phases-the-pipeline-spine).
|
|
77
|
+
|
|
78
|
+
**Pipeline** — the flat, ordered list of systems `world.progress` runs each tick; defined by an
|
|
79
|
+
ordered query over system entities. [Execution model](./execution-model.md#the-pipeline-is-a-query).
|
|
80
|
+
|
|
81
|
+
**Query** — a reactive, always-up-to-date set of entities matching a DSL predicate, with
|
|
82
|
+
`enter` / `exit` / `update` callbacks. [Queries and filters](./queries-and-filters.md).
|
|
83
|
+
|
|
84
|
+
**Query DSL** — the composable predicate language (`all` / `any` / `not` / `only` / `target` /
|
|
85
|
+
`source` / `parent` / `children` / `test`).
|
|
86
|
+
[Queries and filters](./queries-and-filters.md#the-query-dsl).
|
|
87
|
+
|
|
88
|
+
**Relationship** — a component with a `target: Entity` field linking its source entity to a target,
|
|
89
|
+
with reverse lookup (`children`) and cleanup cascades. [Relationships](./relationships.md).
|
|
90
|
+
|
|
91
|
+
**Silence domain / `ignoreSource`** — a query configuration that drops `update` events originating
|
|
92
|
+
from systems matching another query, breaking write feedback loops.
|
|
93
|
+
[Queries and filters](./queries-and-filters.md#silencing-feedback-ignoresource).
|
|
94
|
+
|
|
95
|
+
**Singleton** — a component class constrained to at most one carrier entity; commonly stored on
|
|
96
|
+
its own component entity via `world.set` / `world.get`.
|
|
97
|
+
[Components](./components.md#singletons-and-world-global-data).
|
|
98
|
+
|
|
99
|
+
**System** — a query plus per-tick behavior (`run`, `each`) and inbox-replayed reactive callbacks,
|
|
100
|
+
assigned to one phase. [Systems](./systems.md).
|
|
101
|
+
|
|
102
|
+
**Tick source** — a clock (`IntervalTickSource`, `RateTickSource`, or another system) that gates
|
|
103
|
+
how often a system fires. [Systems](./systems.md#cadence-interval-rate-and-tick-sources).
|
|
104
|
+
|
|
105
|
+
**Watchlist** — the set of component types a query or system reacts to via `update`, stored as a
|
|
106
|
+
bitmask. [Systems](./systems.md#reactive-callbacks-enter-update-exit).
|
|
107
|
+
|
|
108
|
+
**Wire type** — a field-level encoding declaration (`@type` from `@vworlds/vecs-wire`, commonly
|
|
109
|
+
imported as `wireType`) used by the networking packages to serialize components. Networking is
|
|
110
|
+
documented with `@vworlds/vecs-wire` / `@vworlds/vecs-protocol`, not here.
|
|
111
|
+
|
|
112
|
+
**World** — the central container owning entities, component registrations, queries, systems, and
|
|
113
|
+
the pipeline; driven by `world.progress(now, delta)`. [Concepts](./concepts.md).
|