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 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
- position.x += velocity.x * delta;
53
- position.y += velocity.y * delta;
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
- spawnShip: (position) => world.spawn(Position(position), Velocity),
102
- destroyAllShips: (world) => {
103
- world.query(Position, Velocity).forEach((entity) => {
104
- entity.destroy();
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
- const { spawnShip, destroyAllShips } = useActions(actions);
111
+ const { spawnShip, destroyAllShips } = useActions(actions)
111
112
 
112
- // Spawn three ships on mount
113
- useEffect(() => {
114
- spawnShip({ x: 0, y: 1 });
115
- spawnShip({ x: 1, y: 0 });
116
- spawnShip({ x: 1, y: 1 });
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
- // Destroy all ships during cleanup
119
- return () => drestroyAllShips();
120
- }, []);
119
+ // Destroy all ships during cleanup
120
+ return () => drestroyAllShips()
121
+ }, [])
121
122
 
122
- // And destroy all ships on click!
123
- return <button onClick={destroyAllShips}>Boom!</button>;
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
- const entity = world.spawn(Velocity, Position);
134
- return () => entity.destroy();
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)); // Returns child
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); // False, the child and grandchild are destroyed too
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)); // False
197
- hero.has(Targeting(goblin)); // True
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); // Returns [gold, silver]
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)); // Returns [inventory]
215
- const containsAnything = world.query(Contains('*')); // Returns [inventory, chest]
216
- const relatesToGold = world.query(Wildcard(gold)); // Returns [inventory, chest, dwarf]
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
- console.log(`Entity ${entity} changed position`);
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
- console.log(`Entity ${entity} added position`);
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
- console.log(`Entity ${entity} removed position`);
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.query(Position, Velocity).updateEach(([position, velocity]) => {
338
- }, { changeDetection: 'always' });
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 direclty
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
- // Write our own loop over the stores
376
- for (let i = 0; i < entities.length; i++) {
377
- // Get the entity ID to use as the array index
378
- const eid = entities[i].id();
379
- // Write to each array in the store
380
- position.x[eid] += velocity.x[eid] * delta;
381
- position.y[eid] += velocity.y[eid] * delta;
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 compatibiilty with third party libraries like Three, or class instnaces in general.
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 = trait<AttackerSchema>({
655
- continueCombo: null,
656
- currentStageIndex: null,
657
- stages: null,
658
- startedAt: null,
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 => <View key={entity.id()} entity={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