@vworlds/vecs 1.0.10 → 1.0.12
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/.husky/pre-commit +1 -0
- package/README.md +299 -228
- package/dist/command.d.ts +1 -46
- package/dist/component.d.ts +51 -59
- package/dist/component.js +31 -25
- package/dist/component.js.map +1 -1
- package/dist/dsl.d.ts +34 -26
- package/dist/dsl.js +46 -20
- package/dist/dsl.js.map +1 -1
- package/dist/entity.d.ts +96 -106
- package/dist/entity.js +261 -190
- package/dist/entity.js.map +1 -1
- package/dist/filter.d.ts +31 -23
- package/dist/filter.js +24 -17
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/package.json +3 -1
- package/dist/phase.d.ts +12 -30
- package/dist/phase.js +11 -10
- package/dist/phase.js.map +1 -1
- package/dist/query.d.ts +107 -144
- package/dist/query.js +200 -169
- package/dist/query.js.map +1 -1
- package/dist/system.d.ts +170 -86
- package/dist/system.js +253 -114
- package/dist/system.js.map +1 -1
- package/dist/timer.d.ts +50 -0
- package/dist/timer.js +154 -0
- package/dist/timer.js.map +1 -0
- package/dist/util/array_map.d.ts +4 -55
- package/dist/util/array_map.js +35 -37
- package/dist/util/array_map.js.map +1 -1
- package/dist/util/bitset.d.ts +40 -50
- package/dist/util/bitset.js +76 -62
- package/dist/util/bitset.js.map +1 -1
- package/dist/util/events.d.ts +14 -18
- package/dist/util/events.js +24 -3
- package/dist/util/events.js.map +1 -1
- package/dist/util/ordered_set.d.ts +1 -17
- package/dist/util/ordered_set.js +74 -25
- package/dist/util/ordered_set.js.map +1 -1
- package/dist/world.d.ts +230 -218
- package/dist/world.js +422 -327
- package/dist/world.js.map +1 -1
- package/eslint-rules/internal-underscore.js +60 -0
- package/eslint.config.js +5 -0
- package/package.json +3 -1
package/dist/world.js
CHANGED
|
@@ -5,20 +5,27 @@ import { System } from "./system.js";
|
|
|
5
5
|
import { Filter } from "./filter.js";
|
|
6
6
|
import { ArrayMap } from "./util/array_map.js";
|
|
7
7
|
import { Phase } from "./phase.js";
|
|
8
|
+
import { ALWAYS_TICK_SOURCE } from "./timer.js";
|
|
9
|
+
/**
|
|
10
|
+
* Numeric type ids below this value are reserved for components whose id was
|
|
11
|
+
* pre-registered via {@link World.registerComponentType} (typically server
|
|
12
|
+
* assigned). Auto-assigned ids start here.
|
|
13
|
+
*/
|
|
8
14
|
const LOCAL_COMPONENT_MIN = 256;
|
|
9
15
|
/**
|
|
10
|
-
* The central ECS container.
|
|
16
|
+
* The central ECS container. One world per game session.
|
|
11
17
|
*
|
|
12
|
-
* A `World` owns
|
|
13
|
-
* pipeline.
|
|
18
|
+
* A `World` owns every entity, every registered component class, every
|
|
19
|
+
* registered query / system, and the update pipeline. The typical lifecycle:
|
|
14
20
|
*
|
|
15
|
-
* 1. **Register components** —
|
|
16
|
-
* {@link registerComponentType}) for every component class.
|
|
17
|
-
* 2. **
|
|
18
|
-
*
|
|
21
|
+
* 1. **Register components** — {@link registerComponent} (and optionally
|
|
22
|
+
* {@link registerComponentType}) for every component class you plan to use.
|
|
23
|
+
* 2. **Build the pipeline** — {@link addPhase} for every named phase, then
|
|
24
|
+
* {@link system} / {@link query} for each processor.
|
|
19
25
|
* 3. **Start** — call {@link start} to freeze component registration and
|
|
20
26
|
* distribute systems into their phases.
|
|
21
|
-
* 4. **Run loop** — call {@link runPhase}
|
|
27
|
+
* 4. **Run loop** — call {@link runPhase} per phase or {@link progress} for
|
|
28
|
+
* every phase, once per frame.
|
|
22
29
|
*
|
|
23
30
|
* ```ts
|
|
24
31
|
* const world = new World();
|
|
@@ -28,161 +35,201 @@ const LOCAL_COMPONENT_MIN = 256;
|
|
|
28
35
|
*
|
|
29
36
|
* world.system("Move")
|
|
30
37
|
* .requires(Position, Velocity)
|
|
31
|
-
* .
|
|
38
|
+
* .each([Position, Velocity], (e, [pos, vel]) => {
|
|
39
|
+
* pos.x += vel.vx;
|
|
40
|
+
* });
|
|
32
41
|
*
|
|
33
42
|
* world.start();
|
|
34
43
|
*
|
|
35
44
|
* // game loop:
|
|
36
|
-
* world.
|
|
45
|
+
* world.progress(now, delta);
|
|
37
46
|
* ```
|
|
47
|
+
*
|
|
48
|
+
* ## Deferred mode
|
|
49
|
+
*
|
|
50
|
+
* The world can be in **deferred mode**, in which case entity mutations
|
|
51
|
+
* (`add` / `set` / `remove` / `destroy` / `setParent` / `modified`) are
|
|
52
|
+
* queued instead of applied inline. Systems run inside an automatically
|
|
53
|
+
* deferred scope; user code can wrap arbitrary blocks with
|
|
54
|
+
* {@link beginDefer} / {@link endDefer} or {@link defer}. {@link flush}
|
|
55
|
+
* drains the queue at top level.
|
|
38
56
|
*/
|
|
39
57
|
export class World {
|
|
40
|
-
/** `true` when the world is in deferred mode — mutations are queued rather than applied immediately. */
|
|
41
|
-
get deferred() {
|
|
42
|
-
return this.deferredDepth > 0 || this.draining;
|
|
43
|
-
}
|
|
44
58
|
constructor() {
|
|
45
|
-
|
|
46
|
-
this.
|
|
59
|
+
/** @internal Entity id → entity. Owns every live entity. */
|
|
60
|
+
this._entities = new Map();
|
|
61
|
+
/** @internal All registered queries, including systems (which extend `Query`). */
|
|
47
62
|
this._queries = [];
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
this.
|
|
63
|
+
/** @internal Component class → meta record. */
|
|
64
|
+
this._Class2Meta = new Map();
|
|
65
|
+
/** @internal Component type id → meta record. */
|
|
66
|
+
this._Type2Meta = new ArrayMap();
|
|
67
|
+
/** @internal Pre-registered name → type id mappings (server-assigned ids). */
|
|
68
|
+
this._componentNameTypeMap = new Map();
|
|
69
|
+
/** @internal Counter used to auto-assign type ids for "local" components (≥ 256). */
|
|
70
|
+
this._localComponentCounter = LOCAL_COMPONENT_MIN;
|
|
71
|
+
/** @internal `true` once {@link start} (or {@link disableComponentRegistration}) has been called. */
|
|
72
|
+
this._componentRegistrationDisabled = false;
|
|
73
|
+
/** @internal Auto-incrementing entity id counter, seeded by {@link setEntityIdRange}. */
|
|
74
|
+
this._eidCounter = 0;
|
|
52
75
|
/** @internal Single ordered command queue used in deferred mode. */
|
|
53
|
-
this.
|
|
54
|
-
/** @internal Nested
|
|
55
|
-
this.
|
|
56
|
-
/** @internal
|
|
57
|
-
this.
|
|
58
|
-
/** @internal */
|
|
76
|
+
this._commandQueue = [];
|
|
77
|
+
/** @internal Nested {@link beginDefer} / {@link endDefer} count. */
|
|
78
|
+
this._deferredDepth = 0;
|
|
79
|
+
/** @internal `true` while {@link _processCommandQueue} is iterating, to avoid re-entrant drains. */
|
|
80
|
+
this._draining = false;
|
|
81
|
+
/** @internal Phase name → phase. Insertion-ordered, matches pipeline execution order. */
|
|
59
82
|
this._pipeline = new Map();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
get queries() {
|
|
68
|
-
return this._queries;
|
|
83
|
+
/** @internal World-owned tick sources evaluated once per frame. */
|
|
84
|
+
this._tickSources = new Set();
|
|
85
|
+
/** @internal Monotonic frame id used to memoize tick-source evaluation. */
|
|
86
|
+
this._frameCounter = 0;
|
|
87
|
+
/** @internal True while the world is driving one logical frame. */
|
|
88
|
+
this._frameInProgress = false;
|
|
89
|
+
this._tickSources.add(ALWAYS_TICK_SOURCE);
|
|
69
90
|
}
|
|
70
91
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* ```ts
|
|
76
|
-
* const e = world.getOrCreateEntity(snapshot.eid, (e) => {
|
|
77
|
-
* networkEntities.add(e);
|
|
78
|
-
* });
|
|
79
|
-
* e.add(snapshot.type, false);
|
|
80
|
-
* ```
|
|
81
|
-
*
|
|
82
|
-
* @param eid - The entity id to look up or create.
|
|
83
|
-
* @param onCreateCallback - Optional callback invoked only when a **new**
|
|
84
|
-
* entity is created, before it is returned. Use this to initialise
|
|
85
|
-
* bookkeeping (e.g. tracking it in a local set).
|
|
86
|
-
* @returns The existing or newly created entity.
|
|
92
|
+
* @internal Drain the top-level command queue: walk it in arrival order,
|
|
93
|
+
* executing each command. Callbacks may push more commands; they are picked
|
|
94
|
+
* up by index iteration in the same pass.
|
|
87
95
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
e = new Entity(this, eid);
|
|
92
|
-
this._entities.set(eid, e);
|
|
93
|
-
if (onCreateCallback) {
|
|
94
|
-
onCreateCallback(e);
|
|
95
|
-
}
|
|
96
|
+
_processCommandQueue() {
|
|
97
|
+
if (this._draining) {
|
|
98
|
+
return;
|
|
96
99
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this._enqueue({ kind: 0 /* CommandKind.CreateEntity */, entity: e });
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
this._entities.set(eid, e);
|
|
100
|
+
if (this._commandQueue.length === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this._draining = true;
|
|
104
|
+
try {
|
|
105
|
+
for (let i = 0; i < this._commandQueue.length; i++) {
|
|
106
|
+
this._executeCommand(this._commandQueue[i]);
|
|
108
107
|
}
|
|
109
|
-
|
|
108
|
+
this._commandQueue.length = 0;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this._draining = false;
|
|
110
112
|
}
|
|
111
|
-
return this._entities.get(id);
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* Must be called **before** {@link start} (or
|
|
117
|
-
* {@link disableComponentRegistration}). Useful when the world runs alongside
|
|
118
|
-
* a server that owns a different id range — for example, locally-created
|
|
119
|
-
* client entities can start at a high offset to avoid collisions with
|
|
120
|
-
* server-assigned ids.
|
|
121
|
-
*
|
|
122
|
-
* @param min - The first id that will be assigned by {@link entity}.
|
|
123
|
-
* @throws If called after registration has been disabled.
|
|
115
|
+
* @internal Run one command's side effects: data-layer mutation, hook
|
|
116
|
+
* firing, and routing to every registered query / system.
|
|
124
117
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
_executeCommand(cmd) {
|
|
119
|
+
switch (cmd.kind) {
|
|
120
|
+
case 0 /* CommandKind.CreateEntity */:
|
|
121
|
+
this._entities.set(cmd.entity.eid, cmd.entity);
|
|
122
|
+
return;
|
|
123
|
+
case 1 /* CommandKind.Set */:
|
|
124
|
+
cmd.entity._set(cmd.type, cmd.props);
|
|
125
|
+
return;
|
|
126
|
+
case 2 /* CommandKind.Modified */:
|
|
127
|
+
cmd.entity._modified(cmd.type);
|
|
128
|
+
return;
|
|
129
|
+
case 3 /* CommandKind.Remove */:
|
|
130
|
+
cmd.entity._remove(cmd.type);
|
|
131
|
+
return;
|
|
132
|
+
case 4 /* CommandKind.Destroy */:
|
|
133
|
+
cmd.entity._destroy();
|
|
134
|
+
return;
|
|
135
|
+
case 5 /* CommandKind.SetParent */:
|
|
136
|
+
cmd.entity._setParent(cmd.parent);
|
|
137
|
+
return;
|
|
128
138
|
}
|
|
129
|
-
this.eidCounter = min;
|
|
130
139
|
}
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* @returns The corresponding `ComponentMeta`.
|
|
136
|
-
* @throws If no component with that class or type id has been registered.
|
|
141
|
+
* @internal Distribute every registered system into its phase's `systems`
|
|
142
|
+
* list. Called by {@link start}; idempotent so it can be re-run if the
|
|
143
|
+
* pipeline is rebuilt.
|
|
137
144
|
*/
|
|
138
|
-
|
|
139
|
-
let
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
else {
|
|
144
|
-
meta = this.Type2Meta.get(typeOrClass);
|
|
145
|
-
}
|
|
146
|
-
if (!meta) {
|
|
147
|
-
throw `unregistered component meta for component type or class '${typeOrClass}'`;
|
|
145
|
+
_reindexSystems() {
|
|
146
|
+
let _defaultPhase = this._pipeline.get("update");
|
|
147
|
+
if (!_defaultPhase) {
|
|
148
|
+
_defaultPhase = new Phase("update", this);
|
|
149
|
+
this._pipeline.set(_defaultPhase.name, _defaultPhase);
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
const defaultPhase = _defaultPhase;
|
|
152
|
+
this._queries.forEach((q) => {
|
|
153
|
+
if (!(q instanceof System)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let phase = q._phase;
|
|
157
|
+
if (typeof phase === "string") {
|
|
158
|
+
phase = this._pipeline.get(phase);
|
|
159
|
+
}
|
|
160
|
+
phase = phase || defaultPhase;
|
|
161
|
+
phase.systems.push(q);
|
|
162
|
+
});
|
|
163
|
+
this._pipeline.forEach((phase) => {
|
|
164
|
+
console.log("Phase %s : %s", phase.name, phase.systems.map((s) => s.name).join(" -> "));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** @internal Append a command to the deferred-mode queue. */
|
|
168
|
+
_enqueue(cmd) {
|
|
169
|
+
this._commandQueue.push(cmd);
|
|
170
|
+
}
|
|
171
|
+
/** @internal Register a freshly created {@link Query} (called from its constructor). */
|
|
172
|
+
_addQuery(q) {
|
|
173
|
+
this._queries.push(q);
|
|
174
|
+
}
|
|
175
|
+
/** @internal Register a tick source with this world. */
|
|
176
|
+
_registerTickSource(t) {
|
|
177
|
+
this._tickSources.add(t);
|
|
150
178
|
}
|
|
151
179
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* @param typeOrClass - A component class constructor or a numeric type id.
|
|
155
|
-
* @returns The numeric type id.
|
|
180
|
+
* @internal Unregister a query and purge its membership from every entity.
|
|
181
|
+
* Called by {@link Query.destroy}.
|
|
156
182
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
_removeQuery(q) {
|
|
184
|
+
const idx = this._queries.indexOf(q);
|
|
185
|
+
if (idx !== -1) {
|
|
186
|
+
this._queries.splice(idx, 1);
|
|
160
187
|
}
|
|
161
|
-
|
|
188
|
+
this._entities.forEach((e) => e._purgeQuery(q));
|
|
189
|
+
}
|
|
190
|
+
/** @internal Remove an entity from the world's entity map (called by `Entity._destroy`). */
|
|
191
|
+
_unregisterEntity(entity) {
|
|
192
|
+
this._entities.delete(entity.eid);
|
|
193
|
+
}
|
|
194
|
+
/** Read-only view of the live entities, keyed by entity id. */
|
|
195
|
+
get entities() {
|
|
196
|
+
return this._entities;
|
|
197
|
+
}
|
|
198
|
+
/** Read-only view of every registered query (includes systems). */
|
|
199
|
+
get queries() {
|
|
200
|
+
return this._queries;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* `true` while the world is in deferred mode — entity mutations are queued
|
|
204
|
+
* rather than applied inline. Equivalent to "the queue depth is non-zero or
|
|
205
|
+
* the world is currently draining".
|
|
206
|
+
*/
|
|
207
|
+
get deferred() {
|
|
208
|
+
return this._deferredDepth > 0 || this._draining;
|
|
162
209
|
}
|
|
163
210
|
/**
|
|
164
211
|
* Enter deferred mode. Mutations made until the matching {@link endDefer}
|
|
165
212
|
* are queued instead of executing inline.
|
|
166
213
|
*
|
|
167
|
-
* Nested
|
|
168
|
-
* triggers a drain.
|
|
169
|
-
*
|
|
214
|
+
* Nested `beginDefer` / `endDefer` pairs are allowed; only the outermost
|
|
215
|
+
* `endDefer` triggers a queue drain.
|
|
170
216
|
*/
|
|
171
217
|
beginDefer() {
|
|
172
|
-
this.
|
|
218
|
+
this._deferredDepth++;
|
|
173
219
|
}
|
|
174
220
|
/**
|
|
175
|
-
* Leave deferred mode. When the depth returns to zero
|
|
176
|
-
*
|
|
177
|
-
*
|
|
221
|
+
* Leave deferred mode. When the depth returns to zero the world drains the
|
|
222
|
+
* command queue (firing hooks and routing enter / exit / update events).
|
|
178
223
|
*/
|
|
179
224
|
endDefer() {
|
|
180
|
-
this.
|
|
225
|
+
this._deferredDepth--;
|
|
181
226
|
this.flush();
|
|
182
227
|
}
|
|
183
228
|
/**
|
|
229
|
+
* Run `fn` inside a deferred scope. Equivalent to
|
|
230
|
+
* `beginDefer(); try { fn(); } finally { endDefer(); }`.
|
|
184
231
|
*
|
|
185
|
-
* @param fn
|
|
232
|
+
* @param fn - Callback executed in deferred mode.
|
|
186
233
|
*/
|
|
187
234
|
defer(fn) {
|
|
188
235
|
this.beginDefer();
|
|
@@ -194,80 +241,36 @@ export class World {
|
|
|
194
241
|
}
|
|
195
242
|
}
|
|
196
243
|
/**
|
|
197
|
-
* Drain any
|
|
244
|
+
* Drain any commands queued at the top level (depth 0).
|
|
198
245
|
*
|
|
199
|
-
*
|
|
200
|
-
* accumulated mutations
|
|
201
|
-
* the next read or system run.
|
|
246
|
+
* Call between phases or after batch-loading network snapshots to surface
|
|
247
|
+
* accumulated mutations (firing hooks and routing enter / exit / update)
|
|
248
|
+
* before the next read or system run.
|
|
202
249
|
*/
|
|
203
250
|
flush() {
|
|
204
|
-
if (this.
|
|
205
|
-
this.
|
|
251
|
+
if (this._deferredDepth === 0) {
|
|
252
|
+
this._processCommandQueue();
|
|
206
253
|
}
|
|
207
254
|
}
|
|
208
|
-
/** @internal Append a command to the queue. */
|
|
209
|
-
_enqueue(cmd) {
|
|
210
|
-
this.commandQueue.push(cmd);
|
|
211
|
-
}
|
|
212
255
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
this.draining = true;
|
|
225
|
-
try {
|
|
226
|
-
for (let i = 0; i < this.commandQueue.length; i++) {
|
|
227
|
-
this.executeCommand(this.commandQueue[i]);
|
|
228
|
-
}
|
|
229
|
-
this.commandQueue.length = 0;
|
|
230
|
-
}
|
|
231
|
-
finally {
|
|
232
|
-
this.draining = false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* @internal Run a single command's side effects: data-layer mutation, hook
|
|
237
|
-
* firing, and routing to all registered queries / systems.
|
|
256
|
+
* Pre-register a `componentName → typeId` mapping without binding a class.
|
|
257
|
+
*
|
|
258
|
+
* Useful when network messages refer to components by type id and the
|
|
259
|
+
* corresponding class may be registered later. Call this **before**
|
|
260
|
+
* {@link registerComponent} so the class picks up the server-assigned id
|
|
261
|
+
* rather than a locally generated one.
|
|
262
|
+
*
|
|
263
|
+
* @param componentName - String name used in network payloads.
|
|
264
|
+
* @param type - Numeric type id assigned by the server.
|
|
238
265
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
case 0 /* CommandKind.CreateEntity */:
|
|
242
|
-
this._entities.set(cmd.entity.eid, cmd.entity);
|
|
243
|
-
return;
|
|
244
|
-
case 1 /* CommandKind.Set */:
|
|
245
|
-
cmd.entity._set(cmd.type, cmd.props);
|
|
246
|
-
return;
|
|
247
|
-
case 2 /* CommandKind.Modified */:
|
|
248
|
-
cmd.entity._modified(cmd.type);
|
|
249
|
-
return;
|
|
250
|
-
case 3 /* CommandKind.Remove */:
|
|
251
|
-
cmd.entity._remove(cmd.type);
|
|
252
|
-
return;
|
|
253
|
-
case 4 /* CommandKind.Destroy */:
|
|
254
|
-
cmd.entity._destroy();
|
|
255
|
-
return;
|
|
256
|
-
case 5 /* CommandKind.SetParent */:
|
|
257
|
-
cmd.entity._setParent(cmd.parent);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/** @internal Remove an entity from the world's entity map. Called by Entity._destroy. */
|
|
262
|
-
_unregisterEntity(entity) {
|
|
263
|
-
this._entities.delete(entity.eid);
|
|
266
|
+
registerComponentType(componentName, type) {
|
|
267
|
+
this._componentNameTypeMap.set(componentName, type);
|
|
264
268
|
}
|
|
265
269
|
registerComponent(ComponentClass, typeOrComponentName, componentName) {
|
|
266
|
-
if (this.
|
|
270
|
+
if (this._componentRegistrationDisabled) {
|
|
267
271
|
throw "World component registartion is disabled";
|
|
268
272
|
}
|
|
269
273
|
let type = undefined;
|
|
270
|
-
// Determine if the second argument is type or componentName based on its type
|
|
271
274
|
if (typeof typeOrComponentName === "number") {
|
|
272
275
|
type = typeOrComponentName;
|
|
273
276
|
}
|
|
@@ -277,78 +280,202 @@ export class World {
|
|
|
277
280
|
componentName = componentName || ComponentClass.name;
|
|
278
281
|
let local = false;
|
|
279
282
|
if (type === undefined) {
|
|
280
|
-
|
|
281
|
-
type = this.componentNameTypeMap.get(componentName);
|
|
283
|
+
type = this._componentNameTypeMap.get(componentName);
|
|
282
284
|
if (type === undefined) {
|
|
283
|
-
type = this.
|
|
285
|
+
type = this._localComponentCounter++;
|
|
284
286
|
local = true;
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
|
-
let meta = this.
|
|
289
|
+
let meta = this._Class2Meta.get(ComponentClass);
|
|
288
290
|
if (meta) {
|
|
289
291
|
if (local) {
|
|
290
|
-
this.
|
|
292
|
+
this._localComponentCounter--;
|
|
291
293
|
}
|
|
292
294
|
throw `Trying to register ${componentName} with type=${type} which is already registered to ${meta.componentName}`;
|
|
293
295
|
}
|
|
294
296
|
this.registerComponentType(componentName, type);
|
|
295
297
|
meta = new ComponentMeta(ComponentClass, type, componentName);
|
|
296
|
-
this.
|
|
297
|
-
this.
|
|
298
|
+
this._Class2Meta.set(ComponentClass, meta);
|
|
299
|
+
this._Type2Meta.set(type, meta);
|
|
298
300
|
console.log("Registered component %s with type=%d as %s component", componentName, type, local ? "local" : "networked");
|
|
299
301
|
}
|
|
300
302
|
/**
|
|
301
|
-
*
|
|
302
|
-
* class.
|
|
303
|
+
* Look up the {@link ComponentMeta} for a registered component.
|
|
303
304
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
|
|
305
|
+
* @param typeOrClass - Component class or numeric type id.
|
|
306
|
+
* @returns The corresponding meta record.
|
|
307
|
+
* @throws When no component with that class or type id has been registered.
|
|
308
|
+
*/
|
|
309
|
+
getComponentMeta(typeOrClass) {
|
|
310
|
+
let meta;
|
|
311
|
+
if (typeof typeOrClass === "function") {
|
|
312
|
+
meta = this._Class2Meta.get(typeOrClass);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
meta = this._Type2Meta.get(typeOrClass);
|
|
316
|
+
}
|
|
317
|
+
if (!meta) {
|
|
318
|
+
throw `unregistered component meta for component type or class '${typeOrClass}'`;
|
|
319
|
+
}
|
|
320
|
+
return meta;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolve a component class or type id to its numeric type id.
|
|
308
324
|
*
|
|
309
|
-
* @param
|
|
310
|
-
* @
|
|
325
|
+
* @param typeOrClass - Component class or numeric type id.
|
|
326
|
+
* @returns The numeric type id.
|
|
311
327
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
328
|
+
getComponentType(typeOrClass) {
|
|
329
|
+
if (typeof typeOrClass === "function") {
|
|
330
|
+
return this.getComponentMeta(typeOrClass).type;
|
|
331
|
+
}
|
|
332
|
+
return typeOrClass;
|
|
314
333
|
}
|
|
315
|
-
/**
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Return the {@link Hook} for a component class.
|
|
336
|
+
*
|
|
337
|
+
* Hooks let you react to component lifecycle events (add / remove / set)
|
|
338
|
+
* without building a full {@link System}. The same hook is returned on every
|
|
339
|
+
* call — handlers stack on the underlying meta record.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* world.hook(Sprite)
|
|
343
|
+
* .onAdd(c => c.initialize(scene))
|
|
344
|
+
* .onRemove(c => c.destroy());
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @param C - Component class.
|
|
348
|
+
* @returns The hook bound to that component type.
|
|
349
|
+
*/
|
|
350
|
+
hook(C) {
|
|
351
|
+
return this.getComponentMeta(C);
|
|
318
352
|
}
|
|
319
|
-
/**
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Declare a group of mutually exclusive components.
|
|
355
|
+
*
|
|
356
|
+
* Adding any component in the group to an entity that already has another
|
|
357
|
+
* member of the group automatically removes the previous member. Members
|
|
358
|
+
* not in the group are unaffected.
|
|
359
|
+
*
|
|
360
|
+
* ```ts
|
|
361
|
+
* world.setExclusiveComponents(Walking, Running, Idle);
|
|
362
|
+
* entity.add(Walking);
|
|
363
|
+
* entity.add(Running); // Walking is removed automatically
|
|
364
|
+
* ```
|
|
365
|
+
*
|
|
366
|
+
* Each call defines one independent group. A component may belong to at
|
|
367
|
+
* most one group at a time; calling {@link setExclusiveComponents} with the
|
|
368
|
+
* same class again overwrites its group. Safe to call before or after
|
|
369
|
+
* {@link start}.
|
|
370
|
+
*
|
|
371
|
+
* @param components - Two or more component classes that cannot coexist.
|
|
372
|
+
* @throws When any class has not been registered.
|
|
373
|
+
*/
|
|
374
|
+
setExclusiveComponents(...components) {
|
|
375
|
+
const types = components.map((C) => this.getComponentType(C));
|
|
376
|
+
for (let i = 0; i < components.length; i++) {
|
|
377
|
+
this.getComponentMeta(components[i]).exclusive = types.filter((_, j) => j !== i);
|
|
324
378
|
}
|
|
325
|
-
this._entities.forEach((e) => e._purgeQuery(q));
|
|
326
379
|
}
|
|
327
380
|
/**
|
|
328
|
-
*
|
|
381
|
+
* Set the starting value of the auto-incrementing entity id counter.
|
|
382
|
+
*
|
|
383
|
+
* Must be called **before** {@link start} (or
|
|
384
|
+
* {@link disableComponentRegistration}). Useful when the world runs
|
|
385
|
+
* alongside a server that owns a different id range — locally created
|
|
386
|
+
* client entities can start at a high offset to avoid collisions with
|
|
387
|
+
* server-assigned ids.
|
|
388
|
+
*
|
|
389
|
+
* @param min - First id assigned by {@link entity}.
|
|
390
|
+
* @throws When called after registration has been disabled.
|
|
391
|
+
*/
|
|
392
|
+
setEntityIdRange(min) {
|
|
393
|
+
if (this._componentRegistrationDisabled) {
|
|
394
|
+
throw "setEntityIdRange must be called before component registration is disabled";
|
|
395
|
+
}
|
|
396
|
+
this._eidCounter = min;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Return the entity with id `eid`, creating it if it does not yet exist.
|
|
400
|
+
*
|
|
401
|
+
* Used by networking code to materialise server-assigned entities:
|
|
402
|
+
*
|
|
403
|
+
* ```ts
|
|
404
|
+
* const e = world.getOrCreateEntity(snapshot.eid, (e) => {
|
|
405
|
+
* networkEntities.add(e);
|
|
406
|
+
* });
|
|
407
|
+
* e.add(snapshot.type);
|
|
408
|
+
* ```
|
|
409
|
+
*
|
|
410
|
+
* @param eid - Entity id to look up or create.
|
|
411
|
+
* @param onCreateCallback - Optional callback invoked only when a new
|
|
412
|
+
* entity is created, before it is returned. Use it to initialise
|
|
413
|
+
* bookkeeping (e.g. tracking it in a local set).
|
|
414
|
+
* @returns The existing or newly created entity.
|
|
415
|
+
*/
|
|
416
|
+
getOrCreateEntity(eid, onCreateCallback) {
|
|
417
|
+
let e = this._entities.get(eid);
|
|
418
|
+
if (!e) {
|
|
419
|
+
e = new Entity(this, eid);
|
|
420
|
+
this._entities.set(eid, e);
|
|
421
|
+
if (onCreateCallback) {
|
|
422
|
+
onCreateCallback(e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return e;
|
|
426
|
+
}
|
|
427
|
+
entity(id) {
|
|
428
|
+
if (id === undefined) {
|
|
429
|
+
const eid = this._eidCounter++;
|
|
430
|
+
const e = new Entity(this, eid);
|
|
431
|
+
if (this.deferred) {
|
|
432
|
+
this._enqueue({ kind: 0 /* CommandKind.CreateEntity */, entity: e });
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
this._entities.set(eid, e);
|
|
436
|
+
}
|
|
437
|
+
return e;
|
|
438
|
+
}
|
|
439
|
+
return this._entities.get(id);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Destroy every entity currently tracked by the world.
|
|
443
|
+
*
|
|
444
|
+
* Triggers all `onRemove` hooks and `exit` callbacks. Useful when
|
|
445
|
+
* transitioning between game sessions or resetting to a clean state.
|
|
446
|
+
*/
|
|
447
|
+
clearAllEntities() {
|
|
448
|
+
this._entities.forEach((e) => {
|
|
449
|
+
e.destroy();
|
|
450
|
+
});
|
|
451
|
+
this.flush();
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Create, register, and return a new {@link System}, ready for fluent
|
|
455
|
+
* configuration.
|
|
329
456
|
*
|
|
330
457
|
* ```ts
|
|
331
458
|
* world.system("Render")
|
|
332
459
|
* .phase("update")
|
|
333
460
|
* .requires(Position, Sprite)
|
|
334
461
|
* .enter([Sprite], (e, [sprite]) => sprite.initialize(scene))
|
|
335
|
-
* .
|
|
462
|
+
* .each([Position, Sprite], (e, [pos, sprite]) => sprite.draw(pos.x, pos.y));
|
|
336
463
|
* ```
|
|
337
464
|
*
|
|
338
|
-
* @param name -
|
|
339
|
-
* @returns The new
|
|
465
|
+
* @param name - Unique display name for the system.
|
|
466
|
+
* @returns The new system.
|
|
340
467
|
*/
|
|
341
468
|
system(name) {
|
|
342
469
|
return new System(name, this);
|
|
343
470
|
}
|
|
344
471
|
/**
|
|
345
|
-
* Create a standalone {@link Query},
|
|
472
|
+
* Create, register, and return a standalone {@link Query}, ready for fluent
|
|
346
473
|
* configuration.
|
|
347
474
|
*
|
|
348
475
|
* Unlike a {@link System}, a standalone query has no phase and no per-tick
|
|
349
|
-
* callbacks — it is a reactive
|
|
350
|
-
*
|
|
351
|
-
*
|
|
476
|
+
* callbacks — it is a reactive entity set that can be read at any time. It
|
|
477
|
+
* can also be created **after** {@link start}; existing matched entities
|
|
478
|
+
* are backfilled immediately.
|
|
352
479
|
*
|
|
353
480
|
* ```ts
|
|
354
481
|
* const enemies = world.query("Enemies")
|
|
@@ -359,8 +486,8 @@ export class World {
|
|
|
359
486
|
* // enemies.entities is kept up-to-date automatically
|
|
360
487
|
* ```
|
|
361
488
|
*
|
|
362
|
-
* @param name -
|
|
363
|
-
* @returns The new
|
|
489
|
+
* @param name - Unique display name for the query.
|
|
490
|
+
* @returns The new query.
|
|
364
491
|
*/
|
|
365
492
|
query(name) {
|
|
366
493
|
return new Query(name, this);
|
|
@@ -368,157 +495,125 @@ export class World {
|
|
|
368
495
|
filter(q, _guaranteed) {
|
|
369
496
|
return new Filter(this, q);
|
|
370
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Add a named phase to the update pipeline.
|
|
500
|
+
*
|
|
501
|
+
* Phases are executed in insertion order when {@link runPhase} or
|
|
502
|
+
* {@link progress} is called. Systems join a phase via {@link System.phase}.
|
|
503
|
+
*
|
|
504
|
+
* ```ts
|
|
505
|
+
* const preUpdate = world.addPhase("preupdate");
|
|
506
|
+
* const update = world.addPhase("update");
|
|
507
|
+
* const send = world.addPhase("send");
|
|
508
|
+
* ```
|
|
509
|
+
*
|
|
510
|
+
* @param name - Unique phase name. Systems can reference it by this string.
|
|
511
|
+
* @returns The new phase.
|
|
512
|
+
*/
|
|
513
|
+
addPhase(name) {
|
|
514
|
+
const phase = new Phase(name, this);
|
|
515
|
+
this._pipeline.set(name, phase);
|
|
516
|
+
return phase;
|
|
517
|
+
}
|
|
371
518
|
/**
|
|
372
519
|
* Prevent any further calls to {@link registerComponent}.
|
|
373
520
|
*
|
|
374
|
-
* Called automatically by {@link start}.
|
|
375
|
-
*
|
|
521
|
+
* Called automatically by {@link start}. Call directly if you want to lock
|
|
522
|
+
* registration before the rest of the systems are wired up.
|
|
376
523
|
*/
|
|
377
524
|
disableComponentRegistration() {
|
|
378
|
-
this.
|
|
525
|
+
this._componentRegistrationDisabled = true;
|
|
379
526
|
}
|
|
380
527
|
/**
|
|
381
528
|
* Freeze component registration and prepare the world for running.
|
|
382
529
|
*
|
|
383
|
-
* Distributes
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
530
|
+
* Distributes every system registered so far into its phase (defaulting to
|
|
531
|
+
* `"update"`) and logs the phase → system order to the console. Systems
|
|
532
|
+
* and queries can still be created after this call — standalone queries
|
|
533
|
+
* backfill existing matched entities immediately.
|
|
387
534
|
*
|
|
388
|
-
* Call
|
|
535
|
+
* Call once before the first {@link runPhase} / {@link progress}.
|
|
389
536
|
*/
|
|
390
537
|
start() {
|
|
391
|
-
this.
|
|
392
|
-
this.
|
|
393
|
-
}
|
|
394
|
-
reindexSystems() {
|
|
395
|
-
let _defaultPhase = this._pipeline.get("update");
|
|
396
|
-
if (!_defaultPhase) {
|
|
397
|
-
_defaultPhase = new Phase("update", this);
|
|
398
|
-
this._pipeline.set(_defaultPhase.name, _defaultPhase);
|
|
399
|
-
}
|
|
400
|
-
const defaultPhase = _defaultPhase;
|
|
401
|
-
this._queries.forEach((q) => {
|
|
402
|
-
if (!(q instanceof System)) {
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
let phase = q._phase;
|
|
406
|
-
if (typeof phase === "string") {
|
|
407
|
-
phase = this._pipeline.get(phase);
|
|
408
|
-
}
|
|
409
|
-
phase = phase || defaultPhase;
|
|
410
|
-
phase.systems.push(q);
|
|
411
|
-
});
|
|
412
|
-
this._pipeline.forEach((phase) => {
|
|
413
|
-
console.log("Phase %s : %s", phase.name, phase.systems.map((s) => s.name).join(" -> "));
|
|
414
|
-
});
|
|
538
|
+
this._componentRegistrationDisabled = true;
|
|
539
|
+
this._reindexSystems();
|
|
415
540
|
}
|
|
416
541
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* Hooks let you react to component lifecycle events (add / remove / set)
|
|
420
|
-
* without building a full {@link System}. The hook is backed by the
|
|
421
|
-
* component's {@link ComponentMeta} and the same object is returned on every
|
|
422
|
-
* call.
|
|
542
|
+
* Open a new frame and evaluate every registered tick source once.
|
|
423
543
|
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* .onAdd(c => c.initialize(scene))
|
|
427
|
-
* .onRemove(c => c.destroy());
|
|
428
|
-
* ```
|
|
544
|
+
* Call this before one or more {@link runPhase} calls when manually driving
|
|
545
|
+
* phases. {@link progress} wraps this automatically for the full pipeline.
|
|
429
546
|
*
|
|
430
|
-
* @param
|
|
431
|
-
* @
|
|
547
|
+
* @param delta - Milliseconds elapsed since the previous frame.
|
|
548
|
+
* @throws When a frame is already open.
|
|
432
549
|
*/
|
|
433
|
-
|
|
434
|
-
|
|
550
|
+
beginFrame(delta) {
|
|
551
|
+
if (this._frameInProgress) {
|
|
552
|
+
throw "endFrame() not called before beginFrame()";
|
|
553
|
+
}
|
|
554
|
+
this._frameInProgress = true;
|
|
555
|
+
this._frameCounter++;
|
|
556
|
+
this.flush();
|
|
557
|
+
this._tickSources.forEach((t) => t._evalTick(delta, this._frameCounter));
|
|
435
558
|
}
|
|
436
559
|
/**
|
|
437
|
-
*
|
|
560
|
+
* Close the current frame.
|
|
438
561
|
*
|
|
439
|
-
*
|
|
440
|
-
* each one. Systems are assigned to a phase via {@link System.phase}.
|
|
441
|
-
*
|
|
442
|
-
* ```ts
|
|
443
|
-
* const preUpdate = world.addPhase("preupdate");
|
|
444
|
-
* const update = world.addPhase("update");
|
|
445
|
-
* const send = world.addPhase("send");
|
|
446
|
-
* ```
|
|
447
|
-
*
|
|
448
|
-
* @param name - Unique phase name. Systems can reference it by this string.
|
|
449
|
-
* @returns The new {@link IPhase}.
|
|
562
|
+
* @throws When no frame is currently open.
|
|
450
563
|
*/
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
564
|
+
endFrame() {
|
|
565
|
+
if (!this._frameInProgress) {
|
|
566
|
+
throw "beginFrame() not called before endFrame()";
|
|
567
|
+
}
|
|
568
|
+
this._frameInProgress = false;
|
|
455
569
|
}
|
|
456
570
|
/**
|
|
457
|
-
* Execute
|
|
571
|
+
* Execute every system in `phase` within the current frame.
|
|
458
572
|
*
|
|
459
|
-
* Pending top-level mutations are drained
|
|
460
|
-
*
|
|
461
|
-
* deferred scope; mutations made by callbacks
|
|
462
|
-
*
|
|
463
|
-
* next system runs.
|
|
573
|
+
* Pending top-level mutations are drained before the first system runs so
|
|
574
|
+
* each system observes a consistent world. Each system body executes in a
|
|
575
|
+
* deferred scope; mutations made by callbacks land in the world queue and
|
|
576
|
+
* are processed before the next system runs.
|
|
464
577
|
*
|
|
465
|
-
*
|
|
578
|
+
* `runPhase` is safe to call re-entrantly from a system body: it reuses the
|
|
579
|
+
* frame opened by {@link beginFrame} and does not advance `_frameCounter` or
|
|
580
|
+
* re-evaluate tick sources.
|
|
581
|
+
*
|
|
582
|
+
* @param phase - Phase reference returned from {@link addPhase}.
|
|
466
583
|
* @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
|
|
467
584
|
* @param delta - Milliseconds elapsed since the previous tick.
|
|
585
|
+
* @throws When called outside an open frame.
|
|
468
586
|
*/
|
|
469
587
|
runPhase(phase, now, delta) {
|
|
588
|
+
if (!this._frameInProgress) {
|
|
589
|
+
throw "runPhase() called outside a frame — call beginFrame() first";
|
|
590
|
+
}
|
|
470
591
|
this.flush();
|
|
471
592
|
phase.systems.forEach((s) => {
|
|
472
593
|
s._run(now, delta);
|
|
473
|
-
// System._run wraps in begin/end which drains on return; nothing more
|
|
474
|
-
// to do here.
|
|
475
594
|
});
|
|
476
595
|
}
|
|
477
596
|
/**
|
|
478
|
-
* Run every phase in the pipeline in
|
|
479
|
-
*
|
|
480
|
-
* {@link runPhase} for each
|
|
597
|
+
* Run every phase in the pipeline in registration order.
|
|
598
|
+
*
|
|
599
|
+
* Equivalent to `beginFrame(delta)`, calling {@link runPhase} for each
|
|
600
|
+
* phase, then {@link endFrame}. All registered tick sources are evaluated
|
|
601
|
+
* once up front for the whole frame, and the frame is closed in a `finally`
|
|
602
|
+
* block if a system throws.
|
|
481
603
|
*
|
|
482
604
|
* @param now - Absolute timestamp in milliseconds (e.g. `Date.now()`).
|
|
483
605
|
* @param delta - Milliseconds elapsed since the previous tick.
|
|
484
606
|
*/
|
|
485
607
|
progress(now, delta) {
|
|
486
|
-
this.
|
|
487
|
-
|
|
488
|
-
this.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
* After this call, adding any component in the group to an entity that
|
|
495
|
-
* already has another component from the same group will remove the other component
|
|
496
|
-
*
|
|
497
|
-
* ```ts
|
|
498
|
-
* world.setExclusiveComponents(Walking, Running, Idle);
|
|
499
|
-
* // entity.add(Running) throws if entity already has Walking or Idle
|
|
500
|
-
* ```
|
|
501
|
-
*
|
|
502
|
-
* @param components - Two or more component classes that cannot coexist.
|
|
503
|
-
* @throws If any class has not been registered.
|
|
504
|
-
*/
|
|
505
|
-
setExclusiveComponents(...components) {
|
|
506
|
-
const types = components.map((C) => this.getComponentType(C));
|
|
507
|
-
for (let i = 0; i < components.length; i++) {
|
|
508
|
-
this.getComponentMeta(components[i]).exclusive = types.filter((_, j) => j !== i);
|
|
608
|
+
this.beginFrame(delta);
|
|
609
|
+
try {
|
|
610
|
+
this._pipeline.forEach((phase) => {
|
|
611
|
+
this.runPhase(phase, now, delta);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
this.endFrame();
|
|
509
616
|
}
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Destroy every entity currently tracked by the world.
|
|
513
|
-
*
|
|
514
|
-
* Triggers all `onRemove` hooks and `exit` callbacks. Useful when
|
|
515
|
-
* transitioning between game sessions or resetting to a clean state.
|
|
516
|
-
*/
|
|
517
|
-
clearAllEntities() {
|
|
518
|
-
this._entities.forEach((e) => {
|
|
519
|
-
e.destroy();
|
|
520
|
-
});
|
|
521
|
-
this.flush();
|
|
522
617
|
}
|
|
523
618
|
}
|
|
524
619
|
//# sourceMappingURL=world.js.map
|