archetype-ecs 1.3.1 → 1.3.2
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 +104 -34
- package/bench/allocations-1m.js +1 -1
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +81 -81
- package/bench/vs-bitecs.js +31 -56
- package/dist/ComponentRegistry.d.ts +18 -0
- package/dist/ComponentRegistry.js +29 -0
- package/dist/EntityManager.d.ts +52 -0
- package/dist/EntityManager.js +891 -0
- package/dist/Profiler.d.ts +12 -0
- package/dist/Profiler.js +38 -0
- package/dist/System.d.ts +41 -0
- package/dist/System.js +159 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -0
- package/dist/src/ComponentRegistry.d.ts +18 -0
- package/dist/src/ComponentRegistry.js +29 -0
- package/dist/src/EntityManager.d.ts +49 -0
- package/dist/src/EntityManager.js +853 -0
- package/dist/src/Profiler.d.ts +12 -0
- package/dist/src/Profiler.js +38 -0
- package/dist/src/System.d.ts +37 -0
- package/dist/src/System.js +139 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +29 -0
- package/dist/tests/EntityManager.test.d.ts +1 -0
- package/dist/tests/EntityManager.test.js +651 -0
- package/dist/tests/System.test.d.ts +1 -0
- package/dist/tests/System.test.js +630 -0
- package/dist/tests/types.d.ts +1 -0
- package/dist/tests/types.js +129 -0
- package/package.json +9 -7
- package/src/ComponentRegistry.ts +49 -0
- package/src/EntityManager.ts +1018 -0
- package/src/{Profiler.js → Profiler.ts} +18 -5
- package/src/System.ts +226 -0
- package/src/index.ts +44 -0
- package/tests/{EntityManager.test.js → EntityManager.test.ts} +338 -70
- package/tests/System.test.ts +730 -0
- package/tests/types.ts +67 -66
- package/tsconfig.json +8 -5
- package/tsconfig.test.json +13 -0
- package/src/ComponentRegistry.js +0 -21
- package/src/EntityManager.js +0 -578
- package/src/index.d.ts +0 -118
- package/src/index.js +0 -37
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
export interface ProfilerEntry {
|
|
2
|
+
avg: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface Profiler {
|
|
6
|
+
readonly enabled: boolean;
|
|
7
|
+
setEnabled(value: boolean): void;
|
|
8
|
+
begin(): number;
|
|
9
|
+
end(name: string, t0: number): void;
|
|
10
|
+
record(name: string, ms: number): void;
|
|
11
|
+
getData(): Map<string, ProfilerEntry>;
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
const EMA_ALPHA = 0.1;
|
|
2
|
-
const data = new Map();
|
|
15
|
+
const data = new Map<string, ProfilerEntry>();
|
|
3
16
|
let enabled = false;
|
|
4
17
|
|
|
5
|
-
export const profiler = {
|
|
18
|
+
export const profiler: Profiler = {
|
|
6
19
|
get enabled() { return enabled; },
|
|
7
20
|
|
|
8
|
-
setEnabled(value) {
|
|
21
|
+
setEnabled(value: boolean) {
|
|
9
22
|
enabled = value;
|
|
10
23
|
if (!value) data.clear();
|
|
11
24
|
},
|
|
@@ -14,7 +27,7 @@ export const profiler = {
|
|
|
14
27
|
return enabled ? performance.now() : 0;
|
|
15
28
|
},
|
|
16
29
|
|
|
17
|
-
end(name, t0) {
|
|
30
|
+
end(name: string, t0: number) {
|
|
18
31
|
if (!enabled) return;
|
|
19
32
|
const ms = performance.now() - t0;
|
|
20
33
|
const entry = data.get(name);
|
|
@@ -25,7 +38,7 @@ export const profiler = {
|
|
|
25
38
|
}
|
|
26
39
|
},
|
|
27
40
|
|
|
28
|
-
record(name, ms) {
|
|
41
|
+
record(name: string, ms: number) {
|
|
29
42
|
if (!enabled) return;
|
|
30
43
|
const entry = data.get(name);
|
|
31
44
|
if (entry) {
|
package/src/System.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { ComponentDef } from './ComponentRegistry.js';
|
|
2
|
+
import type { EntityId, EntityManager, ArchetypeView } from './EntityManager.js';
|
|
3
|
+
|
|
4
|
+
// ── Decorators (TC39 Stage 3) ────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function OnAdded(...types: ComponentDef[]) {
|
|
7
|
+
return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
|
|
8
|
+
_context.addInitializer(function () {
|
|
9
|
+
const self = this as unknown as System;
|
|
10
|
+
self._registerHook('add', types, method.bind(self));
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function OnRemoved(...types: ComponentDef[]) {
|
|
16
|
+
return function (method: (id: EntityId) => void, _context: ClassMethodDecoratorContext) {
|
|
17
|
+
_context.addInitializer(function () {
|
|
18
|
+
const self = this as unknown as System;
|
|
19
|
+
self._registerHook('remove', types, method.bind(self));
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Base class ───────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface Hook {
|
|
27
|
+
buffer: Set<EntityId>;
|
|
28
|
+
handler: (id: EntityId) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class System {
|
|
32
|
+
em: EntityManager;
|
|
33
|
+
_unsubs: (() => void)[] = [];
|
|
34
|
+
_hooks: Hook[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(em: EntityManager) {
|
|
37
|
+
this.em = em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_registerHook(kind: 'add' | 'remove', types: ComponentDef[], handler: (id: EntityId) => void): void {
|
|
41
|
+
const buffer = new Set<EntityId>();
|
|
42
|
+
|
|
43
|
+
if (kind === 'add') {
|
|
44
|
+
for (const comp of types) {
|
|
45
|
+
const unsub = this.em.onAdd(comp, (id: EntityId) => {
|
|
46
|
+
for (let i = 0; i < types.length; i++) {
|
|
47
|
+
if (!this.em.hasComponent(id, types[i])) return;
|
|
48
|
+
}
|
|
49
|
+
buffer.add(id);
|
|
50
|
+
});
|
|
51
|
+
this._unsubs.push(unsub);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
for (const comp of types) {
|
|
55
|
+
const unsub = this.em.onRemove(comp, (id: EntityId) => buffer.add(id));
|
|
56
|
+
this._unsubs.push(unsub);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._hooks.push({ buffer, handler });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
forEach(types: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void {
|
|
64
|
+
this.em.forEach(types, callback, exclude);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tick?(): void;
|
|
68
|
+
|
|
69
|
+
/** Process hooks and tick without clearing removed-data snapshots. */
|
|
70
|
+
_runCore(): void {
|
|
71
|
+
for (const hook of this._hooks) {
|
|
72
|
+
for (const id of hook.buffer) hook.handler(id);
|
|
73
|
+
hook.buffer.clear();
|
|
74
|
+
}
|
|
75
|
+
if (this.tick) this.tick();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
run(): void {
|
|
79
|
+
this._runCore();
|
|
80
|
+
this.em.commitRemovals();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dispose(): void {
|
|
84
|
+
for (const unsub of this._unsubs) unsub();
|
|
85
|
+
this._unsubs.length = 0;
|
|
86
|
+
this._hooks.length = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Functional API ───────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
type HookCallback = (entityId: EntityId) => void;
|
|
93
|
+
|
|
94
|
+
export interface SystemContext {
|
|
95
|
+
onAdded(...args: [...ComponentDef[], HookCallback]): void;
|
|
96
|
+
onRemoved(...args: [...ComponentDef[], HookCallback]): void;
|
|
97
|
+
forEach(types: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type FunctionalSystemConstructor = (sys: SystemContext) => (() => void) | void;
|
|
101
|
+
|
|
102
|
+
interface FunctionalHook {
|
|
103
|
+
unsubs: (() => void)[];
|
|
104
|
+
buffer: Set<EntityId>;
|
|
105
|
+
callback: HookCallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface FunctionalSystem {
|
|
109
|
+
(): void;
|
|
110
|
+
/** @internal Process hooks and tick without clearing removed-data snapshots. */
|
|
111
|
+
_runCore(): void;
|
|
112
|
+
dispose(): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createSystem(em: EntityManager, constructor: FunctionalSystemConstructor): FunctionalSystem {
|
|
116
|
+
const hooks: FunctionalHook[] = [];
|
|
117
|
+
|
|
118
|
+
const sys: SystemContext = {
|
|
119
|
+
onAdded(...args: [...ComponentDef[], HookCallback]) {
|
|
120
|
+
const callback = args[args.length - 1] as HookCallback;
|
|
121
|
+
const types = args.slice(0, -1) as ComponentDef[];
|
|
122
|
+
const buffer = new Set<EntityId>();
|
|
123
|
+
const unsubs: (() => void)[] = [];
|
|
124
|
+
|
|
125
|
+
for (const comp of types) {
|
|
126
|
+
const unsub = em.onAdd(comp, (id: EntityId) => {
|
|
127
|
+
for (let i = 0; i < types.length; i++) {
|
|
128
|
+
if (!em.hasComponent(id, types[i])) return;
|
|
129
|
+
}
|
|
130
|
+
buffer.add(id);
|
|
131
|
+
});
|
|
132
|
+
unsubs.push(unsub);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
hooks.push({ unsubs, buffer, callback });
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onRemoved(...args: [...ComponentDef[], HookCallback]) {
|
|
139
|
+
const callback = args[args.length - 1] as HookCallback;
|
|
140
|
+
const types = args.slice(0, -1) as ComponentDef[];
|
|
141
|
+
const buffer = new Set<EntityId>();
|
|
142
|
+
const unsubs: (() => void)[] = [];
|
|
143
|
+
|
|
144
|
+
for (const comp of types) {
|
|
145
|
+
const unsub = em.onRemove(comp, (id: EntityId) => {
|
|
146
|
+
buffer.add(id);
|
|
147
|
+
});
|
|
148
|
+
unsubs.push(unsub);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
hooks.push({ unsubs, buffer, callback });
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
forEach(types: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]) {
|
|
155
|
+
em.forEach(types, callback, exclude);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const tick = constructor(sys);
|
|
160
|
+
|
|
161
|
+
function runCore() {
|
|
162
|
+
for (const hook of hooks) {
|
|
163
|
+
for (const id of hook.buffer) hook.callback(id);
|
|
164
|
+
hook.buffer.clear();
|
|
165
|
+
}
|
|
166
|
+
if (tick) tick();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function system() {
|
|
170
|
+
runCore();
|
|
171
|
+
em.commitRemovals();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
system._runCore = runCore;
|
|
175
|
+
|
|
176
|
+
system.dispose = function () {
|
|
177
|
+
for (const hook of hooks) {
|
|
178
|
+
for (const unsub of hook.unsubs) unsub();
|
|
179
|
+
}
|
|
180
|
+
hooks.length = 0;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return system;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Activator ────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
interface Runnable {
|
|
189
|
+
_runCore(): void;
|
|
190
|
+
dispose(): void;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface Pipeline {
|
|
194
|
+
(): void;
|
|
195
|
+
dispose(): void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isSystemClass(entry: Function): entry is new (em: EntityManager) => System {
|
|
199
|
+
let proto = entry.prototype;
|
|
200
|
+
while (proto) {
|
|
201
|
+
proto = Object.getPrototypeOf(proto);
|
|
202
|
+
if (proto === System.prototype) return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createSystems(em: EntityManager, entries: (FunctionalSystemConstructor | (new (em: EntityManager) => System))[]): Pipeline {
|
|
208
|
+
const systems: Runnable[] = entries.map(Entry => {
|
|
209
|
+
if (isSystemClass(Entry)) {
|
|
210
|
+
return new Entry(em);
|
|
211
|
+
}
|
|
212
|
+
const sys = createSystem(em, Entry as FunctionalSystemConstructor);
|
|
213
|
+
return { _runCore: sys._runCore, dispose: sys.dispose };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
function pipeline() {
|
|
217
|
+
for (let i = 0; i < systems.length; i++) systems[i]._runCore();
|
|
218
|
+
em.commitRemovals();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pipeline.dispose = function () {
|
|
222
|
+
for (let i = 0; i < systems.length; i++) systems[i].dispose();
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return pipeline;
|
|
226
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { createEntityManager } from './EntityManager.js';
|
|
2
|
+
export type { EntityId, ArchetypeView, EntityManager, SerializedData } from './EntityManager.js';
|
|
3
|
+
export { createSystem, createSystems, System, OnAdded, OnRemoved } from './System.js';
|
|
4
|
+
export type { SystemContext, FunctionalSystemConstructor, FunctionalSystem, Pipeline } from './System.js';
|
|
5
|
+
export { profiler } from './Profiler.js';
|
|
6
|
+
export type { Profiler, ProfilerEntry } from './Profiler.js';
|
|
7
|
+
export { TYPED, componentSchemas, parseTypeSpec } from './ComponentRegistry.js';
|
|
8
|
+
export type { ComponentDef, FieldRef, TypeSpec } from './ComponentRegistry.js';
|
|
9
|
+
|
|
10
|
+
import { parseTypeSpec, componentSchemas, type ComponentDef, type FieldRef, type TypeSpec } from './ComponentRegistry.js';
|
|
11
|
+
|
|
12
|
+
// Tag component (no fields)
|
|
13
|
+
export function component(name: string): ComponentDef;
|
|
14
|
+
// Uniform type with field list
|
|
15
|
+
export function component<const F extends readonly string[]>(name: string, type: string, fields: F): ComponentDef<F[number]>;
|
|
16
|
+
// Schema object with mixed types
|
|
17
|
+
export function component<S extends Record<string, string>>(name: string, schema: S): ComponentDef<Extract<keyof S, string>>;
|
|
18
|
+
export function component(name: string, typeOrSchema?: string | Record<string, string>, fields?: string[]): ComponentDef<string> {
|
|
19
|
+
const sym = Symbol(name);
|
|
20
|
+
const comp: Record<string, unknown> = { _sym: sym, _name: name };
|
|
21
|
+
|
|
22
|
+
let schema: Record<string, TypeSpec> | undefined;
|
|
23
|
+
|
|
24
|
+
if (typeof typeOrSchema === 'string' && Array.isArray(fields)) {
|
|
25
|
+
const spec = parseTypeSpec(typeOrSchema);
|
|
26
|
+
schema = {};
|
|
27
|
+
for (const f of fields) {
|
|
28
|
+
schema[f] = spec;
|
|
29
|
+
comp[f] = { _sym: sym, _field: f } satisfies FieldRef;
|
|
30
|
+
}
|
|
31
|
+
} else if (typeOrSchema && typeof typeOrSchema === 'object') {
|
|
32
|
+
schema = {};
|
|
33
|
+
for (const [field, type] of Object.entries(typeOrSchema)) {
|
|
34
|
+
schema[field] = parseTypeSpec(type);
|
|
35
|
+
comp[field] = { _sym: sym, _field: field } satisfies FieldRef;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (schema) {
|
|
40
|
+
componentSchemas.set(sym, schema);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return comp as ComponentDef<string>;
|
|
44
|
+
}
|