@vworlds/vecs 1.0.0
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/.claude/settings.json +12 -0
- package/.devcontainer/devcontainer.json +22 -0
- package/.github/workflows/publish.yml +32 -0
- package/README.md +464 -0
- package/dist/component.d.ts +135 -0
- package/dist/component.js +101 -0
- package/dist/component.js.map +1 -0
- package/dist/entity.d.ts +157 -0
- package/dist/entity.js +199 -0
- package/dist/entity.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +25 -0
- package/dist/phase.d.ts +47 -0
- package/dist/phase.js +23 -0
- package/dist/phase.js.map +1 -0
- package/dist/system.d.ts +361 -0
- package/dist/system.js +396 -0
- package/dist/system.js.map +1 -0
- package/dist/util/array_map.d.ts +58 -0
- package/dist/util/array_map.js +84 -0
- package/dist/util/array_map.js.map +1 -0
- package/dist/util/bitset.d.ts +117 -0
- package/dist/util/bitset.js +177 -0
- package/dist/util/bitset.js.map +1 -0
- package/dist/util/events.d.ts +27 -0
- package/dist/util/events.js +43 -0
- package/dist/util/events.js.map +1 -0
- package/dist/util/ordered_set.d.ts +17 -0
- package/dist/util/ordered_set.js +69 -0
- package/dist/util/ordered_set.js.map +1 -0
- package/dist/world.d.ts +279 -0
- package/dist/world.js +453 -0
- package/dist/world.js.map +1 -0
- package/package.json +25 -0
- package/src/component.ts +180 -0
- package/src/entity.ts +276 -0
- package/src/index.ts +6 -0
- package/src/phase.ts +49 -0
- package/src/system.ts +693 -0
- package/src/util/array_map.ts +93 -0
- package/src/util/bitset.ts +199 -0
- package/src/util/events.ts +95 -0
- package/src/util/ordered_set.ts +82 -0
- package/src/world.ts +534 -0
- package/tests/_helpers.ts +30 -0
- package/tests/array_map.test.ts +68 -0
- package/tests/bitset.test.ts +127 -0
- package/tests/component.test.ts +104 -0
- package/tests/entity.test.ts +179 -0
- package/tests/events.test.ts +48 -0
- package/tests/ordered_set.test.ts +153 -0
- package/tests/setup.ts +6 -0
- package/tests/system.test.ts +800 -0
- package/tests/world.test.ts +174 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
package/src/entity.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Component } from "./component.js";
|
|
2
|
+
import type { World } from "./world.js";
|
|
3
|
+
import { ArrayMap } from "./util/array_map.js";
|
|
4
|
+
import { type System } from "./system.js";
|
|
5
|
+
import { Events } from "./util/events.js";
|
|
6
|
+
import { Bitset } from "./util/bitset.js";
|
|
7
|
+
|
|
8
|
+
type EntityEvents = Events<{ destroy(): void }>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A game object — a unique identifier with an arbitrary set of
|
|
12
|
+
* {@link Component | components} attached to it.
|
|
13
|
+
*
|
|
14
|
+
* You never construct an `Entity` directly. Use {@link World.createEntity} for
|
|
15
|
+
* locally-owned entities or {@link World.getOrCreateEntity} when the id is
|
|
16
|
+
* assigned by an external authority (e.g. the server):
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const e = world.createEntity();
|
|
20
|
+
* const pos = e.add(Position);
|
|
21
|
+
* pos.x = 100;
|
|
22
|
+
* pos.modified();
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Entities support a parent–child hierarchy. When a parent is destroyed its
|
|
26
|
+
* children are destroyed recursively. The `children` set is created lazily.
|
|
27
|
+
*/
|
|
28
|
+
export class Entity {
|
|
29
|
+
private components = new ArrayMap<Component>(); //maps component types to Components
|
|
30
|
+
private deletedComponents = new ArrayMap<Component>(); //maps deleted component types to Components
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bitmask representing the set of component types currently attached to this
|
|
34
|
+
* entity. Used by the world to efficiently match entities against system
|
|
35
|
+
* queries.
|
|
36
|
+
*/
|
|
37
|
+
public readonly componentBitmask = new Bitset();
|
|
38
|
+
private readonly systems = new Set<System>();
|
|
39
|
+
private readonly newSystems: System[] = [];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A free-form property bag that modules can use to associate arbitrary data
|
|
43
|
+
* with an entity without registering a component.
|
|
44
|
+
*/
|
|
45
|
+
public properties = new Map<string, any>();
|
|
46
|
+
public declare _events: EntityEvents;
|
|
47
|
+
|
|
48
|
+
/** Parent entity in the scene hierarchy, or `undefined` if root. */
|
|
49
|
+
public parent: Entity | undefined;
|
|
50
|
+
private _children: Set<Entity> | undefined;
|
|
51
|
+
public _archetypeChanged: boolean = false;
|
|
52
|
+
private destroyed = false;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
/** The {@link World} that owns this entity. */
|
|
56
|
+
public readonly world: World,
|
|
57
|
+
/** Unique numeric entity id assigned at creation time. */
|
|
58
|
+
public readonly eid: number
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The set of direct child entities in the scene hierarchy.
|
|
63
|
+
*
|
|
64
|
+
* The set is created lazily on first access. Mutate it only through
|
|
65
|
+
* {@link Entity.destroy} or by setting {@link Entity.parent} on a child —
|
|
66
|
+
* both will keep the parent–child links consistent.
|
|
67
|
+
*/
|
|
68
|
+
public get children(): Set<Entity> {
|
|
69
|
+
if (!this._children) this._children = new Set<Entity>();
|
|
70
|
+
return this._children;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add a component of type `Class` to this entity and return the instance.
|
|
75
|
+
*
|
|
76
|
+
* If the component is already present the existing instance is returned and
|
|
77
|
+
* no callback is fired. Pass `markAsModified = false` to suppress the
|
|
78
|
+
* initial `onSet` / `update` notification (useful when bulk-loading
|
|
79
|
+
* network snapshots before systems are running).
|
|
80
|
+
*
|
|
81
|
+
* @param Class - The component class to instantiate.
|
|
82
|
+
* @param markAsModified - Whether to immediately queue an `update`
|
|
83
|
+
* notification. Defaults to `true`.
|
|
84
|
+
* @returns The new (or existing) component instance, typed as
|
|
85
|
+
* `InstanceType<Class>`.
|
|
86
|
+
*/
|
|
87
|
+
public add<C extends typeof Component>(
|
|
88
|
+
Class: C,
|
|
89
|
+
markAsModified?: boolean
|
|
90
|
+
): InstanceType<C>;
|
|
91
|
+
/**
|
|
92
|
+
* Add a component by its numeric type id.
|
|
93
|
+
*
|
|
94
|
+
* @param type - Numeric component type id (as returned by
|
|
95
|
+
* {@link World.getComponentType}).
|
|
96
|
+
* @param markAsModified - Whether to queue an update notification.
|
|
97
|
+
*/
|
|
98
|
+
public add(type: number, markAsModified?: boolean): Component;
|
|
99
|
+
public add(
|
|
100
|
+
typeOrClass: number | typeof Component,
|
|
101
|
+
markAsModified: boolean = true
|
|
102
|
+
) {
|
|
103
|
+
const type = this.world.getComponentType(typeOrClass);
|
|
104
|
+
|
|
105
|
+
let c = this.components.get(type);
|
|
106
|
+
if (c) {
|
|
107
|
+
return c;
|
|
108
|
+
}
|
|
109
|
+
c = this.world["getComponentInstance"](typeOrClass, this);
|
|
110
|
+
|
|
111
|
+
this.components.set(type, c);
|
|
112
|
+
this.componentBitmask.add(type);
|
|
113
|
+
this.world._notifyComponentAdded(this, c);
|
|
114
|
+
if (markAsModified) this.world._queueUpdatedComponent(c);
|
|
115
|
+
|
|
116
|
+
return c;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove the component of the given class from this entity.
|
|
121
|
+
*
|
|
122
|
+
* The `onRemove` hook and any `exit` callbacks on matching systems are
|
|
123
|
+
* called when archetype changes are flushed at the end of the next system
|
|
124
|
+
* run. Does nothing if the component is not present.
|
|
125
|
+
*
|
|
126
|
+
* @param Class - The component class to remove.
|
|
127
|
+
*/
|
|
128
|
+
public remove<C extends typeof Component>(Class: C): void;
|
|
129
|
+
/**
|
|
130
|
+
* Remove a component by its numeric type id.
|
|
131
|
+
*
|
|
132
|
+
* @param type - Numeric component type id.
|
|
133
|
+
*/
|
|
134
|
+
public remove(type: number): void;
|
|
135
|
+
public remove(typeOrClass: number | typeof Component): void {
|
|
136
|
+
const type = this.world.getComponentType(typeOrClass);
|
|
137
|
+
const c = this.components.get(type);
|
|
138
|
+
if (c) {
|
|
139
|
+
this.components.delete(type);
|
|
140
|
+
this.deletedComponents.set(type, c);
|
|
141
|
+
this.componentBitmask.delete(type);
|
|
142
|
+
this.world._notifyComponentRemoved(this, c);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @internal Called by systems to deliver update notifications. */
|
|
147
|
+
public _notifyModified(component: Component) {
|
|
148
|
+
this.systems.forEach((s) => {
|
|
149
|
+
s.notifyModified(component);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Retrieve the component of type `Class`, or `undefined` if not present.
|
|
155
|
+
*
|
|
156
|
+
* @param typeOrClass - Component class or numeric type id.
|
|
157
|
+
* @param get_deleted - If `true`, also search components that were removed
|
|
158
|
+
* in the current frame but not yet garbage-collected. Useful inside
|
|
159
|
+
* `exit` callbacks to read final component values.
|
|
160
|
+
* @returns The component instance or `undefined`.
|
|
161
|
+
*/
|
|
162
|
+
public get<C extends typeof Component>(
|
|
163
|
+
typeOrClass: number | C,
|
|
164
|
+
get_deleted: boolean = false
|
|
165
|
+
): InstanceType<C> | undefined {
|
|
166
|
+
const type = this.world.getComponentType(typeOrClass);
|
|
167
|
+
|
|
168
|
+
const c = this.components.get(type);
|
|
169
|
+
if (!c && get_deleted) {
|
|
170
|
+
return this.deletedComponents.get(type) as InstanceType<C> | undefined;
|
|
171
|
+
}
|
|
172
|
+
return c as InstanceType<C> | undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Typed event emitter for entity-level lifecycle events.
|
|
177
|
+
*
|
|
178
|
+
* Currently emits one event:
|
|
179
|
+
* - `"destroy"` — fired just before the entity is fully torn down.
|
|
180
|
+
*
|
|
181
|
+
* The emitter is created lazily on first access.
|
|
182
|
+
*/
|
|
183
|
+
public get events(): EntityEvents {
|
|
184
|
+
if (!this._events) {
|
|
185
|
+
this._events = new Events();
|
|
186
|
+
}
|
|
187
|
+
return this._events;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @internal */
|
|
191
|
+
public _hasSystem(s: System) {
|
|
192
|
+
return this.systems.has(s);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @internal */
|
|
196
|
+
public _addSystem(s: System) {
|
|
197
|
+
if (!this.systems.has(s)) {
|
|
198
|
+
this.newSystems.push(s);
|
|
199
|
+
s._enter(this);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @internal */
|
|
204
|
+
public _removeSystem(s: System) {
|
|
205
|
+
if (this.systems.delete(s)) {
|
|
206
|
+
s._exit(this);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** @internal */
|
|
211
|
+
public _updateSystems() {
|
|
212
|
+
this.newSystems.forEach((s) => {
|
|
213
|
+
this.systems.add(s);
|
|
214
|
+
});
|
|
215
|
+
this.newSystems.length = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** `true` when the entity has no components attached. */
|
|
219
|
+
public get empty() {
|
|
220
|
+
return this.components.size == 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private _destroy() {
|
|
224
|
+
if (this.destroyed) return;
|
|
225
|
+
this.destroyed = true;
|
|
226
|
+
this.systems.forEach((s) => {
|
|
227
|
+
s._exit(this);
|
|
228
|
+
});
|
|
229
|
+
this.systems.clear();
|
|
230
|
+
|
|
231
|
+
if (this._events) {
|
|
232
|
+
this._events.emit("destroy");
|
|
233
|
+
this._events.removeAllListeners("destroy");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Destroy this entity and recursively destroy all of its children.
|
|
239
|
+
*
|
|
240
|
+
* All components are removed (triggering `onRemove` hooks and `exit`
|
|
241
|
+
* callbacks), the entity is unregistered from the world, and the `"destroy"`
|
|
242
|
+
* event is emitted. The entity must not be used after calling this method.
|
|
243
|
+
*/
|
|
244
|
+
public destroy() {
|
|
245
|
+
this.world._notifyEntityDestroyed(this);
|
|
246
|
+
this.children.forEach((child) => {
|
|
247
|
+
child.destroy();
|
|
248
|
+
});
|
|
249
|
+
this.children.clear();
|
|
250
|
+
if (this.parent) {
|
|
251
|
+
this.parent.children.delete(this);
|
|
252
|
+
this.world.archetypeChanged(this.parent);
|
|
253
|
+
this.parent = undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @internal */
|
|
258
|
+
public clearDeletedComponents() {
|
|
259
|
+
this.deletedComponents.clear();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Iterate over every component currently attached to this entity.
|
|
264
|
+
*
|
|
265
|
+
* @param callback - Called with each component instance. Iteration order is
|
|
266
|
+
* not guaranteed.
|
|
267
|
+
*/
|
|
268
|
+
public forEachComponent(callback: (c: Component) => void) {
|
|
269
|
+
this.components.forEach(callback);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Returns `"EntityN"` where N is the entity id. */
|
|
273
|
+
public toString(): string {
|
|
274
|
+
return `Entity${this.eid}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type System } from "./system.js";
|
|
2
|
+
export { World } from "./world.js";
|
|
3
|
+
export { Component, type ComponentMeta } from "./component.js";
|
|
4
|
+
export { type Entity } from "./entity.js";
|
|
5
|
+
export { type IPhase } from "./phase.js";
|
|
6
|
+
export { Bitset } from "./util/bitset.js";
|
package/src/phase.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type System } from "./system.js";
|
|
2
|
+
import { type World } from "./world.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A named, ordered bucket of {@link System | systems} within the world's
|
|
6
|
+
* update pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Created internally by {@link World.addPhase}. The systems in a phase run in
|
|
9
|
+
* the order they were registered. Between each system run the world flushes
|
|
10
|
+
* pending archetype changes, so `enter` / `exit` callbacks are always
|
|
11
|
+
* delivered before the next system executes.
|
|
12
|
+
*
|
|
13
|
+
* @internal The concrete class is not part of the public API. Use
|
|
14
|
+
* {@link IPhase} to refer to phases in user code.
|
|
15
|
+
*/
|
|
16
|
+
export class Phase {
|
|
17
|
+
/** Systems that belong to this phase, in execution order. */
|
|
18
|
+
public systems: System[] = [];
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
/** Name used to look up the phase in the pipeline. */
|
|
22
|
+
public readonly name: string,
|
|
23
|
+
public world: World
|
|
24
|
+
) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Public interface for a pipeline phase returned by {@link World.addPhase}.
|
|
29
|
+
*
|
|
30
|
+
* Pass an `IPhase` to {@link System.phase} to assign a system to that phase,
|
|
31
|
+
* or to {@link World.runPhase} to execute it:
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const preUpdate = world.addPhase("preupdate");
|
|
35
|
+
* const send = world.addPhase("send");
|
|
36
|
+
*
|
|
37
|
+
* world.system("NetworkUpdate").phase(preUpdate).run(tick);
|
|
38
|
+
*
|
|
39
|
+
* // each frame:
|
|
40
|
+
* world.runPhase(preUpdate, now, delta);
|
|
41
|
+
* world.runPhase(send, now, delta);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export interface IPhase {
|
|
45
|
+
/** The name this phase was registered under. */
|
|
46
|
+
get name(): string;
|
|
47
|
+
/** The world that owns this phase. */
|
|
48
|
+
get world(): World;
|
|
49
|
+
}
|