koota 0.4.2 → 0.4.4
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 +234 -190
- package/dist/{chunk-IOZKRHDY.js → chunk-6HKEF2RH.js} +863 -743
- package/dist/index.cjs +549 -426
- package/dist/index.d.cts +33 -25
- package/dist/index.d.ts +33 -25
- package/dist/index.js +9 -3
- package/dist/react.cjs +541 -441
- package/dist/react.d.cts +12 -11
- package/dist/react.d.ts +12 -11
- package/dist/react.js +37 -42
- package/dist/{world-DpkWjNZp.d.cts → world-DBLDAm85.d.cts} +10 -6
- package/dist/{world-DpkWjNZp.d.ts → world-DBLDAm85.d.ts} +10 -6
- package/package.json +10 -6
- package/react/index.cjs +541 -441
- package/react/index.d.cts +12 -11
- package/react/index.d.ts +12 -11
- package/react/index.js +37 -42
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Koota is an ECS-based state management library optimized for real-time apps, gam
|
|
|
7
7
|
```bash
|
|
8
8
|
npm i koota
|
|
9
9
|
```
|
|
10
|
+
|
|
10
11
|
👉 [Try the starter template](https://github.com/Ctrlmonster/r3f-koota-starter)
|
|
11
12
|
|
|
12
13
|
### First, define traits
|
|
@@ -14,18 +15,18 @@ npm i koota
|
|
|
14
15
|
Traits are the building blocks of your state. They represent slices of data with specific meanings.
|
|
15
16
|
|
|
16
17
|
```js
|
|
17
|
-
import { trait } from 'koota'
|
|
18
|
+
import { trait } from 'koota'
|
|
18
19
|
|
|
19
20
|
// Basic trait with default values
|
|
20
|
-
const Position = trait({ x: 0, y: 0 })
|
|
21
|
-
const Velocity = trait({ x: 0, y: 0 })
|
|
21
|
+
const Position = trait({ x: 0, y: 0 })
|
|
22
|
+
const Velocity = trait({ x: 0, y: 0 })
|
|
22
23
|
|
|
23
24
|
// Trait with a callback for initial value
|
|
24
25
|
// ⚠️ Must be an object
|
|
25
|
-
const Mesh = trait(() => new THREE.Mesh())
|
|
26
|
+
const Mesh = trait(() => new THREE.Mesh())
|
|
26
27
|
|
|
27
28
|
// Tag trait (no data)
|
|
28
|
-
const IsActive = trait()
|
|
29
|
+
const IsActive = trait()
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
### Spawn entities
|
|
@@ -33,13 +34,13 @@ const IsActive = trait();
|
|
|
33
34
|
Entities are spawned in a world. By adding traits to an entity they gain content.
|
|
34
35
|
|
|
35
36
|
```js
|
|
36
|
-
import { createWorld } from 'koota'
|
|
37
|
+
import { createWorld } from 'koota'
|
|
37
38
|
|
|
38
|
-
const world = createWorld()
|
|
39
|
+
const world = createWorld()
|
|
39
40
|
|
|
40
|
-
const player = world.spawn(Position, Velocity)
|
|
41
|
+
const player = world.spawn(Position, Velocity)
|
|
41
42
|
// Initial values can be passed in to the trait by using it as a function
|
|
42
|
-
const goblin = world.spawn(Position({ x: 10, y: 10 }), Velocity, Mesh)
|
|
43
|
+
const goblin = world.spawn(Position({ x: 10, y: 10 }), Velocity, Mesh)
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
### Query and update data
|
|
@@ -49,9 +50,9 @@ Queries fetch entities sharing traits (archetypes). Use them to batch update ent
|
|
|
49
50
|
```js
|
|
50
51
|
// Run this in a loop
|
|
51
52
|
world.query(Position, Velocity).updateEach(([position, velocity]) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
53
|
+
position.x += velocity.x * delta
|
|
54
|
+
position.y += velocity.y * delta
|
|
55
|
+
})
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
### Use in your React components
|
|
@@ -95,44 +96,44 @@ Use actions to safely modify Koota from inside of React in either effects or eve
|
|
|
95
96
|
|
|
96
97
|
```js
|
|
97
98
|
import { createActions } from 'koota'
|
|
98
|
-
import { useActions } from 'koota/react'
|
|
99
|
+
import { useActions } from 'koota/react'
|
|
99
100
|
|
|
100
101
|
const actions = createActions((world) => ({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}))
|
|
102
|
+
spawnShip: (position) => world.spawn(Position(position), Velocity),
|
|
103
|
+
destroyAllShips: (world) => {
|
|
104
|
+
world.query(Position, Velocity).forEach((entity) => {
|
|
105
|
+
entity.destroy()
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
}))
|
|
108
109
|
|
|
109
110
|
function DoomButton() {
|
|
110
|
-
|
|
111
|
+
const { spawnShip, destroyAllShips } = useActions(actions)
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
// Spawn three ships on mount
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
spawnShip({ x: 0, y: 1 })
|
|
116
|
+
spawnShip({ x: 1, y: 0 })
|
|
117
|
+
spawnShip({ x: 1, y: 1 })
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Destroy all ships during cleanup
|
|
120
|
+
return () => drestroyAllShips()
|
|
121
|
+
}, [])
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
// And destroy all ships on click!
|
|
124
|
+
return <button onClick={destroyAllShips}>Boom!</button>
|
|
124
125
|
}
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
Or access world directly and use it.
|
|
128
129
|
|
|
129
130
|
```js
|
|
130
|
-
const world = useWorld()
|
|
131
|
+
const world = useWorld()
|
|
131
132
|
|
|
132
133
|
useEffect(() => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
})
|
|
134
|
+
const entity = world.spawn(Velocity, Position)
|
|
135
|
+
return () => entity.destroy()
|
|
136
|
+
})
|
|
136
137
|
```
|
|
137
138
|
|
|
138
139
|
## Advanced
|
|
@@ -142,12 +143,12 @@ useEffect(() => {
|
|
|
142
143
|
Koota supports relationships between entities using the `relation` function. Relationships allow you to create connections between entities and query them efficiently.
|
|
143
144
|
|
|
144
145
|
```js
|
|
145
|
-
const ChildOf = relation()
|
|
146
|
+
const ChildOf = relation()
|
|
146
147
|
|
|
147
|
-
const parent = world.spawn()
|
|
148
|
-
const child = world.spawn(ChildOf(parent))
|
|
148
|
+
const parent = world.spawn()
|
|
149
|
+
const child = world.spawn(ChildOf(parent))
|
|
149
150
|
|
|
150
|
-
const entity = world.queryFirst(ChildOf(parent))
|
|
151
|
+
const entity = world.queryFirst(ChildOf(parent)) // Returns child
|
|
151
152
|
```
|
|
152
153
|
|
|
153
154
|
#### With data
|
|
@@ -155,12 +156,12 @@ const entity = world.queryFirst(ChildOf(parent)); // Returns child
|
|
|
155
156
|
Relationships can contain data like any trait.
|
|
156
157
|
|
|
157
158
|
```js
|
|
158
|
-
const Contains = relation({ store: { amount: 0 } })
|
|
159
|
+
const Contains = relation({ store: { amount: 0 } })
|
|
159
160
|
|
|
160
|
-
const inventory = world.spawn()
|
|
161
|
-
const gold = world.spawn()
|
|
162
|
-
inventory.add(Contains(gold))
|
|
163
|
-
inventory.set(Contains(gold), { amount: 10 })
|
|
161
|
+
const inventory = world.spawn()
|
|
162
|
+
const gold = world.spawn()
|
|
163
|
+
inventory.add(Contains(gold))
|
|
164
|
+
inventory.set(Contains(gold), { amount: 10 })
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
#### Auto remove target
|
|
@@ -168,15 +169,15 @@ inventory.set(Contains(gold), { amount: 10 });
|
|
|
168
169
|
Relations can automatically remove target entities and their descendants.
|
|
169
170
|
|
|
170
171
|
```js
|
|
171
|
-
const ChildOf = relation({ autoRemoveTarget: true })
|
|
172
|
+
const ChildOf = relation({ autoRemoveTarget: true })
|
|
172
173
|
|
|
173
|
-
const parent = world.spawn()
|
|
174
|
-
const child = world.spawn(ChildOf(parent))
|
|
175
|
-
const grandchild = world.spawn(ChildOf(child))
|
|
174
|
+
const parent = world.spawn()
|
|
175
|
+
const child = world.spawn(ChildOf(parent))
|
|
176
|
+
const grandchild = world.spawn(ChildOf(child))
|
|
176
177
|
|
|
177
|
-
parent.destroy()
|
|
178
|
+
parent.destroy()
|
|
178
179
|
|
|
179
|
-
world.has(child)
|
|
180
|
+
world.has(child) // False, the child and grandchild are destroyed too
|
|
180
181
|
```
|
|
181
182
|
|
|
182
183
|
#### Exclusive Relationships
|
|
@@ -184,17 +185,17 @@ world.has(child); // False, the child and grandchild are destroyed too
|
|
|
184
185
|
Exclusive relationships ensure each entity can only have one target.
|
|
185
186
|
|
|
186
187
|
```js
|
|
187
|
-
const Targeting = relation({ exclusive: true })
|
|
188
|
+
const Targeting = relation({ exclusive: true })
|
|
188
189
|
|
|
189
|
-
const hero = world.spawn()
|
|
190
|
-
const rat = world.spawn()
|
|
191
|
-
const goblin = world.spawn()
|
|
190
|
+
const hero = world.spawn()
|
|
191
|
+
const rat = world.spawn()
|
|
192
|
+
const goblin = world.spawn()
|
|
192
193
|
|
|
193
|
-
hero.add(Targeting(rat))
|
|
194
|
-
hero.add(Targeting(goblin))
|
|
194
|
+
hero.add(Targeting(rat))
|
|
195
|
+
hero.add(Targeting(goblin))
|
|
195
196
|
|
|
196
|
-
hero.has(Targeting(rat))
|
|
197
|
-
hero.has(Targeting(goblin))
|
|
197
|
+
hero.has(Targeting(rat)) // False
|
|
198
|
+
hero.has(Targeting(goblin)) // True
|
|
198
199
|
```
|
|
199
200
|
|
|
200
201
|
#### Querying relationships
|
|
@@ -202,18 +203,18 @@ hero.has(Targeting(goblin)); // True
|
|
|
202
203
|
Relationships can be queried with specific targets, wildcard targets using `*` and even inverted wildcard searches with `Wildcard` to get all entities with a relationship targeting another entity.
|
|
203
204
|
|
|
204
205
|
```js
|
|
205
|
-
const gold = world.spawn()
|
|
206
|
-
const silver = world.spawn()
|
|
207
|
-
const inventory = world.spawn(Contains(gold), Contains(silver))
|
|
206
|
+
const gold = world.spawn()
|
|
207
|
+
const silver = world.spawn()
|
|
208
|
+
const inventory = world.spawn(Contains(gold), Contains(silver))
|
|
208
209
|
|
|
209
|
-
const targets = inventory.targetsFor(Contains)
|
|
210
|
+
const targets = inventory.targetsFor(Contains) // Returns [gold, silver]
|
|
210
211
|
|
|
211
|
-
const chest = world.spawn(Contains(gold))
|
|
212
|
-
const dwarf = world.spawn(Desires(gold))
|
|
212
|
+
const chest = world.spawn(Contains(gold))
|
|
213
|
+
const dwarf = world.spawn(Desires(gold))
|
|
213
214
|
|
|
214
|
-
const constainsSilver = world.query(Contains(silver))
|
|
215
|
-
const containsAnything = world.query(Contains('*'))
|
|
216
|
-
const relatesToGold = world.query(Wildcard(gold))
|
|
215
|
+
const constainsSilver = world.query(Contains(silver)) // Returns [inventory]
|
|
216
|
+
const containsAnything = world.query(Contains('*')) // Returns [inventory, chest]
|
|
217
|
+
const relatesToGold = world.query(Wildcard(gold)) // Returns [inventory, chest, dwarf]
|
|
217
218
|
```
|
|
218
219
|
|
|
219
220
|
### Query modifiers
|
|
@@ -225,9 +226,9 @@ Modifiers are used to filter query results enabling powerful patterns. All modif
|
|
|
225
226
|
The `Not` modifier excludes entities that have specific traits from the query results.
|
|
226
227
|
|
|
227
228
|
```js
|
|
228
|
-
import { Not } from 'koota'
|
|
229
|
+
import { Not } from 'koota'
|
|
229
230
|
|
|
230
|
-
const staticEntities = world.query(Position, Not(Velocity))
|
|
231
|
+
const staticEntities = world.query(Position, Not(Velocity))
|
|
231
232
|
```
|
|
232
233
|
|
|
233
234
|
#### Or
|
|
@@ -235,9 +236,9 @@ const staticEntities = world.query(Position, Not(Velocity));
|
|
|
235
236
|
By default all query parameters are combined with logical AND. The `Or` modifier enables using logical OR instead.
|
|
236
237
|
|
|
237
238
|
```js
|
|
238
|
-
import { Or } from 'koota'
|
|
239
|
+
import { Or } from 'koota'
|
|
239
240
|
|
|
240
|
-
const movingOrVisible = world.query(Or(Velocity, Renderable))
|
|
241
|
+
const movingOrVisible = world.query(Or(Velocity, Renderable))
|
|
241
242
|
```
|
|
242
243
|
|
|
243
244
|
#### Added
|
|
@@ -245,12 +246,12 @@ const movingOrVisible = world.query(Or(Velocity, Renderable));
|
|
|
245
246
|
The `Added` modifier tracks all entities that have added the specified traits since the last time the query was run. A new instance of the modifier must be created for tracking to be unique.
|
|
246
247
|
|
|
247
248
|
```js
|
|
248
|
-
import { createAdded } from 'koota'
|
|
249
|
+
import { createAdded } from 'koota'
|
|
249
250
|
|
|
250
|
-
const Added = createAdded()
|
|
251
|
+
const Added = createAdded()
|
|
251
252
|
|
|
252
253
|
// This query will return entities that have just added the Position trait
|
|
253
|
-
const newPositions = world.query(Added(Position))
|
|
254
|
+
const newPositions = world.query(Added(Position))
|
|
254
255
|
|
|
255
256
|
// After running the query, the Added modifier is reset
|
|
256
257
|
```
|
|
@@ -260,12 +261,12 @@ const newPositions = world.query(Added(Position));
|
|
|
260
261
|
The `Removed` modifier tracks all entities that have removed the specified traits since the last time the query was run. This includes entities that have been destroyed. A new instance of the modifier must be created for tracking to be unique.
|
|
261
262
|
|
|
262
263
|
```js
|
|
263
|
-
import { createRemoved } from 'koota'
|
|
264
|
+
import { createRemoved } from 'koota'
|
|
264
265
|
|
|
265
|
-
const Removed = createRemoved()
|
|
266
|
+
const Removed = createRemoved()
|
|
266
267
|
|
|
267
268
|
// This query will return entities that have just removed the Velocity trait
|
|
268
|
-
const stoppedEntities = world.query(Removed(Velocity))
|
|
269
|
+
const stoppedEntities = world.query(Removed(Velocity))
|
|
269
270
|
|
|
270
271
|
// After running the query, the Removed modifier is reset
|
|
271
272
|
```
|
|
@@ -275,12 +276,12 @@ const stoppedEntities = world.query(Removed(Velocity));
|
|
|
275
276
|
The `Changed` modifier tracks all entities that have had the specified traits values change since the last time the query was run. A new instance of the modifier must be created for tracking to be unique.
|
|
276
277
|
|
|
277
278
|
```js
|
|
278
|
-
import { createChanged } from 'koota'
|
|
279
|
+
import { createChanged } from 'koota'
|
|
279
280
|
|
|
280
|
-
const Changed = createChanged()
|
|
281
|
+
const Changed = createChanged()
|
|
281
282
|
|
|
282
283
|
// This query will return entities whose Position has changed
|
|
283
|
-
const movedEntities = world.query(Changed(Position))
|
|
284
|
+
const movedEntities = world.query(Changed(Position))
|
|
284
285
|
|
|
285
286
|
// After running the query, the Changed modifier is reset
|
|
286
287
|
```
|
|
@@ -296,29 +297,25 @@ Koota allows you to subscribe to add, remove, and change events for specific tra
|
|
|
296
297
|
```js
|
|
297
298
|
// Subscribe to Position changes
|
|
298
299
|
const unsub = world.onChange(Position, (entity) => {
|
|
299
|
-
|
|
300
|
-
})
|
|
300
|
+
console.log(`Entity ${entity} changed position`)
|
|
301
|
+
})
|
|
301
302
|
|
|
302
303
|
// Subscribe to Position additions
|
|
303
304
|
const unsub = world.onAdd(Position, (entity) => {
|
|
304
|
-
|
|
305
|
-
})
|
|
305
|
+
console.log(`Entity ${entity} added position`)
|
|
306
|
+
})
|
|
306
307
|
|
|
307
308
|
// Subscribe to Position removals
|
|
308
309
|
const unsub = world.onRemove(Position, (entity) => {
|
|
309
|
-
|
|
310
|
-
})
|
|
310
|
+
console.log(`Entity ${entity} removed position`)
|
|
311
|
+
})
|
|
311
312
|
|
|
312
313
|
// Trigger events
|
|
313
|
-
const entity = world.spawn(Position)
|
|
314
|
-
entity.set(Position, { x: 10, y: 20 })
|
|
315
|
-
entity.remove(Position)
|
|
314
|
+
const entity = world.spawn(Position)
|
|
315
|
+
entity.set(Position, { x: 10, y: 20 })
|
|
316
|
+
entity.remove(Position)
|
|
316
317
|
```
|
|
317
318
|
|
|
318
|
-
### Query all entities
|
|
319
|
-
|
|
320
|
-
To get al queryable entities you simply query with not paramerters. Note, that not all entities are queryable. Any entity that has `IsExcluded` will not be able to be queried. This is used in Koota to exclude world entities, for example, but maybe used for other system level entities in the future. To get all entities regardless, use `world.entities`.
|
|
321
|
-
|
|
322
319
|
```js
|
|
323
320
|
// Returns all queryable entities
|
|
324
321
|
const allQueryableEntities = world.query()
|
|
@@ -330,12 +327,12 @@ By default, `updateEach` will automatically turn on change detection for traits
|
|
|
330
327
|
|
|
331
328
|
```js
|
|
332
329
|
// Setting changeDetection to 'never' will silence it, triggering no change events
|
|
333
|
-
world.query(Position, Velocity).updateEach(([position, velocity]) => {
|
|
334
|
-
}, { changeDetection: 'never' });
|
|
330
|
+
world.query(Position, Velocity).updateEach(([position, velocity]) => {}, { changeDetection: 'never' })
|
|
335
331
|
|
|
336
332
|
// Setting changeDetection to 'always' will ignore selective tracking and always emit change events for all traits that are mutated
|
|
337
|
-
world
|
|
338
|
-
|
|
333
|
+
world
|
|
334
|
+
.query(Position, Velocity)
|
|
335
|
+
.updateEach(([position, velocity]) => {}, { changeDetection: 'always' })
|
|
339
336
|
```
|
|
340
337
|
|
|
341
338
|
### World traits
|
|
@@ -343,14 +340,15 @@ world.query(Position, Velocity).updateEach(([position, velocity]) => {
|
|
|
343
340
|
For global data like time, these can be traits added to the world. **World traits do not appear in queries.**
|
|
344
341
|
|
|
345
342
|
```js
|
|
346
|
-
const Time = trait({ delta: 0, current: 0 })
|
|
347
|
-
world.add(Time)
|
|
343
|
+
const Time = trait({ delta: 0, current: 0 })
|
|
344
|
+
world.add(Time)
|
|
348
345
|
|
|
349
|
-
const time = world.get(Time)
|
|
350
|
-
world.set(Time, { current: performance.now() })
|
|
346
|
+
const time = world.get(Time)
|
|
347
|
+
world.set(Time, { current: performance.now() })
|
|
351
348
|
```
|
|
352
349
|
|
|
353
350
|
### Select traits on queries for updates
|
|
351
|
+
|
|
354
352
|
Query filters entity results and `select` is used to choose what traits are fetched for `updateEach` and `useStore`. This can be useful if your query is wider than the data you want to modify.
|
|
355
353
|
|
|
356
354
|
```js
|
|
@@ -365,46 +363,22 @@ world.query(Position, Velocity, Mass)
|
|
|
365
363
|
});
|
|
366
364
|
```
|
|
367
365
|
|
|
368
|
-
### Modifying trait stores
|
|
366
|
+
### Modifying trait stores directly
|
|
369
367
|
|
|
370
368
|
For performance-critical operations, you can modify trait stores directly using the `useStore` hook. This approach bypasses some of the safety checks and event triggers, so use it with caution. All stores are structure of arrays for performance purposes.
|
|
371
369
|
|
|
372
370
|
```js
|
|
373
371
|
// Returns the SoA stores
|
|
374
372
|
world.query(Position, Velocity).useStore(([position, velocity], entities) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
})
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Caching queries
|
|
387
|
-
|
|
388
|
-
Inline queries are great for readability and are optimized to be as fast as possible, but there is still some small overhead in hashing the query each time it is called.
|
|
389
|
-
|
|
390
|
-
```js
|
|
391
|
-
// Every time this query runs a hash for the query parameters (Position, Velocity)
|
|
392
|
-
// is created and then used to get the cached query internally
|
|
393
|
-
function updateMovement(world) {
|
|
394
|
-
world.query(Position, Velocity).updateEach(([pos, vel]) => { })
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
While this is not likely to be a bottleneck in your code compared to the actual update function, if you want to save these CPU cycles you can cache the query ahead of time and use the returned key. This will have the additional effect of creating the internal query immediately on a worlds, otherwise it will get created the first time it is run.
|
|
399
|
-
|
|
400
|
-
```js
|
|
401
|
-
// The internal query is created immediately before it is invoked
|
|
402
|
-
const movementQuery = cacheQuery(Position, Velocity)
|
|
403
|
-
|
|
404
|
-
// They query key is hashed ahead of time and we just use it
|
|
405
|
-
function updateMovement(world) {
|
|
406
|
-
world.query(movementQuery).updateEach(([pos, vel]) => { })
|
|
407
|
-
}
|
|
373
|
+
// Write our own loop over the stores
|
|
374
|
+
for (let i = 0; i < entities.length; i++) {
|
|
375
|
+
// Get the entity ID to use as the array index
|
|
376
|
+
const eid = entities[i].id()
|
|
377
|
+
// Write to each array in the store
|
|
378
|
+
position.x[eid] += velocity.x[eid] * delta
|
|
379
|
+
position.y[eid] += velocity.y[eid] * delta
|
|
380
|
+
}
|
|
381
|
+
})
|
|
408
382
|
```
|
|
409
383
|
|
|
410
384
|
### Query tips for the curious
|
|
@@ -417,7 +391,7 @@ The standard pattern for `updateEach`, and handlers in general, uses an arrow fu
|
|
|
417
391
|
|
|
418
392
|
```js
|
|
419
393
|
// Create the function once
|
|
420
|
-
const handleMove = ([position, velocity]) => {
|
|
394
|
+
const handleMove = ([position, velocity]) => {}
|
|
421
395
|
|
|
422
396
|
function updateMovement(world) {
|
|
423
397
|
// Use it for the updateEach
|
|
@@ -430,13 +404,14 @@ function updateMovement(world) {
|
|
|
430
404
|
A query result is just an array of entities with some extra methods. This means you can use `for of` instead of `forEach` to get a nice iterator. Additionally, this will save a little performance since `forEach` calls a function on each member, while `for of` will compile down to what is basically a for loop.
|
|
431
405
|
|
|
432
406
|
```js
|
|
433
|
-
// This is nice and ergonomic but will cost some overhead since we are
|
|
407
|
+
// This is nice and ergonomic but will cost some overhead since we are
|
|
434
408
|
// creating a fresh function for each entity and then calling it
|
|
435
|
-
world.query().forEach((entity) => {
|
|
409
|
+
world.query().forEach((entity) => {})
|
|
436
410
|
|
|
437
|
-
// By contrast, this compiles down to a for loop and will have a
|
|
411
|
+
// By contrast, this compiles down to a for loop and will have a
|
|
438
412
|
// single block of code executed for each entity
|
|
439
|
-
for (const entity of world.query()) {
|
|
413
|
+
for (const entity of world.query()) {
|
|
414
|
+
}
|
|
440
415
|
```
|
|
441
416
|
|
|
442
417
|
## APIs in detail until I make docs
|
|
@@ -488,7 +463,7 @@ world.set(Time, { current: performance.now() })
|
|
|
488
463
|
// Can take a callback with the previous state passed in
|
|
489
464
|
world.set(Time, (prev) => ({
|
|
490
465
|
current: performance.now(),
|
|
491
|
-
delta: performance.now() - prev.current
|
|
466
|
+
delta: performance.now() - prev.current,
|
|
492
467
|
}))
|
|
493
468
|
|
|
494
469
|
// Subscribe to add, remove or change events for entity traits
|
|
@@ -526,14 +501,14 @@ An entity is a number encoded with a world, generation and ID. Every entity is u
|
|
|
526
501
|
|
|
527
502
|
```js
|
|
528
503
|
// Add a trait to the entity
|
|
529
|
-
entity.add(Position)
|
|
504
|
+
entity.add(Position)
|
|
530
505
|
|
|
531
506
|
// Remove a trait from the entity
|
|
532
507
|
entity.remove(Position)
|
|
533
508
|
|
|
534
509
|
// Checks if the entity has the trait
|
|
535
510
|
// Return boolean
|
|
536
|
-
const result = entity.has(Position)
|
|
511
|
+
const result = entity.has(Position)
|
|
537
512
|
|
|
538
513
|
// Gets a snapshot instance of the trait
|
|
539
514
|
// Return TraitInstance
|
|
@@ -544,7 +519,7 @@ entity.set(Position, { x: 10, y: 10 })
|
|
|
544
519
|
// Can take a callback with the previous state passed in
|
|
545
520
|
entity.set(Position, (prev) => ({
|
|
546
521
|
x: prev + 1,
|
|
547
|
-
y: prev + 1
|
|
522
|
+
y: prev + 1,
|
|
548
523
|
}))
|
|
549
524
|
|
|
550
525
|
// Get the targets for a relationship
|
|
@@ -559,15 +534,25 @@ const target = entity.targetFor(Contains)
|
|
|
559
534
|
// Return number
|
|
560
535
|
const id = entity.id()
|
|
561
536
|
|
|
537
|
+
// Get the entity generation
|
|
538
|
+
// Return number
|
|
539
|
+
const generation = entity.generation()
|
|
540
|
+
|
|
562
541
|
// Destroys the entity making its number no longer valid
|
|
563
542
|
entity.destroy()
|
|
564
543
|
```
|
|
565
544
|
|
|
545
|
+
For introspection, `unpackEntity` can be used to get all of the encoded values. This can be useful for debugging.
|
|
546
|
+
|
|
547
|
+
```js
|
|
548
|
+
const { entityId, generation, worldId } = unpackEntity(entity)
|
|
549
|
+
```
|
|
550
|
+
|
|
566
551
|
### Trait
|
|
567
552
|
|
|
568
|
-
A trait is a specific block of data. They are added to entities to build up its overall data signature. If you are familiar with ECS, it is our version of a component. It is called a trait instead to not get confused with React or web components.
|
|
553
|
+
A trait is a specific block of data. They are added to entities to build up its overall data signature. If you are familiar with ECS, it is our version of a component. It is called a trait instead to not get confused with React or web components.
|
|
569
554
|
|
|
570
|
-
A trait can be created with a schema that describes the kind of data it will hold.
|
|
555
|
+
A trait can be created with a schema that describes the kind of data it will hold.
|
|
571
556
|
|
|
572
557
|
```js
|
|
573
558
|
const Position = trait({ x: 0, y: 0, z: 0 })
|
|
@@ -577,15 +562,15 @@ In cases where the data needs to be initialized for each instance of the trait c
|
|
|
577
562
|
|
|
578
563
|
```js
|
|
579
564
|
// ❌ The items array will be shared between every instance of this trait
|
|
580
|
-
const Inventory = trait({
|
|
581
|
-
items: [],
|
|
582
|
-
max: 10,
|
|
565
|
+
const Inventory = trait({
|
|
566
|
+
items: [],
|
|
567
|
+
max: 10,
|
|
583
568
|
})
|
|
584
569
|
|
|
585
570
|
// ✅ With a lazy initializer, each instance will now get its own array
|
|
586
|
-
const Inventory = trait({
|
|
587
|
-
items: () => [],
|
|
588
|
-
max: 10,
|
|
571
|
+
const Inventory = trait({
|
|
572
|
+
items: () => [],
|
|
573
|
+
max: 10,
|
|
589
574
|
})
|
|
590
575
|
```
|
|
591
576
|
|
|
@@ -622,10 +607,10 @@ const store = {
|
|
|
622
607
|
|
|
623
608
|
#### Array of Structures (AoS) - Callback-based traits
|
|
624
609
|
|
|
625
|
-
When using a callback, each entity's trait data is stored as an object in an array. This is best used for
|
|
610
|
+
When using a callback, each entity's trait data is stored as an object in an array. This is best used for compatibility with third party libraries like Three, or class instances in general.
|
|
626
611
|
|
|
627
612
|
```js
|
|
628
|
-
const Velocity = trait(() => ({ x: 0, y: 0, z: 0 }))
|
|
613
|
+
const Velocity = trait(() => ({ x: 0, y: 0, z: 0 }))
|
|
629
614
|
|
|
630
615
|
// Internally, this creates a store structure like:
|
|
631
616
|
const store = [
|
|
@@ -633,7 +618,7 @@ const store = [
|
|
|
633
618
|
{ x: 0, y: 0, z: 0 },
|
|
634
619
|
{ x: 0, y: 0, z: 0 },
|
|
635
620
|
// ...
|
|
636
|
-
]
|
|
621
|
+
]
|
|
637
622
|
|
|
638
623
|
// Similarly, this will create a new instance of Mesh in each index
|
|
639
624
|
const Mesh = trait(() => new THREE.Mesh())
|
|
@@ -651,12 +636,15 @@ type AttackerSchema = {
|
|
|
651
636
|
startedAt: number | null,
|
|
652
637
|
}
|
|
653
638
|
|
|
654
|
-
const Attacker =
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
639
|
+
const Attacker =
|
|
640
|
+
trait <
|
|
641
|
+
AttackerSchema >
|
|
642
|
+
{
|
|
643
|
+
continueCombo: null,
|
|
644
|
+
currentStageIndex: null,
|
|
645
|
+
stages: null,
|
|
646
|
+
startedAt: null,
|
|
647
|
+
}
|
|
660
648
|
```
|
|
661
649
|
|
|
662
650
|
However, this will not work with interfaces without a workaround due to intended behavior in TypeScript: https://github.com/microsoft/TypeScript/issues/15300
|
|
@@ -679,41 +667,99 @@ const Attacker = trait<Pick<AttackerSchema, keyof AttackerSchema>>({
|
|
|
679
667
|
})
|
|
680
668
|
```
|
|
681
669
|
|
|
670
|
+
#### Accessing the store directly
|
|
671
|
+
|
|
672
|
+
The store can be accessed with `getStore`, but this low-level access is risky as it bypasses Koota's guard rails. However, this can be useful for debugging where direct introspection of the store is needed. For direct store mutations, use the [`useStore` API](#modifying-trait-stores-direclty) instead.
|
|
673
|
+
|
|
674
|
+
```js
|
|
675
|
+
// Returns SoA or AoS depending on the trait
|
|
676
|
+
const positions = getStore(world, Position)
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Query
|
|
680
|
+
|
|
681
|
+
A Koota query is a lot like a database query. Parameters define how to find entities and efficiently process them in batches. Queries are the primary way to update and transform your app state, similar to how you'd use SQL to filter and modify database records.
|
|
682
|
+
|
|
683
|
+
#### Caching queries
|
|
684
|
+
|
|
685
|
+
Inline queries are great for readability and are optimized to be as fast as possible, but there is still some small overhead in hashing the query each time it is called.
|
|
686
|
+
|
|
687
|
+
```js
|
|
688
|
+
// Every time this query runs a hash for the query parameters (Position, Velocity)
|
|
689
|
+
// is created and then used to get the cached query internally
|
|
690
|
+
function updateMovement(world) {
|
|
691
|
+
world.query(Position, Velocity).updateEach(([pos, vel]) => {})
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
While this is not likely to be a bottleneck in your code compared to the actual update function, if you want to save these CPU cycles you can cache the query ahead of time and use the returned key. This will have the additional effect of creating the internal query immediately on a worlds, otherwise it will get created the first time it is run.
|
|
696
|
+
|
|
697
|
+
```js
|
|
698
|
+
// The internal query is created immediately before it is invoked
|
|
699
|
+
const movementQuery = cacheQuery(Position, Velocity)
|
|
700
|
+
|
|
701
|
+
// They query key is hashed ahead of time and we just use it
|
|
702
|
+
function updateMovement(world) {
|
|
703
|
+
world.query(movementQuery).updateEach(([pos, vel]) => {})
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
#### Query all entities
|
|
708
|
+
|
|
709
|
+
To get all queryable entities you simply query with no parameters.
|
|
710
|
+
|
|
711
|
+
```js
|
|
712
|
+
const allEntities = world.query()
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
This differs from `world.entities` which includes all entities, even system ones. Koota excludes its internal system entities from queries to keep userland queries from being polluted.
|
|
716
|
+
|
|
717
|
+
#### Excluding entities from queries
|
|
718
|
+
|
|
719
|
+
Any entity can be excluded from queries by adding the built-in tag `IsExcluded` to it. System entities get this tag added to them so that they do not interfere with the app.
|
|
720
|
+
|
|
721
|
+
```js
|
|
722
|
+
const entity = world.spawn(Position)
|
|
723
|
+
// This entity can no longer be queried
|
|
724
|
+
entity.add(IsExcluded)
|
|
725
|
+
|
|
726
|
+
const entities = world.query(Position)
|
|
727
|
+
entities.includes(entity) // This will always be false
|
|
728
|
+
```
|
|
682
729
|
|
|
683
730
|
### React
|
|
684
731
|
|
|
685
|
-
### `useQuery`
|
|
732
|
+
### `useQuery`
|
|
686
733
|
|
|
687
734
|
Reactively updates when entities matching the query changes. Returns a `QueryResult`, which is like an array of entities.
|
|
688
735
|
|
|
689
736
|
```js
|
|
690
737
|
// Get all entities with Position and Velocity traits
|
|
691
|
-
const entities = useQuery(Position, Velocity)
|
|
738
|
+
const entities = useQuery(Position, Velocity)
|
|
692
739
|
|
|
693
740
|
// Render a view
|
|
694
741
|
return (
|
|
695
742
|
<>
|
|
696
|
-
{entities.map(entity =>
|
|
743
|
+
{entities.map((entity) => (
|
|
744
|
+
<View key={entity.id()} entity={entity} />
|
|
745
|
+
))}
|
|
697
746
|
</>
|
|
698
|
-
)
|
|
747
|
+
)
|
|
699
748
|
```
|
|
700
749
|
|
|
701
|
-
### `usQueryFirst`
|
|
750
|
+
### `usQueryFirst`
|
|
702
751
|
|
|
703
752
|
Works like `useQuery` but only returns the first result. Can either be an entity of undefined.
|
|
704
753
|
|
|
705
754
|
```js
|
|
706
755
|
// Get the first entity with Player and Position traits
|
|
707
|
-
const player = useQueryFirst(Player, Position)
|
|
756
|
+
const player = useQueryFirst(Player, Position)
|
|
708
757
|
|
|
709
758
|
// Render a view if an entity is found
|
|
710
|
-
return player ?
|
|
711
|
-
<View entity={player} />
|
|
712
|
-
) : null;
|
|
713
|
-
|
|
759
|
+
return player ? <View entity={player} /> : null
|
|
714
760
|
```
|
|
715
761
|
|
|
716
|
-
### `useWorld`
|
|
762
|
+
### `useWorld`
|
|
717
763
|
|
|
718
764
|
Returns the world held in context via `WorldProvider`.
|
|
719
765
|
|
|
@@ -729,13 +775,13 @@ useEffect(() => {
|
|
|
729
775
|
|
|
730
776
|
```
|
|
731
777
|
|
|
732
|
-
### `WorldProvider`
|
|
778
|
+
### `WorldProvider`
|
|
733
779
|
|
|
734
780
|
The provider for the world context. A world must be created and passed in.
|
|
735
781
|
|
|
736
782
|
```js
|
|
737
783
|
// Create a world and pass it to the provider
|
|
738
|
-
const world = createWorld()
|
|
784
|
+
const world = createWorld()
|
|
739
785
|
|
|
740
786
|
// All hooks will now use this world instead of the default
|
|
741
787
|
function App() {
|
|
@@ -743,18 +789,17 @@ function App() {
|
|
|
743
789
|
<WorldProvider world={world}>
|
|
744
790
|
<Game />
|
|
745
791
|
</WorldProvider>
|
|
746
|
-
)
|
|
792
|
+
)
|
|
747
793
|
}
|
|
748
|
-
|
|
749
794
|
```
|
|
750
795
|
|
|
751
|
-
### `useTrait`
|
|
796
|
+
### `useTrait`
|
|
752
797
|
|
|
753
798
|
Observes an entity, or world, for a given trait and reactively updates when it is added, removed or changes value. The returned trait snapshot maybe `undefined` if the trait is no longer on the target. This can be used to conditionally render.
|
|
754
799
|
|
|
755
800
|
```js
|
|
756
801
|
// Get the position trait from an entity and reactively updates when it changes
|
|
757
|
-
const position = useTrait(entity, Position)
|
|
802
|
+
const position = useTrait(entity, Position)
|
|
758
803
|
|
|
759
804
|
// If position is removed from entity then it will be undefined
|
|
760
805
|
if (!position) return null
|
|
@@ -764,7 +809,7 @@ return (
|
|
|
764
809
|
<div>
|
|
765
810
|
Position: {position.x}, {position.y}
|
|
766
811
|
</div>
|
|
767
|
-
)
|
|
812
|
+
)
|
|
768
813
|
```
|
|
769
814
|
|
|
770
815
|
The entity passed into `useTrait` can be `undefined` or `null`. This helps with situations where `useTrait` is combined with queries in the same component since hooks cannot be conditionally called. However, this means that result can be `undefined` if the trait is not on the entity or if the target is itself `undefined`. In most cases the distinction will not matter, but if it does you can disambiguate by testing the target.
|
|
@@ -773,7 +818,7 @@ The entity passed into `useTrait` can be `undefined` or `null`. This helps with
|
|
|
773
818
|
// The entity may be undefined if there is no valid result
|
|
774
819
|
const entity = useQueryFirst(Position, Velocity)
|
|
775
820
|
// useTrait handles this by returned undefined if the target passed in does not exist
|
|
776
|
-
const position = useTrait(entity, Position)
|
|
821
|
+
const position = useTrait(entity, Position)
|
|
777
822
|
|
|
778
823
|
// However, undefined here can mean no entity or no component on entity
|
|
779
824
|
// To make the outcome no longer ambiguous you have to test the entity
|
|
@@ -786,29 +831,28 @@ return (
|
|
|
786
831
|
<div>
|
|
787
832
|
Position: {position.x}, {position.y}
|
|
788
833
|
</div>
|
|
789
|
-
)
|
|
834
|
+
)
|
|
790
835
|
```
|
|
791
836
|
|
|
792
|
-
### `useTraitEffect`
|
|
837
|
+
### `useTraitEffect`
|
|
793
838
|
|
|
794
839
|
Subscribes a callback to a trait on an entity. This callback fires as an effect whenenver it is added, removed or changes value without rerendering.
|
|
795
840
|
|
|
796
841
|
```js
|
|
797
842
|
// Subscribe to position changes on an entity and update a ref without causing a rerender
|
|
798
843
|
useTraitEffect(entity, Position, (position) => {
|
|
799
|
-
if (!position) return
|
|
800
|
-
meshRef.current.position.copy(position)
|
|
801
|
-
})
|
|
844
|
+
if (!position) return
|
|
845
|
+
meshRef.current.position.copy(position)
|
|
846
|
+
})
|
|
802
847
|
|
|
803
848
|
// Subscribe to world-level traits
|
|
804
849
|
useTraitEffect(world, GameState, (state) => {
|
|
805
|
-
if (!state) return
|
|
806
|
-
console.log('Game state changed:', state)
|
|
807
|
-
})
|
|
808
|
-
|
|
850
|
+
if (!state) return
|
|
851
|
+
console.log('Game state changed:', state)
|
|
852
|
+
})
|
|
809
853
|
```
|
|
810
854
|
|
|
811
|
-
### `useActions`
|
|
855
|
+
### `useActions`
|
|
812
856
|
|
|
813
857
|
Returns actions bound to the world that is context. Use actions created by `createActions`.
|
|
814
858
|
|