@codehz/ecs 0.8.0 → 0.8.1
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/dist/builder.d.mts +28 -9
- package/dist/world.mjs +247 -89
- package/dist/world.mjs.map +1 -1
- package/package.json +2 -1
- package/skills/ecs/SKILL.md +333 -0
- package/src/__tests__/core/archetype.test.ts +4 -2
- package/src/__tests__/perf/dontfragment-wildcard.perf.test.ts +107 -0
- package/src/__tests__/query/filter.test.ts +3 -2
- package/src/archetype/archetype.ts +64 -60
- package/src/archetype/helpers.ts +13 -6
- package/src/archetype/store.ts +222 -15
- package/src/world/commands.ts +22 -34
- package/src/world/references.ts +59 -0
- package/src/world/world.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codehz/ecs",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dist",
|
|
23
23
|
"examples",
|
|
24
24
|
"src",
|
|
25
|
+
"skills",
|
|
25
26
|
"LICENSE",
|
|
26
27
|
"README.md",
|
|
27
28
|
"README.en.md"
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# @codehz/ecs — User Guide for AI Coding Assistants
|
|
2
|
+
|
|
3
|
+
**Purpose**: This document defines the strict usage contract for `@codehz/ecs`. Follow these rules when writing application code. Violating them is the most common source of subtle, hard-to-debug errors.
|
|
4
|
+
|
|
5
|
+
The library uses **archetype storage + deferred command buffering**. All structural changes are queued and applied only on `sync()`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Single Most Important Concept
|
|
10
|
+
|
|
11
|
+
**Every structural change is deferred.**
|
|
12
|
+
|
|
13
|
+
- `world.set()`, `world.remove()`, `world.delete()`, `world.new()`, `world.spawn().build()`, `world.spawnMany()`
|
|
14
|
+
- These calls **never** take effect immediately.
|
|
15
|
+
- They are only applied when `world.sync()` is called.
|
|
16
|
+
|
|
17
|
+
**Reading state after a mutation without calling `sync()` will see stale data.**
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Golden Rules (Memorize)
|
|
22
|
+
|
|
23
|
+
1. **MUST** call `world.sync()` to make any structural change visible.
|
|
24
|
+
2. **MUST** treat `world.sync()` as the very last operation in a frame / pipeline pass.
|
|
25
|
+
3. **NEVER** call `world.get()` without first confirming the component exists on that entity.
|
|
26
|
+
4. **NEVER** store raw `EntityId` values inside normal component data.
|
|
27
|
+
5. **MUST** create queries once via `createQuery()` at startup and reuse them.
|
|
28
|
+
6. **MUST** only pass required (non-optional) components to `createQuery()`.
|
|
29
|
+
7. **NEVER** call `sync()` inside `forEach`, hooks, or while iterating query results.
|
|
30
|
+
8. **NEVER** confuse `remove(entity, Component)` with `delete(entity)`.
|
|
31
|
+
9. **MUST** use relation components (`relation(Comp, target)`) instead of storing `EntityId` in data when you need to reference other entities.
|
|
32
|
+
10. **MUST** understand the three relation flags (`exclusive`, `cascadeDelete`, `dontFragment`) before using relations.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Detailed Rules & Danger Zones
|
|
37
|
+
|
|
38
|
+
### 1. Deferred Execution & `sync()`
|
|
39
|
+
|
|
40
|
+
- All mutations are buffered in the command buffer.
|
|
41
|
+
- `world.sync()` executes the entire buffer in one batch.
|
|
42
|
+
- After `sync()`, queries, hooks, and direct access will see the new state.
|
|
43
|
+
|
|
44
|
+
**Correct pattern**:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
world.set(e, Position, { x: 10, y: 20 });
|
|
48
|
+
world.set(e, Velocity, { x: 1, y: 0 });
|
|
49
|
+
world.sync(); // Changes become visible here
|
|
50
|
+
|
|
51
|
+
const pos = world.get(e, Position); // Safe
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Anti-pattern**:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
world.set(e, Position, { x: 10, y: 20 });
|
|
58
|
+
const pos = world.get(e, Position); // Still sees old data (or throws)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Accessing Component Data Safely
|
|
62
|
+
|
|
63
|
+
**Both `get()` and `getOptional()` throw if the entity does not exist.**
|
|
64
|
+
|
|
65
|
+
- `world.get(entity, Comp)` — throws if component is absent on an existing entity.
|
|
66
|
+
- `world.getOptional(entity, Comp)` — returns `undefined` only when the component is missing (entity must exist).
|
|
67
|
+
- `world.has(entity, Comp)` — safe existence check.
|
|
68
|
+
|
|
69
|
+
**Correct pattern**:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
if (world.has(e, Health)) {
|
|
73
|
+
const h = world.get(e, Health); // Safe
|
|
74
|
+
// or
|
|
75
|
+
const opt = world.getOptional(e, Health);
|
|
76
|
+
if (opt) {
|
|
77
|
+
/* ... */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Anti-pattern**:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const val = world.get(e, SomeComp); // May throw even if value could be undefined
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Rule**: Prefer `has()` + `get()`, or `getOptional()`, never bare `get()`.
|
|
89
|
+
|
|
90
|
+
### 3. Queries — Creation, Caching, Optional Components
|
|
91
|
+
|
|
92
|
+
- `createQuery()` builds a cached query that is kept up-to-date by the world.
|
|
93
|
+
- Queries are **expensive to create** and **must be reused** across frames.
|
|
94
|
+
|
|
95
|
+
**Critical rule for optional components**:
|
|
96
|
+
|
|
97
|
+
- Only put **required** components in `createQuery([...])`.
|
|
98
|
+
- Supply optional components **only at iteration time**.
|
|
99
|
+
|
|
100
|
+
**Correct pattern**:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const q = world.createQuery([Position]); // Only required components here
|
|
104
|
+
|
|
105
|
+
q.forEach([Position, { optional: Velocity }], (e, pos, vel) => {
|
|
106
|
+
// vel may be undefined
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Anti-pattern**:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const q = world.createQuery([Position, { optional: Velocity }]); // Wrong
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Performance rule**: Create all queries once during initialization. Store them in variables or a container. Never call `createQuery` inside a loop or per-frame logic.
|
|
117
|
+
|
|
118
|
+
### 4. Lifecycle Hooks
|
|
119
|
+
|
|
120
|
+
- `world.hook([CompA, CompB], { on_init, on_set, on_remove })`
|
|
121
|
+
- `on_init` is called **at registration time** for every entity that already matches.
|
|
122
|
+
- `on_set` / `on_remove` fire **after `sync()`** when an entity enters or leaves the set.
|
|
123
|
+
- Optional components and negative filters are supported.
|
|
124
|
+
|
|
125
|
+
**Correct pattern**:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
world.hook([Position, { optional: Velocity }], {
|
|
129
|
+
on_set: (e, pos, vel) => {
|
|
130
|
+
/* ... */
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Rule**: Do not assume hooks run synchronously with the `set()` call.
|
|
136
|
+
|
|
137
|
+
### 5. Structural Changes Inside Iteration / Hooks
|
|
138
|
+
|
|
139
|
+
You **may** queue structural changes inside `forEach` and hook callbacks.
|
|
140
|
+
|
|
141
|
+
**You MUST NOT**:
|
|
142
|
+
|
|
143
|
+
- Call `world.sync()` inside them.
|
|
144
|
+
- Read data expecting the queued changes to be visible.
|
|
145
|
+
|
|
146
|
+
**Correct pattern**:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
movementQuery.forEach([Position, Velocity], (e, pos, vel) => {
|
|
150
|
+
if (shouldDestroy(e)) {
|
|
151
|
+
world.delete(e); // Queued safely
|
|
152
|
+
} else if (needsNewComp) {
|
|
153
|
+
world.set(e, NewComp, data);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
world.sync(); // Apply everything after iteration
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Anti-pattern**:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
query.forEach([A], (e, a) => {
|
|
163
|
+
world.set(e, B, data);
|
|
164
|
+
world.sync(); // Extremely dangerous
|
|
165
|
+
const b = world.get(e, B); // Undefined behavior
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 6. `remove()` vs `delete()` — Do Not Confuse Them
|
|
170
|
+
|
|
171
|
+
| Method | Effect |
|
|
172
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
173
|
+
| `world.remove(e, Comp)` | Removes **one component** from the entity |
|
|
174
|
+
| `world.delete(e)` | Destroys the **entire entity** and all its components |
|
|
175
|
+
|
|
176
|
+
**Anti-pattern** (very common):
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
world.remove(e, Enemy); // This does NOT destroy the entity!
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Correct pattern for destroying an entity**:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
world.delete(e);
|
|
186
|
+
world.sync();
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 7. Relations — The Three Important Flags
|
|
190
|
+
|
|
191
|
+
Use `relation(Component, target)` to create entity-to-entity references.
|
|
192
|
+
|
|
193
|
+
**`exclusive: true`** (on the component definition)
|
|
194
|
+
|
|
195
|
+
- An entity can have at most one relation of this base component.
|
|
196
|
+
- Setting a new target automatically removes the previous one during `sync()`.
|
|
197
|
+
|
|
198
|
+
**`cascadeDelete: true`**
|
|
199
|
+
|
|
200
|
+
- When the target entity is deleted, **the entire referencing entity is deleted**.
|
|
201
|
+
- This is transitive and powerful. Use deliberately.
|
|
202
|
+
|
|
203
|
+
**`dontFragment: true`**
|
|
204
|
+
|
|
205
|
+
- Prevents archetype fragmentation when many different targets exist.
|
|
206
|
+
- **Required** for relations with high cardinality or frequent target changes (e.g. `ChildOf` with thousands of children, AI targeting, inventory).
|
|
207
|
+
|
|
208
|
+
**Correct definition patterns**:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const ChildOf = component({ exclusive: true, cascadeDelete: true });
|
|
212
|
+
const Targeting = component({ exclusive: true, dontFragment: true });
|
|
213
|
+
const InventorySlot = component({ dontFragment: true });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 8. Referencing Other Entities — Never Store Raw EntityId
|
|
217
|
+
|
|
218
|
+
**Strong rule**: Do **not** put `EntityId` values inside normal component data.
|
|
219
|
+
|
|
220
|
+
Reasons:
|
|
221
|
+
|
|
222
|
+
- Entities can be deleted.
|
|
223
|
+
- Entity IDs are reused after deletion.
|
|
224
|
+
- You will create dangling references and extremely subtle bugs.
|
|
225
|
+
|
|
226
|
+
**Correct approach** — always use a relation component:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const Owner = component({ exclusive: true });
|
|
230
|
+
|
|
231
|
+
world.set(item, relation(Owner, player));
|
|
232
|
+
world.sync();
|
|
233
|
+
|
|
234
|
+
// Later the relation is automatically cleaned up if player is deleted
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Anti-pattern**:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
type Item = { owner: EntityId }; // Dangerous
|
|
241
|
+
world.set(item, ItemComp, { owner: player });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 9. Singletons (Component-as-Entity)
|
|
245
|
+
|
|
246
|
+
You can use a component ID itself as a singleton:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
world.set(GlobalConfig, { debug: true, maxEntities: 10000 });
|
|
250
|
+
world.sync();
|
|
251
|
+
|
|
252
|
+
const cfg = world.get(GlobalConfig); // Note: no entity argument
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This is useful for global configuration, time, resources, etc.
|
|
256
|
+
|
|
257
|
+
### 10. Serialization
|
|
258
|
+
|
|
259
|
+
- `world.serialize()` produces an **in-memory snapshot**.
|
|
260
|
+
- `new World(snapshot)` restores entities and component data.
|
|
261
|
+
- **Not restored**: cached queries, lifecycle hooks, command buffer state.
|
|
262
|
+
- `undefined` is a valid component value and is preserved.
|
|
263
|
+
- For real persistence you must implement custom encode/decode.
|
|
264
|
+
|
|
265
|
+
**Rule**: Treat serialization as "save the current world state for later in this process or for network transfer", not as a general-purpose save file format.
|
|
266
|
+
|
|
267
|
+
### 11. Entity / Component ID Rules (Quick Reference)
|
|
268
|
+
|
|
269
|
+
- Component IDs: `1` – `1023`
|
|
270
|
+
- Entity IDs: `1024` and above
|
|
271
|
+
- Relation IDs: negative (encoded as `-(componentId * 2^42 + targetId)`)
|
|
272
|
+
- Do not rely on specific numeric values in application code.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Recommended Patterns (Copy These Shapes)
|
|
277
|
+
|
|
278
|
+
**Game loop with @codehz/pipeline**:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const gameLoop = pipeline<{ deltaTime: number }>()
|
|
282
|
+
.addPass(() => {
|
|
283
|
+
/* read-only systems */
|
|
284
|
+
})
|
|
285
|
+
.addPass(() => {
|
|
286
|
+
/* systems that queue mutations */
|
|
287
|
+
})
|
|
288
|
+
.addPass(() => {
|
|
289
|
+
world.sync();
|
|
290
|
+
}) // Always last
|
|
291
|
+
.build();
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Standard query + mutation pattern**:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const query = world.createQuery([Position, Velocity]);
|
|
298
|
+
|
|
299
|
+
function update(dt: number) {
|
|
300
|
+
query.forEach([Position, Velocity], (e, pos, vel) => {
|
|
301
|
+
pos.x += vel.x * dt;
|
|
302
|
+
if (shouldDestroy(e)) world.delete(e);
|
|
303
|
+
});
|
|
304
|
+
world.sync();
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Safe entity creation**:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const e = world.spawn().with(Position, { x: 0, y: 0 }).withRelation(ChildOf, parent).build();
|
|
312
|
+
world.sync();
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Common Anti-Patterns
|
|
318
|
+
|
|
319
|
+
- Calling `get()` immediately after `set()` without `sync()`
|
|
320
|
+
- Creating queries inside the update loop
|
|
321
|
+
- Putting optional wrappers inside `createQuery()`
|
|
322
|
+
- Storing `EntityId` in component payloads
|
|
323
|
+
- Using `remove()` when `delete()` was intended
|
|
324
|
+
- Calling `sync()` inside `forEach` or hook callbacks
|
|
325
|
+
- Assuming `on_set` fires synchronously with `set()`
|
|
326
|
+
- Forgetting that `exclusive` relations silently remove the previous target
|
|
327
|
+
- Using `cascadeDelete` without understanding it deletes whole entities
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
**Follow these rules. They exist because the architecture deliberately trades immediate visibility for performance and cache efficiency.**
|
|
332
|
+
|
|
333
|
+
When in doubt, ask: "Has `sync()` been called since the last structural change, and am I allowed to see the result?"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { Archetype } from "../../archetype/archetype";
|
|
3
|
+
import { DontFragmentStoreImpl } from "../../archetype/store";
|
|
3
4
|
import { component, createEntityId, relation, type EntityId } from "../../entity";
|
|
4
5
|
|
|
5
6
|
describe("Archetype", () => {
|
|
@@ -9,8 +10,9 @@ describe("Archetype", () => {
|
|
|
9
10
|
const positionComponent = component<Position>();
|
|
10
11
|
const velocityComponent = component<Velocity>();
|
|
11
12
|
|
|
12
|
-
// Helper function to create a
|
|
13
|
-
|
|
13
|
+
// Helper function to create a real DontFragmentStore for testing.
|
|
14
|
+
// We use the production implementation because the interface is now fully semantic.
|
|
15
|
+
const createDontFragmentRelations = () => new DontFragmentStoreImpl();
|
|
14
16
|
|
|
15
17
|
it("should create an archetype with component types", () => {
|
|
16
18
|
const archetype = new Archetype([positionComponent, velocityComponent], createDontFragmentRelations());
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { component, relation, type EntityId } from "../../entity";
|
|
3
|
+
import { World } from "../../world/world";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Focused performance tests for the refactored DontFragmentStore.
|
|
7
|
+
*
|
|
8
|
+
* These tests specifically exercise the X-class paths that motivated the refactor:
|
|
9
|
+
* - Wildcard queries over dontFragment relations (relation(Comp, "*"))
|
|
10
|
+
* - Frequent exclusive relation flips (the classic ChildOf pattern)
|
|
11
|
+
* - hasRelationWithComponentId / archetype filtering cost
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function benchmark(label: string, warmupRounds: number, measuredRounds: number, fn: (round: number) => void): number {
|
|
15
|
+
const durations: number[] = [];
|
|
16
|
+
const totalRounds = warmupRounds + measuredRounds;
|
|
17
|
+
|
|
18
|
+
for (let round = 0; round < totalRounds; round++) {
|
|
19
|
+
const start = performance.now();
|
|
20
|
+
fn(round);
|
|
21
|
+
const duration = performance.now() - start;
|
|
22
|
+
if (round >= warmupRounds) {
|
|
23
|
+
durations.push(duration);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const average = durations.reduce((sum, d) => sum + d, 0) / durations.length;
|
|
28
|
+
console.log(
|
|
29
|
+
`${label}: avg ${average.toFixed(2)}ms after ${warmupRounds} warmup (${durations.map((d) => d.toFixed(2)).join("ms, ")}ms)`,
|
|
30
|
+
);
|
|
31
|
+
return average;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("DontFragment + Wildcard Performance (post-refactor)", () => {
|
|
35
|
+
it("should handle large numbers of entities with exclusive dontFragment + wildcard queries efficiently", () => {
|
|
36
|
+
const world = new World();
|
|
37
|
+
const Position = component<{ x: number; y: number }>();
|
|
38
|
+
const ChildOf = component({ dontFragment: true, exclusive: true });
|
|
39
|
+
|
|
40
|
+
const parentCount = 20;
|
|
41
|
+
const parents: EntityId[] = [];
|
|
42
|
+
for (let i = 0; i < parentCount; i++) {
|
|
43
|
+
parents.push(world.new() as EntityId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entityCount = 10_000;
|
|
47
|
+
const entities: EntityId[] = [];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < entityCount; i++) {
|
|
50
|
+
const e = world.new();
|
|
51
|
+
entities.push(e);
|
|
52
|
+
world.set(e, Position, { x: i, y: i });
|
|
53
|
+
const parent = parents[i % parentCount]!;
|
|
54
|
+
world.set(e, relation(ChildOf, parent));
|
|
55
|
+
}
|
|
56
|
+
world.sync();
|
|
57
|
+
|
|
58
|
+
const wildcard = relation(ChildOf, "*");
|
|
59
|
+
const q = world.createQuery([Position, wildcard]);
|
|
60
|
+
|
|
61
|
+
const avg = benchmark("10k entities: wildcard query over exclusive dontFragment", 2, 6, () => {
|
|
62
|
+
let count = 0;
|
|
63
|
+
q.forEach([Position, wildcard], (_entity, _pos, rels) => {
|
|
64
|
+
count += rels.length; // force materialization
|
|
65
|
+
});
|
|
66
|
+
expect(count).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
q.dispose();
|
|
70
|
+
|
|
71
|
+
// These numbers will be tuned after the implementation stabilizes.
|
|
72
|
+
// The goal is to verify we did not regress the hot wildcard + dontFragment path.
|
|
73
|
+
expect(avg).toBeLessThan(50); // generous upper bound post-refactor
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should support frequent exclusive dontFragment flips without leaking relations", () => {
|
|
77
|
+
const world = new World();
|
|
78
|
+
const ChildOf = component({ dontFragment: true, exclusive: true });
|
|
79
|
+
|
|
80
|
+
const parentA = world.new();
|
|
81
|
+
const parentB = world.new();
|
|
82
|
+
|
|
83
|
+
const entityCount = 2000;
|
|
84
|
+
const entities: EntityId[] = [];
|
|
85
|
+
for (let i = 0; i < entityCount; i++) {
|
|
86
|
+
const e = world.new();
|
|
87
|
+
entities.push(e);
|
|
88
|
+
world.set(e, relation(ChildOf, parentA));
|
|
89
|
+
}
|
|
90
|
+
world.sync();
|
|
91
|
+
|
|
92
|
+
// Flip many times
|
|
93
|
+
const flipAvg = benchmark("2k entities: exclusive dontFragment flip (100 rounds)", 1, 3, (round) => {
|
|
94
|
+
const target = round % 2 === 0 ? parentB : parentA;
|
|
95
|
+
for (const e of entities) {
|
|
96
|
+
world.set(e, relation(ChildOf, target));
|
|
97
|
+
}
|
|
98
|
+
world.sync();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const wildcardCount = world.query([relation(ChildOf, "*")]).length;
|
|
102
|
+
expect(wildcardCount).toBe(entityCount);
|
|
103
|
+
|
|
104
|
+
// Should stay fast even after many flips (no quadratic degradation)
|
|
105
|
+
expect(flipAvg).toBeLessThan(120);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { Archetype } from "../../archetype/archetype";
|
|
3
|
+
import { DontFragmentStoreImpl } from "../../archetype/store";
|
|
3
4
|
import type { ComponentId, EntityId } from "../../entity";
|
|
4
5
|
import { relation } from "../../entity";
|
|
5
6
|
import { matchesComponentTypes, matchesFilter, type QueryFilter } from "../../query/filter";
|
|
@@ -10,8 +11,8 @@ const velocityComponent = 2 as ComponentId<{ dx: number; dy: number }>;
|
|
|
10
11
|
const healthComponent = 3 as ComponentId<{ value: number }>;
|
|
11
12
|
const relationComponent = 4 as ComponentId<{ strength: number }>;
|
|
12
13
|
|
|
13
|
-
// Helper function to create a
|
|
14
|
-
const createDontFragmentRelations = () => new
|
|
14
|
+
// Helper function to create a real DontFragmentStore for testing.
|
|
15
|
+
const createDontFragmentRelations = () => new DontFragmentStoreImpl();
|
|
15
16
|
|
|
16
17
|
describe("Query Filter Functions", () => {
|
|
17
18
|
describe("matchesComponentTypes", () => {
|