aiecsjs 0.4.0 → 0.4.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/README_ZHTW.md +3 -3
- package/api.json +1 -1
- package/dist/{chunk-RHH5JA74.cjs → chunk-2CF2MNMH.cjs} +2 -2
- package/dist/{chunk-RHH5JA74.cjs.map → chunk-2CF2MNMH.cjs.map} +1 -1
- package/dist/{chunk-SJDWI3OZ.cjs → chunk-7NNM4PGP.cjs} +2 -2
- package/dist/{chunk-SJDWI3OZ.cjs.map → chunk-7NNM4PGP.cjs.map} +1 -1
- package/dist/{chunk-CTESP3XL.js → chunk-AW254W5F.js} +2 -2
- package/dist/{chunk-CTESP3XL.js.map → chunk-AW254W5F.js.map} +1 -1
- package/dist/{chunk-B777DQR7.cjs → chunk-XU3BVVCJ.cjs} +2 -2
- package/dist/{chunk-B777DQR7.cjs.map → chunk-XU3BVVCJ.cjs.map} +1 -1
- package/dist/{chunk-F7KNZ27O.js → chunk-YAHZHZNR.js} +2 -2
- package/dist/{chunk-F7KNZ27O.js.map → chunk-YAHZHZNR.js.map} +1 -1
- package/dist/{chunk-SQWZUC2Q.js → chunk-ZNLOJVCV.js} +2 -2
- package/dist/{chunk-SQWZUC2Q.js.map → chunk-ZNLOJVCV.js.map} +1 -1
- package/dist/commands.cjs +1 -1
- package/dist/commands.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/observers.cjs +1 -1
- package/dist/observers.js +1 -1
- package/dist/relations.cjs +1 -1
- package/dist/relations.js +1 -1
- package/dist/serialize.cjs +1 -1
- package/dist/serialize.js +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.js +1 -1
- package/llms-full.txt +13 -4
- package/package.json +5 -8
- package/CHANGELOG.md +0 -292
- package/STABILITY.md +0 -156
- package/STABILITY_ZHTW.md +0 -155
- package/docs/MIGRATION.md +0 -252
- package/docs/MIGRATION_ZHTW.md +0 -252
package/docs/MIGRATION.md
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
# Migration Guide
|
|
2
|
-
|
|
3
|
-
[English](MIGRATION.md) | [繁體中文](MIGRATION_ZHTW.md)
|
|
4
|
-
|
|
5
|
-
Concrete name-mapping tables and mental-model notes for switching to `aiecsjs` from other JavaScript ECS libraries.
|
|
6
|
-
|
|
7
|
-
## From bitECS 0.4
|
|
8
|
-
|
|
9
|
-
bitECS and aiecsjs share the most DNA: both are functional, both use TypedArray columns, both compose systems with `pipe`. The differences are real but small.
|
|
10
|
-
|
|
11
|
-
### Name mappings
|
|
12
|
-
|
|
13
|
-
| bitECS 0.4 | aiecsjs 0.1 |
|
|
14
|
-
|---|---|
|
|
15
|
-
| `createWorld()` | `createWorld()` |
|
|
16
|
-
| `defineComponent({ x: Types.f32 })` | `defineComponent({ x: Types.f32 })` |
|
|
17
|
-
| `addComponent(world, Comp, eid)` | `addComponent(world, eid, Comp, init?)` ← **arg order!** |
|
|
18
|
-
| `removeComponent(world, Comp, eid)` | `removeComponent(world, eid, Comp)` |
|
|
19
|
-
| `hasComponent(world, Comp, eid)` | `hasComponent(world, eid, Comp)` |
|
|
20
|
-
| `addEntity(world)` | `createEntity(world)` |
|
|
21
|
-
| `removeEntity(world, eid)` | `destroyEntity(world, eid)` |
|
|
22
|
-
| `defineQuery([Comp])(world)` | `forEachEntity(world, defineQuery([Comp]), fn)` |
|
|
23
|
-
| `enterQuery(query)` | `enterQuery(defineQuery([...]))` (no `world` arg) |
|
|
24
|
-
| `exitQuery(query)` | `exitQuery(defineQuery([...]))` |
|
|
25
|
-
| `Not(Comp)` | `defineQuery({ all: [...], none: [Comp] })` |
|
|
26
|
-
| `pipe(s1, s2)(world)` | `pipe(s1, s2)(world, ctx)` (ctx threaded through) |
|
|
27
|
-
| `defineSerializer(...)` | `createDeltaSerializer(world, { components })` |
|
|
28
|
-
| `createRelation(...)` | `defineRelation(...)` (target 0.2) |
|
|
29
|
-
| `withVersioning(bits)` | `createWorld({ indexBits, generationBits })` |
|
|
30
|
-
| `observe(world, query, ...)` | `observe(world, query, event, handler)` |
|
|
31
|
-
|
|
32
|
-
### Mental shifts
|
|
33
|
-
|
|
34
|
-
**Storage model.** bitECS uses per-component SparseSet + bitmask. aiecsjs uses archetype tables. The performance characteristics differ:
|
|
35
|
-
|
|
36
|
-
- Adding/removing a tag every frame is **cheaper in bitECS** (sparse set has O(1) toggle).
|
|
37
|
-
- Iterating a hot query over 10k entities is **cheaper in aiecsjs** (contiguous archetype columns).
|
|
38
|
-
- For tags you toggle often, store a `boolean` field in a stable component instead of `add`/`removeComponent`.
|
|
39
|
-
|
|
40
|
-
**Argument order.** This is the #1 source of bugs when porting:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
// bitECS:
|
|
44
|
-
addComponent(world, Position, eid)
|
|
45
|
-
|
|
46
|
-
// aiecsjs:
|
|
47
|
-
addComponent(world, eid, Position, { x: 0, y: 0 })
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The aiecsjs order is `(world, eid, component, init)` — entity first because it's the subject of the operation.
|
|
51
|
-
|
|
52
|
-
**Query iteration.** bitECS returns the entity array from the query function call. aiecsjs separates query definition from execution:
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
// bitECS:
|
|
56
|
-
const movers = defineQuery([Position, Velocity])
|
|
57
|
-
const eids = movers(world)
|
|
58
|
-
for (let i = 0; i < eids.length; i++) {
|
|
59
|
-
const e = eids[i]
|
|
60
|
-
Position.x[e] += Velocity.x[e]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// aiecsjs:
|
|
64
|
-
const movers = defineQuery([Position, Velocity])
|
|
65
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
66
|
-
pos.x[e] += vel.x[e]
|
|
67
|
-
})
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The aiecsjs version is shorter and gets typed column views as callback arguments.
|
|
71
|
-
|
|
72
|
-
**Entity versioning.** Both support it. bitECS exposes `withVersioning(bits)`; aiecsjs takes `indexBits` and `generationBits` directly in `WorldOptions`.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
// bitECS:
|
|
76
|
-
const world = createWorld(withVersioning(8))
|
|
77
|
-
|
|
78
|
-
// aiecsjs:
|
|
79
|
-
const world = createWorld({ indexBits: 24, generationBits: 8 })
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Porting a system
|
|
83
|
-
|
|
84
|
-
bitECS:
|
|
85
|
-
```ts
|
|
86
|
-
const movementSystem = (world) => {
|
|
87
|
-
const ents = movers(world)
|
|
88
|
-
for (let i = 0; i < ents.length; i++) {
|
|
89
|
-
const eid = ents[i]
|
|
90
|
-
Position.x[eid] += Velocity.x[eid]
|
|
91
|
-
Position.y[eid] += Velocity.y[eid]
|
|
92
|
-
}
|
|
93
|
-
return world
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
aiecsjs:
|
|
98
|
-
```ts
|
|
99
|
-
const movementSystem = (world, dt = 1) => {
|
|
100
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
101
|
-
pos.x[e] += vel.x[e] * dt
|
|
102
|
-
pos.y[e] += vel.y[e] * dt
|
|
103
|
-
})
|
|
104
|
-
return world
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## From miniplex
|
|
109
|
-
|
|
110
|
-
miniplex is object-oriented and entity-shape-driven; aiecsjs is functional and component-declaration-driven. The port is a small mental adjustment but worthwhile if you need TypedArray performance or multi-thread support.
|
|
111
|
-
|
|
112
|
-
### Name mappings
|
|
113
|
-
|
|
114
|
-
| miniplex 2.0 | aiecsjs 0.1 |
|
|
115
|
-
|---|---|
|
|
116
|
-
| `const world = new World<Entity>()` | `const world = createWorld()` |
|
|
117
|
-
| `world.add({ position: {x, y}, velocity: {x, y} })` | `createEntity` + multiple `addComponent` calls |
|
|
118
|
-
| `world.with('position', 'velocity')` | `defineQuery([Position, Velocity])` |
|
|
119
|
-
| `world.archetype('position', 'velocity')` | `defineQuery([Position, Velocity])` |
|
|
120
|
-
| `query.entities` | `runQuery(world, query)` |
|
|
121
|
-
| `for (const e of query)` | `forEachEntity(world, query, fn)` |
|
|
122
|
-
| `query.onEntityAdded.add(fn)` | `enterQuery(query)` + observe in a system |
|
|
123
|
-
| `query.onEntityRemoved.add(fn)` | `exitQuery(query)` |
|
|
124
|
-
| `world.remove(entity)` | `destroyEntity(world, eid)` |
|
|
125
|
-
| `world.queue.add(...)`, `world.queue.flush()` | `withCommandBuffer(world, cb => cb.create() ...)` |
|
|
126
|
-
| `world.where(predicate)` | (filter inside `forEachEntity` callback) |
|
|
127
|
-
| `<Entities of={query}>` (miniplex-react) | (not yet — see roadmap) |
|
|
128
|
-
|
|
129
|
-
### Mental shifts
|
|
130
|
-
|
|
131
|
-
**Component declaration up front.** In miniplex, components are object property names that exist if you assign them. In aiecsjs, components must be declared:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
// miniplex:
|
|
135
|
-
const e = world.add({ position: { x: 0, y: 0 }, velocity: { x: 1, y: 0 } })
|
|
136
|
-
|
|
137
|
-
// aiecsjs:
|
|
138
|
-
const Position = defineComponent({ x: Types.f32, y: Types.f32 })
|
|
139
|
-
const Velocity = defineComponent({ x: Types.f32, y: Types.f32 })
|
|
140
|
-
const e = createEntity(world)
|
|
141
|
-
addComponent(world, e, Position, { x: 0, y: 0 })
|
|
142
|
-
addComponent(world, e, Velocity, { x: 1, y: 0 })
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The win is TypedArray-backed columns (fast iteration) and SAB-safe storage. The cost is the upfront component declarations.
|
|
146
|
-
|
|
147
|
-
**Heterogeneous references.** If your miniplex entities have `mesh: THREE.Mesh` properties, use `defineObjectComponent` in aiecsjs:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const MeshRef = defineObjectComponent<{ mesh: THREE.Mesh | null }>(() => ({ mesh: null }))
|
|
151
|
-
addComponent(world, e, MeshRef, { mesh: someMesh })
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
But remember: AoS components are main-thread only.
|
|
155
|
-
|
|
156
|
-
**Iteration callbacks vs. iterators.** miniplex's `for (const e of query)` is convenient but allocates the iterator each frame. `forEachEntity(world, query, fn)` is the hot path; reach for `iterQuery` only when you need `for...of` semantics.
|
|
157
|
-
|
|
158
|
-
### Porting a system
|
|
159
|
-
|
|
160
|
-
miniplex:
|
|
161
|
-
```ts
|
|
162
|
-
const movement = (dt: number) => {
|
|
163
|
-
for (const e of world.with('position', 'velocity')) {
|
|
164
|
-
e.position.x += e.velocity.x * dt
|
|
165
|
-
e.position.y += e.velocity.y * dt
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
aiecsjs:
|
|
171
|
-
```ts
|
|
172
|
-
const movers = defineQuery([Position, Velocity])
|
|
173
|
-
const movement = (world, dt) => {
|
|
174
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
175
|
-
pos.x[e] += vel.x[e] * dt
|
|
176
|
-
pos.y[e] += vel.y[e] * dt
|
|
177
|
-
})
|
|
178
|
-
return world
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## From ECSY
|
|
183
|
-
|
|
184
|
-
ECSY is [archived](https://github.com/ecsyjs/ecsy) (April 2025). Migration to aiecsjs is straightforward because both are archetype-style ECS. ECSY's OO ergonomics map cleanly to aiecsjs's functional API.
|
|
185
|
-
|
|
186
|
-
### Name mappings
|
|
187
|
-
|
|
188
|
-
| ECSY | aiecsjs 0.1 |
|
|
189
|
-
|---|---|
|
|
190
|
-
| `class C extends Component { static schema = { x: { type: Types.Number } } }` | `defineComponent({ x: Types.f64 })` |
|
|
191
|
-
| `class Tag extends TagComponent {}` | `defineTag()` |
|
|
192
|
-
| `class S extends System { static queries = { foo: { components: [...] } }; execute(dt) { this.queries.foo.results.forEach(...) } }` | `const fooQ = defineQuery([...])`; `const S = (world, dt) => { forEachEntity(world, fooQ, fn); return world }` |
|
|
193
|
-
| `world.registerComponent(C)` | (implicit on `defineComponent`) |
|
|
194
|
-
| `world.registerSystem(S)` | (none — `pipe` orders systems) |
|
|
195
|
-
| `world.execute(dt, time)` | `tick(world, dt)` where `tick = pipe(S1, S2, ...)` |
|
|
196
|
-
| `world.createEntity()` | `createEntity(world)` |
|
|
197
|
-
| `entity.addComponent(C, data)` | `addComponent(world, eid, C, data)` |
|
|
198
|
-
| `entity.removeComponent(C)` | `removeComponent(world, eid, C)` |
|
|
199
|
-
| `entity.getComponent(C)` | `getComponent(world, eid, C)` |
|
|
200
|
-
| `entity.getMutableComponent(C)` | `getComponent(world, eid, C)` (always mutable in aiecsjs) |
|
|
201
|
-
| `queries.foo.added` | `enterQuery(fooQ)` |
|
|
202
|
-
| `queries.foo.removed` | `exitQuery(fooQ)` |
|
|
203
|
-
| `queries.foo.changed` | (use `onSet` observer or own change tracking) |
|
|
204
|
-
|
|
205
|
-
### Mental shifts
|
|
206
|
-
|
|
207
|
-
**No `class System`.** Systems are functions, not classes. Drop `extends System`, `execute`, and `static queries` — define a query at module top-level and pass it to `forEachEntity`.
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
// ECSY:
|
|
211
|
-
class MovementSystem extends System {
|
|
212
|
-
static queries = { movers: { components: [Position, Velocity] } }
|
|
213
|
-
execute(dt: number) {
|
|
214
|
-
this.queries.movers.results.forEach((e) => {
|
|
215
|
-
const pos = e.getMutableComponent(Position)
|
|
216
|
-
const vel = e.getComponent(Velocity)
|
|
217
|
-
pos.x += vel.x * dt
|
|
218
|
-
pos.y += vel.y * dt
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
world.registerSystem(MovementSystem)
|
|
223
|
-
world.execute(1/60)
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
// aiecsjs:
|
|
228
|
-
const movers = defineQuery([Position, Velocity])
|
|
229
|
-
const movement = (world, dt) => {
|
|
230
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
231
|
-
pos.x[e] += vel.x[e] * dt
|
|
232
|
-
pos.y[e] += vel.y[e] * dt
|
|
233
|
-
})
|
|
234
|
-
return world
|
|
235
|
-
}
|
|
236
|
-
const tick = pipe(movement)
|
|
237
|
-
tick(world, 1/60)
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**SoA columns vs. component instances.** ECSY components are class instances with fields like `pos.x`. aiecsjs SoA components are column maps; you index by entity ID: `pos.x[e]`.
|
|
241
|
-
|
|
242
|
-
**No `priority` or scheduling DSL.** aiecsjs systems run in `pipe()` order. If you depended on ECSY's `priority` for ordering, just write the pipe in the right order.
|
|
243
|
-
|
|
244
|
-
**`Types.Number` → `Types.f64` (or `f32`).** ECSY's numeric type is double-precision; aiecsjs lets you pick the width. Use `f32` for game data, `f64` only if you genuinely need it.
|
|
245
|
-
|
|
246
|
-
## Common pitfalls when migrating (from any library)
|
|
247
|
-
|
|
248
|
-
1. **Forgetting to `pipe(...)` system function** — calling each system manually and forgetting to thread the world reference. Compose with `pipe` once and call `tick(world, ctx)`.
|
|
249
|
-
2. **Calling `defineComponent` inside a system** — components are identity-based and must be module-level constants.
|
|
250
|
-
3. **Caching `getComponent()` return value** — after an entity's archetype changes, the view is stale. Re-fetch each frame.
|
|
251
|
-
4. **Iterating with `for...of` on `runQuery` result** — `runQuery` allocates an array each call. Use `forEachEntity` in hot paths.
|
|
252
|
-
5. **Trying to share AoS components across Workers** — only SoA components live in SharedArrayBuffer. Replace AoS with SoA before going multi-thread.
|
package/docs/MIGRATION_ZHTW.md
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
# 移轉指引
|
|
2
|
-
|
|
3
|
-
[English](MIGRATION.md) | [繁體中文](MIGRATION_ZHTW.md)
|
|
4
|
-
|
|
5
|
-
從其他 JavaScript ECS 函式庫切換到 `aiecsjs` 的具體名稱對照與心態調整筆記。
|
|
6
|
-
|
|
7
|
-
## 從 bitECS 0.4 移轉
|
|
8
|
-
|
|
9
|
-
bitECS 與 aiecsjs 共享最多 DNA:都是函式式、都用 TypedArray 欄位、都用 `pipe` 組合系統。差異雖真實但不大。
|
|
10
|
-
|
|
11
|
-
### 名稱對照
|
|
12
|
-
|
|
13
|
-
| bitECS 0.4 | aiecsjs 0.1 |
|
|
14
|
-
|---|---|
|
|
15
|
-
| `createWorld()` | `createWorld()` |
|
|
16
|
-
| `defineComponent({ x: Types.f32 })` | `defineComponent({ x: Types.f32 })` |
|
|
17
|
-
| `addComponent(world, Comp, eid)` | `addComponent(world, eid, Comp, init?)` ← **參數順序!** |
|
|
18
|
-
| `removeComponent(world, Comp, eid)` | `removeComponent(world, eid, Comp)` |
|
|
19
|
-
| `hasComponent(world, Comp, eid)` | `hasComponent(world, eid, Comp)` |
|
|
20
|
-
| `addEntity(world)` | `createEntity(world)` |
|
|
21
|
-
| `removeEntity(world, eid)` | `destroyEntity(world, eid)` |
|
|
22
|
-
| `defineQuery([Comp])(world)` | `forEachEntity(world, defineQuery([Comp]), fn)` |
|
|
23
|
-
| `enterQuery(query)` | `enterQuery(defineQuery([...]))`(無 `world` 參數) |
|
|
24
|
-
| `exitQuery(query)` | `exitQuery(defineQuery([...]))` |
|
|
25
|
-
| `Not(Comp)` | `defineQuery({ all: [...], none: [Comp] })` |
|
|
26
|
-
| `pipe(s1, s2)(world)` | `pipe(s1, s2)(world, ctx)`(ctx 會被串接) |
|
|
27
|
-
| `defineSerializer(...)` | `createDeltaSerializer(world, { components })` |
|
|
28
|
-
| `createRelation(...)` | `defineRelation(...)`(目標 0.2) |
|
|
29
|
-
| `withVersioning(bits)` | `createWorld({ indexBits, generationBits })` |
|
|
30
|
-
| `observe(world, query, ...)` | `observe(world, query, event, handler)` |
|
|
31
|
-
|
|
32
|
-
### 心態調整
|
|
33
|
-
|
|
34
|
-
**儲存模型。** bitECS 用每元件 SparseSet + bitmask。aiecsjs 用 archetype 表格。效能特性不同:
|
|
35
|
-
|
|
36
|
-
- 每幀 add/remove 一個 tag 在 **bitECS 較便宜**(sparse set O(1) toggle)。
|
|
37
|
-
- 對 1 萬實體做熱查詢迭代在 **aiecsjs 較便宜**(連續 archetype 欄位)。
|
|
38
|
-
- 經常切換的 tag,請改用穩定元件內的 `boolean` 欄位,不要 `add`/`removeComponent`。
|
|
39
|
-
|
|
40
|
-
**參數順序。** 移植時最常出 bug 的點:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
// bitECS:
|
|
44
|
-
addComponent(world, Position, eid)
|
|
45
|
-
|
|
46
|
-
// aiecsjs:
|
|
47
|
-
addComponent(world, eid, Position, { x: 0, y: 0 })
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
aiecsjs 順序為 `(world, eid, component, init)` — 實體在前,因為它才是操作主體。
|
|
51
|
-
|
|
52
|
-
**查詢迭代。** bitECS 是 query 呼叫直接回傳實體陣列。aiecsjs 將定義與執行分離:
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
// bitECS:
|
|
56
|
-
const movers = defineQuery([Position, Velocity])
|
|
57
|
-
const eids = movers(world)
|
|
58
|
-
for (let i = 0; i < eids.length; i++) {
|
|
59
|
-
const e = eids[i]
|
|
60
|
-
Position.x[e] += Velocity.x[e]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// aiecsjs:
|
|
64
|
-
const movers = defineQuery([Position, Velocity])
|
|
65
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
66
|
-
pos.x[e] += vel.x[e]
|
|
67
|
-
})
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
aiecsjs 寫法較短,且 callback 內可取得已對型的欄位 view。
|
|
71
|
-
|
|
72
|
-
**實體版本控制。** 兩者都支援。bitECS 透過 `withVersioning(bits)`;aiecsjs 直接在 `WorldOptions` 中取 `indexBits` 與 `generationBits`。
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
// bitECS:
|
|
76
|
-
const world = createWorld(withVersioning(8))
|
|
77
|
-
|
|
78
|
-
// aiecsjs:
|
|
79
|
-
const world = createWorld({ indexBits: 24, generationBits: 8 })
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 系統移植範例
|
|
83
|
-
|
|
84
|
-
bitECS:
|
|
85
|
-
```ts
|
|
86
|
-
const movementSystem = (world) => {
|
|
87
|
-
const ents = movers(world)
|
|
88
|
-
for (let i = 0; i < ents.length; i++) {
|
|
89
|
-
const eid = ents[i]
|
|
90
|
-
Position.x[eid] += Velocity.x[eid]
|
|
91
|
-
Position.y[eid] += Velocity.y[eid]
|
|
92
|
-
}
|
|
93
|
-
return world
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
aiecsjs:
|
|
98
|
-
```ts
|
|
99
|
-
const movementSystem = (world, dt = 1) => {
|
|
100
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
101
|
-
pos.x[e] += vel.x[e] * dt
|
|
102
|
-
pos.y[e] += vel.y[e] * dt
|
|
103
|
-
})
|
|
104
|
-
return world
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## 從 miniplex 移轉
|
|
109
|
-
|
|
110
|
-
miniplex 是 OO 且以實體 shape 為主;aiecsjs 是函式式且以元件宣告為主。移植需要一點心態調整,但若你需要 TypedArray 效能或多執行緒支援就值得。
|
|
111
|
-
|
|
112
|
-
### 名稱對照
|
|
113
|
-
|
|
114
|
-
| miniplex 2.0 | aiecsjs 0.1 |
|
|
115
|
-
|---|---|
|
|
116
|
-
| `const world = new World<Entity>()` | `const world = createWorld()` |
|
|
117
|
-
| `world.add({ position: {x, y}, velocity: {x, y} })` | `createEntity` + 多次 `addComponent` |
|
|
118
|
-
| `world.with('position', 'velocity')` | `defineQuery([Position, Velocity])` |
|
|
119
|
-
| `world.archetype('position', 'velocity')` | `defineQuery([Position, Velocity])` |
|
|
120
|
-
| `query.entities` | `runQuery(world, query)` |
|
|
121
|
-
| `for (const e of query)` | `forEachEntity(world, query, fn)` |
|
|
122
|
-
| `query.onEntityAdded.add(fn)` | `enterQuery(query)` + 在系統內 observe |
|
|
123
|
-
| `query.onEntityRemoved.add(fn)` | `exitQuery(query)` |
|
|
124
|
-
| `world.remove(entity)` | `destroyEntity(world, eid)` |
|
|
125
|
-
| `world.queue.add(...)`、`world.queue.flush()` | `withCommandBuffer(world, cb => cb.create() ...)` |
|
|
126
|
-
| `world.where(predicate)` | (在 `forEachEntity` callback 內過濾) |
|
|
127
|
-
| `<Entities of={query}>`(miniplex-react) | (尚未支援,見 roadmap) |
|
|
128
|
-
|
|
129
|
-
### 心態調整
|
|
130
|
-
|
|
131
|
-
**元件需預先宣告。** miniplex 中,元件是你賦值就存在的物件屬性名。aiecsjs 中,元件必須先宣告:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
// miniplex:
|
|
135
|
-
const e = world.add({ position: { x: 0, y: 0 }, velocity: { x: 1, y: 0 } })
|
|
136
|
-
|
|
137
|
-
// aiecsjs:
|
|
138
|
-
const Position = defineComponent({ x: Types.f32, y: Types.f32 })
|
|
139
|
-
const Velocity = defineComponent({ x: Types.f32, y: Types.f32 })
|
|
140
|
-
const e = createEntity(world)
|
|
141
|
-
addComponent(world, e, Position, { x: 0, y: 0 })
|
|
142
|
-
addComponent(world, e, Velocity, { x: 1, y: 0 })
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
收益是 TypedArray 欄位(快速迭代)與 SAB 安全儲存。代價是元件需要事先宣告。
|
|
146
|
-
|
|
147
|
-
**異質參考。** 若 miniplex 實體有 `mesh: THREE.Mesh` 屬性,aiecsjs 中請用 `defineObjectComponent`:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const MeshRef = defineObjectComponent<{ mesh: THREE.Mesh | null }>(() => ({ mesh: null }))
|
|
151
|
-
addComponent(world, e, MeshRef, { mesh: someMesh })
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
但記住:AoS 元件僅限主執行緒。
|
|
155
|
-
|
|
156
|
-
**iteration callback vs. iterator。** miniplex 的 `for (const e of query)` 方便但每幀分配 iterator。`forEachEntity(world, query, fn)` 是熱路徑;只有需要 `for...of` 語義時才使用 `iterQuery`。
|
|
157
|
-
|
|
158
|
-
### 系統移植範例
|
|
159
|
-
|
|
160
|
-
miniplex:
|
|
161
|
-
```ts
|
|
162
|
-
const movement = (dt: number) => {
|
|
163
|
-
for (const e of world.with('position', 'velocity')) {
|
|
164
|
-
e.position.x += e.velocity.x * dt
|
|
165
|
-
e.position.y += e.velocity.y * dt
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
aiecsjs:
|
|
171
|
-
```ts
|
|
172
|
-
const movers = defineQuery([Position, Velocity])
|
|
173
|
-
const movement = (world, dt) => {
|
|
174
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
175
|
-
pos.x[e] += vel.x[e] * dt
|
|
176
|
-
pos.y[e] += vel.y[e] * dt
|
|
177
|
-
})
|
|
178
|
-
return world
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## 從 ECSY 移轉
|
|
183
|
-
|
|
184
|
-
ECSY 已[封存](https://github.com/ecsyjs/ecsy)(2025 年 4 月)。移轉到 aiecsjs 並不困難,因為兩者皆為 archetype-style。ECSY 的 OO 慣用可直接對應到 aiecsjs 的函式式 API。
|
|
185
|
-
|
|
186
|
-
### 名稱對照
|
|
187
|
-
|
|
188
|
-
| ECSY | aiecsjs 0.1 |
|
|
189
|
-
|---|---|
|
|
190
|
-
| `class C extends Component { static schema = { x: { type: Types.Number } } }` | `defineComponent({ x: Types.f64 })` |
|
|
191
|
-
| `class Tag extends TagComponent {}` | `defineTag()` |
|
|
192
|
-
| `class S extends System { static queries = { foo: { components: [...] } }; execute(dt) { this.queries.foo.results.forEach(...) } }` | `const fooQ = defineQuery([...])`;`const S = (world, dt) => { forEachEntity(world, fooQ, fn); return world }` |
|
|
193
|
-
| `world.registerComponent(C)` | (在 `defineComponent` 時自動完成) |
|
|
194
|
-
| `world.registerSystem(S)` | (無 — `pipe` 決定順序) |
|
|
195
|
-
| `world.execute(dt, time)` | `tick(world, dt)`,其中 `tick = pipe(S1, S2, ...)` |
|
|
196
|
-
| `world.createEntity()` | `createEntity(world)` |
|
|
197
|
-
| `entity.addComponent(C, data)` | `addComponent(world, eid, C, data)` |
|
|
198
|
-
| `entity.removeComponent(C)` | `removeComponent(world, eid, C)` |
|
|
199
|
-
| `entity.getComponent(C)` | `getComponent(world, eid, C)` |
|
|
200
|
-
| `entity.getMutableComponent(C)` | `getComponent(world, eid, C)`(aiecsjs 永遠是可變的) |
|
|
201
|
-
| `queries.foo.added` | `enterQuery(fooQ)` |
|
|
202
|
-
| `queries.foo.removed` | `exitQuery(fooQ)` |
|
|
203
|
-
| `queries.foo.changed` | (用 `onSet` observer 或自行追蹤) |
|
|
204
|
-
|
|
205
|
-
### 心態調整
|
|
206
|
-
|
|
207
|
-
**不用寫 `class System`。** 系統是函式不是 class。捨棄 `extends System`、`execute`、`static queries` — 在模組頂層定義 query,並傳給 `forEachEntity`。
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
// ECSY:
|
|
211
|
-
class MovementSystem extends System {
|
|
212
|
-
static queries = { movers: { components: [Position, Velocity] } }
|
|
213
|
-
execute(dt: number) {
|
|
214
|
-
this.queries.movers.results.forEach((e) => {
|
|
215
|
-
const pos = e.getMutableComponent(Position)
|
|
216
|
-
const vel = e.getComponent(Velocity)
|
|
217
|
-
pos.x += vel.x * dt
|
|
218
|
-
pos.y += vel.y * dt
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
world.registerSystem(MovementSystem)
|
|
223
|
-
world.execute(1/60)
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
// aiecsjs:
|
|
228
|
-
const movers = defineQuery([Position, Velocity])
|
|
229
|
-
const movement = (world, dt) => {
|
|
230
|
-
forEachEntity(world, movers, (e, pos, vel) => {
|
|
231
|
-
pos.x[e] += vel.x[e] * dt
|
|
232
|
-
pos.y[e] += vel.y[e] * dt
|
|
233
|
-
})
|
|
234
|
-
return world
|
|
235
|
-
}
|
|
236
|
-
const tick = pipe(movement)
|
|
237
|
-
tick(world, 1/60)
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**SoA 欄位 vs. 元件實例。** ECSY 元件是 class 實例,欄位如 `pos.x`。aiecsjs SoA 元件是欄位對應表;以實體 ID 索引:`pos.x[e]`。
|
|
241
|
-
|
|
242
|
-
**沒有 `priority` 或排程 DSL。** aiecsjs 系統以 `pipe()` 順序執行。若你依賴 ECSY 的 `priority` 排序,直接把 pipe 順序寫對即可。
|
|
243
|
-
|
|
244
|
-
**`Types.Number` → `Types.f64`(或 `f32`)。** ECSY 的數值型別為雙精度;aiecsjs 讓你選擇寬度。一般遊戲資料用 `f32`,真的需要才用 `f64`。
|
|
245
|
-
|
|
246
|
-
## 移轉常見陷阱(任一函式庫)
|
|
247
|
-
|
|
248
|
-
1. **忘了用 `pipe(...)` 串系統** — 手動逐一呼叫各系統卻沒把 world 參考帶過去。請用 `pipe` 串好,再 `tick(world, ctx)`。
|
|
249
|
-
2. **在系統內呼叫 `defineComponent`** — 元件是 identity-based,必須是 module-level 常數。
|
|
250
|
-
3. **快取 `getComponent()` 回傳值** — 實體 archetype 改變後該 view 已失效。每幀請重新取得。
|
|
251
|
-
4. **對 `runQuery` 結果用 `for...of` 迭代** — `runQuery` 每次呼叫都分配陣列。熱路徑請改用 `forEachEntity`。
|
|
252
|
-
5. **嘗試跨 Worker 共享 AoS 元件** — 僅 SoA 元件能存於 SharedArrayBuffer。多執行緒前請先把 AoS 換成 SoA。
|