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