koota 0.6.3 → 0.6.5-canary.0b1a1ea
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 +141 -21
- package/dist/{chunk-DMMEOSUY.js → chunk-4KPZSYES.js} +487 -324
- package/dist/index.cjs +487 -324
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/react.cjs +331 -219
- package/dist/react.d.cts +4 -3
- package/dist/react.d.ts +4 -3
- package/dist/react.js +145 -92
- package/dist/{types-Bh4RiNrK.d.cts → types-BopbOvWW.d.cts} +41 -14
- package/dist/{types-Bh4RiNrK.d.ts → types-BopbOvWW.d.ts} +41 -14
- package/package.json +8 -4
- package/react/index.cjs +331 -219
- package/react/index.d.cts +4 -3
- package/react/index.d.ts +4 -3
- package/react/index.js +145 -92
- package/react/package.json +0 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[](https://discord.gg/poimandres)
|
|
2
2
|
|
|
3
|
-
<img src="logo.svg" alt="Koota" width="100%" />
|
|
3
|
+
<img src="docs/logo.svg" alt="Koota" width="100%" />
|
|
4
4
|
|
|
5
5
|
Koota is an ECS-based state management library optimized for real-time apps, games, and XR experiences. Use as much or as little as you need.
|
|
6
6
|
|
|
@@ -8,7 +8,11 @@ Koota is an ECS-based state management library optimized for real-time apps, gam
|
|
|
8
8
|
npm i koota
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The official AI agent skill can be installed from this repo. [Read more about skills here](https://agentskills.io/home).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx skills add pmndrs/koota
|
|
15
|
+
```
|
|
12
16
|
|
|
13
17
|
### First, define traits
|
|
14
18
|
|
|
@@ -48,11 +52,18 @@ const goblin = world.spawn(Position({ x: 10, y: 10 }), Velocity, Mesh)
|
|
|
48
52
|
Queries fetch entities sharing traits (archetypes). Use them to batch update entities efficiently.
|
|
49
53
|
|
|
50
54
|
```js
|
|
55
|
+
// updateEach mutates and writes back to stores
|
|
51
56
|
// Run this in a loop
|
|
52
57
|
world.query(Position, Velocity).updateEach(([position, velocity]) => {
|
|
53
58
|
position.x += velocity.x * delta
|
|
54
59
|
position.y += velocity.y * delta
|
|
55
60
|
})
|
|
61
|
+
|
|
62
|
+
// For read-only operations with no mutation, use readEach
|
|
63
|
+
const data = []
|
|
64
|
+
world.query(Position, Velocity).readEach(([position, velocity]) => {
|
|
65
|
+
data.push({ x: position.x, y: position.y })
|
|
66
|
+
})
|
|
56
67
|
```
|
|
57
68
|
|
|
58
69
|
### Use in your React components
|
|
@@ -91,10 +102,10 @@ function RocketView({ entity }) {
|
|
|
91
102
|
Use actions to safely modify Koota from inside of React in either effects or events.
|
|
92
103
|
|
|
93
104
|
```js
|
|
94
|
-
import {
|
|
105
|
+
import { createActions } from 'koota'
|
|
95
106
|
import { useActions } from 'koota/react'
|
|
96
107
|
|
|
97
|
-
const actions =
|
|
108
|
+
const actions = createActions((world) => ({
|
|
98
109
|
spawnShip: (position) => world.spawn(Position(position), Velocity),
|
|
99
110
|
destroyAllShips: () => {
|
|
100
111
|
world.query(Position, Velocity).forEach((entity) => {
|
|
@@ -136,7 +147,7 @@ useEffect(() => {
|
|
|
136
147
|
|
|
137
148
|
### Relations
|
|
138
149
|
|
|
139
|
-
Koota supports relations between entities using the `relation` function. Relations allow you to
|
|
150
|
+
Koota supports relations between entities using the `relation` function. Relations allow you to build graphs by creating connections between entities with efficient queries.
|
|
140
151
|
|
|
141
152
|
```js
|
|
142
153
|
const ChildOf = relation()
|
|
@@ -147,6 +158,24 @@ const child = world.spawn(ChildOf(parent))
|
|
|
147
158
|
const entity = world.queryFirst(ChildOf(parent)) // Returns child
|
|
148
159
|
```
|
|
149
160
|
|
|
161
|
+
A relation is typically owned by the entity that needs to express it. The **source** is the entity that has the relation added, and the **target** is the entity it points to.
|
|
162
|
+
|
|
163
|
+
```mermaid
|
|
164
|
+
flowchart BT
|
|
165
|
+
subgraph Parent
|
|
166
|
+
end
|
|
167
|
+
subgraph childA["Child"]
|
|
168
|
+
COA["ChildOf(Parent)"]
|
|
169
|
+
end
|
|
170
|
+
subgraph childB["Child"]
|
|
171
|
+
COB["ChildOf(Parent)"]
|
|
172
|
+
end
|
|
173
|
+
COA --> Parent
|
|
174
|
+
COB --> Parent
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
In `child.add(ChildOf(parent))`, child is the source and parent is the target. This design means the parent doesn't need to know about its children, instead children care about their parent, optimzing batch queries.
|
|
178
|
+
|
|
150
179
|
#### With data
|
|
151
180
|
|
|
152
181
|
Relations can contain data like any trait.
|
|
@@ -167,12 +196,14 @@ inventory.set(Contains(gold), { amount: 20 })
|
|
|
167
196
|
const data = inventory.get(Contains(gold)) // { amount: 20 }
|
|
168
197
|
```
|
|
169
198
|
|
|
170
|
-
#### Auto
|
|
199
|
+
#### Auto destroy
|
|
200
|
+
|
|
201
|
+
Relations can automatically destroy related entities when their counterpart is destroyed using the `autoDestroy` option.
|
|
171
202
|
|
|
172
|
-
|
|
203
|
+
**Destroy orphans.** When a target is destroyed, destroy all sources pointing to it. This is commonly used for hierarchies when you want to clean up any detached graphs. It can be enabled with the `'orphan'` or `'source'` option.
|
|
173
204
|
|
|
174
205
|
```js
|
|
175
|
-
const ChildOf = relation({
|
|
206
|
+
const ChildOf = relation({ autoDestroy: 'orphan' }) // Or 'source'
|
|
176
207
|
|
|
177
208
|
const parent = world.spawn()
|
|
178
209
|
const child = world.spawn(ChildOf(parent))
|
|
@@ -183,6 +214,21 @@ parent.destroy()
|
|
|
183
214
|
world.has(child) // False, the child and grandchild are destroyed too
|
|
184
215
|
```
|
|
185
216
|
|
|
217
|
+
**Destroy targets.** When a source is destroyed, destroy all its targets.
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
const Contains = relation({ autoDestroy: 'target' })
|
|
221
|
+
|
|
222
|
+
const container = world.spawn()
|
|
223
|
+
const itemA = world.spawn()
|
|
224
|
+
const itemB = world.spawn()
|
|
225
|
+
|
|
226
|
+
container.add(Contains(itemA), Contains(itemB))
|
|
227
|
+
container.destroy()
|
|
228
|
+
|
|
229
|
+
world.has(itemA) // False, items are destroyed with container
|
|
230
|
+
```
|
|
231
|
+
|
|
186
232
|
#### Exclusive relations
|
|
187
233
|
|
|
188
234
|
Exclusive relations ensure each entity can only have one target.
|
|
@@ -206,7 +252,27 @@ hero.has(Targeting(goblin)) // True
|
|
|
206
252
|
> ⚠️ **Experimental**<br>
|
|
207
253
|
> This API is experimental and may change in future versions. Please provide feedback on GitHub or Discord.
|
|
208
254
|
|
|
209
|
-
Ordered relations maintain a list of related entities with
|
|
255
|
+
Ordered relations maintain a list of related entities with
|
|
256
|
+
bidirectional sync.
|
|
257
|
+
|
|
258
|
+
A query like `world.query(ChildOf(parent))` returns a flat list of children without any ordering. If you need an ordered list, you'd have to store an order field and sort every time you query.
|
|
259
|
+
|
|
260
|
+
An ordered relation solves this by caching the order on the target. It's a trait added to the parent that maintains a view of all entities targeting it.
|
|
261
|
+
|
|
262
|
+
```mermaid
|
|
263
|
+
flowchart BT
|
|
264
|
+
subgraph Parent
|
|
265
|
+
OC["OrderedChildren → [Child B, Child A]"]
|
|
266
|
+
end
|
|
267
|
+
subgraph childA["Child A"]
|
|
268
|
+
COA["ChildOf(Parent)"]
|
|
269
|
+
end
|
|
270
|
+
subgraph childB["Child B"]
|
|
271
|
+
COB["ChildOf(Parent)"]
|
|
272
|
+
end
|
|
273
|
+
COA --> Parent
|
|
274
|
+
COB --> Parent
|
|
275
|
+
```
|
|
210
276
|
|
|
211
277
|
```js
|
|
212
278
|
import { relation, ordered } from 'koota'
|
|
@@ -224,10 +290,10 @@ children.splice(0, 1) // removes ChildOf(parent) from child1
|
|
|
224
290
|
child2.add(ChildOf(parent)) // child2 automatically added to list
|
|
225
291
|
```
|
|
226
292
|
|
|
227
|
-
Ordered relations support array methods like `push()`, `pop()`, `shift()`, `unshift()`, and `splice()`, plus special methods `moveTo()` and `insert()` for precise control. Changes to the list automatically sync with relations, and vice versa.
|
|
293
|
+
Ordered relations support array methods like `push()`, `pop()`, `shift()`, `unshift()`, and `splice()`, plus special methods `moveTo()` and `insert()` for precise control. Changes to the list automatically sync with relations, and vice versa, as well as emit change events.
|
|
228
294
|
|
|
229
295
|
> ⚠️ **Performance note**<br>
|
|
230
|
-
> Ordered relations
|
|
296
|
+
> Ordered relations requires additional bookkeeping where the cost of ordering is paid during structural changes (add, remove, move) instead of at query time. Use ordered relations only when entity order is essential or when hierarchical search (looping over children) is necessary.
|
|
231
297
|
|
|
232
298
|
#### Querying relations
|
|
233
299
|
|
|
@@ -279,9 +345,6 @@ player.has(banana) // false
|
|
|
279
345
|
|
|
280
346
|
Relations work with tracking modifiers to detect when entities gain, lose, or update relations. Changes can only be tracked on relations that have a store.
|
|
281
347
|
|
|
282
|
-
> 👉 **Note**<br>
|
|
283
|
-
> You can currently only track changes to all relations of a given type, such as `ChildOf`, but not specific relation pairs, such as `ChildOf(parent)`.
|
|
284
|
-
|
|
285
348
|
```js
|
|
286
349
|
import { createAdded, createRemoved, createChanged } from 'koota'
|
|
287
350
|
|
|
@@ -301,18 +364,19 @@ const orphaned = world.query(Removed(ChildOf))
|
|
|
301
364
|
const updated = world.query(Changed(ChildOf))
|
|
302
365
|
```
|
|
303
366
|
|
|
304
|
-
|
|
367
|
+
> 👉 **Note**<br>
|
|
368
|
+
> Tracking modifiers do not accept pairs directly such as `Changed(ChildOf(parent))`. Instead, pass the base relation to the modifier and add the pair as a separate query parameter to filter by target.
|
|
305
369
|
|
|
306
370
|
```js
|
|
307
371
|
const parent = world.spawn()
|
|
308
372
|
|
|
309
|
-
//
|
|
373
|
+
// Filter changed entities by a specific target
|
|
310
374
|
const changedChildren = world.query(Changed(ChildOf), ChildOf(parent))
|
|
311
375
|
```
|
|
312
376
|
|
|
313
377
|
#### Relation events
|
|
314
378
|
|
|
315
|
-
Relations emit events per **
|
|
379
|
+
Relations emit events per **pair**. This makes it easy to know exactly which target was involved.
|
|
316
380
|
|
|
317
381
|
- `onAdd(Relation, (entity, target) => {})` triggers when `entity.add(Relation(target))` is called.
|
|
318
382
|
- `onRemove(Relation, (entity, target) => {})` triggers when `entity.remove(Relation(target))` is called.
|
|
@@ -333,6 +397,16 @@ child.set(ChildOf(parent), { priority: 1 }) // onChange(child, parent)
|
|
|
333
397
|
child.remove(ChildOf(parent)) // onRemove(child, parent)
|
|
334
398
|
```
|
|
335
399
|
|
|
400
|
+
Hooks also accept **relation pairs** for target-specific filtering. `ChildOf(parent)` only fires for that specific target, while `ChildOf('*')` fires for any target (equivalent to passing the relation itself).
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
// Only fires when a ChildOf relation to this specific parent is added
|
|
404
|
+
world.onAdd(ChildOf(parent), (entity, target) => {})
|
|
405
|
+
|
|
406
|
+
// Fires for any ChildOf addition, the same as passing the ChildOf trait
|
|
407
|
+
world.onAdd(ChildOf('*'), (entity, target) => {})
|
|
408
|
+
```
|
|
409
|
+
|
|
336
410
|
### Query modifiers
|
|
337
411
|
|
|
338
412
|
Modifiers are used to filter query results enabling powerful patterns. All modifiers can be mixed together.
|
|
@@ -361,6 +435,8 @@ const movingOrVisible = world.query(Or(Velocity, Renderable))
|
|
|
361
435
|
|
|
362
436
|
The `Added` modifier tracks all entities that have added the specified traits or relations since the last time the query was run. A new instance of the modifier must be created for tracking to be unique.
|
|
363
437
|
|
|
438
|
+
When multiple traits are passed to `Added` it uses logical `AND`. Only entities where **all** specified traits have been added will be returned.
|
|
439
|
+
|
|
364
440
|
```js
|
|
365
441
|
import { createAdded } from 'koota'
|
|
366
442
|
|
|
@@ -372,6 +448,12 @@ const newPositions = world.query(Added(Position))
|
|
|
372
448
|
// Track entities that added a ChildOf relation
|
|
373
449
|
const newChildren = world.query(Added(ChildOf))
|
|
374
450
|
|
|
451
|
+
// Track entities where BOTH Position AND Velocity were added
|
|
452
|
+
const fullyAdded = world.query(Added(Position, Velocity))
|
|
453
|
+
|
|
454
|
+
// Track entities where EITHER Position OR Velocity was added
|
|
455
|
+
const eitherAdded = world.query(Or(Added(Position), Added(Velocity)))
|
|
456
|
+
|
|
375
457
|
// After running the query, the Added modifier is reset
|
|
376
458
|
```
|
|
377
459
|
|
|
@@ -379,6 +461,8 @@ const newChildren = world.query(Added(ChildOf))
|
|
|
379
461
|
|
|
380
462
|
The `Removed` modifier tracks all entities that have removed the specified traits or relations 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.
|
|
381
463
|
|
|
464
|
+
When multiple traits are passed to `Removed` it uses logical `AND`. Only entities where **all** specified traits have been removed will be returned.
|
|
465
|
+
|
|
382
466
|
```js
|
|
383
467
|
import { createRemoved } from 'koota'
|
|
384
468
|
|
|
@@ -390,6 +474,12 @@ const stoppedEntities = world.query(Removed(Velocity))
|
|
|
390
474
|
// Track entities that removed a ChildOf relation
|
|
391
475
|
const orphaned = world.query(Removed(ChildOf))
|
|
392
476
|
|
|
477
|
+
// Track entities where BOTH Position AND Velocity were removed
|
|
478
|
+
const fullyRemoved = world.query(Removed(Position, Velocity))
|
|
479
|
+
|
|
480
|
+
// Track entities where EITHER Position OR Velocity was removed
|
|
481
|
+
const eitherRemoved = world.query(Or(Removed(Position), Removed(Velocity)))
|
|
482
|
+
|
|
393
483
|
// After running the query, the Removed modifier is reset
|
|
394
484
|
```
|
|
395
485
|
|
|
@@ -397,6 +487,8 @@ const orphaned = world.query(Removed(ChildOf))
|
|
|
397
487
|
|
|
398
488
|
The `Changed` modifier tracks all entities that have had the specified traits or relation stores change since the last time the query was run. A new instance of the modifier must be created for tracking to be unique.
|
|
399
489
|
|
|
490
|
+
When multiple traits are passed to `Changed` it uses logical `AND`. Only entities where **all** specified traits have changed will be returned.
|
|
491
|
+
|
|
400
492
|
```js
|
|
401
493
|
import { createChanged } from 'koota'
|
|
402
494
|
|
|
@@ -408,6 +500,12 @@ const movedEntities = world.query(Changed(Position))
|
|
|
408
500
|
// Track entities whose ChildOf relation data has changed
|
|
409
501
|
const updatedChildren = world.query(Changed(ChildOf))
|
|
410
502
|
|
|
503
|
+
// Track entities where BOTH Position AND Velocity have changed
|
|
504
|
+
const fullyUpdated = world.query(Changed(Position, Velocity))
|
|
505
|
+
|
|
506
|
+
// Track entities where EITHER Position OR Velocity has changed
|
|
507
|
+
const eitherChanged = world.query(Or(Changed(Position), Changed(Velocity)))
|
|
508
|
+
|
|
411
509
|
// After running the query, the Changed modifier is reset
|
|
412
510
|
```
|
|
413
511
|
|
|
@@ -622,6 +720,10 @@ const unsub = world.onAdd(Position, (entity) => {})
|
|
|
622
720
|
const unsub = world.onRemove(Position, (entity) => {})
|
|
623
721
|
const unsub = world.onChange(Position, (entity) => {})
|
|
624
722
|
|
|
723
|
+
// Hooks also accept relation pairs for target-specific filtering
|
|
724
|
+
const unsub = world.onAdd(ChildOf(parent), (entity, target) => {})
|
|
725
|
+
const unsub = world.onAdd(ChildOf('*'), (entity, target) => {})
|
|
726
|
+
|
|
625
727
|
// Subscribe to add or remove query events
|
|
626
728
|
// This triggers whenever a query is updated
|
|
627
729
|
// Return unsub function
|
|
@@ -876,7 +978,7 @@ While this is not likely to be a bottleneck in your code compared to the actual
|
|
|
876
978
|
|
|
877
979
|
```js
|
|
878
980
|
// The internal query is created immediately before it is invoked
|
|
879
|
-
const movementQuery =
|
|
981
|
+
const movementQuery = createQuery(Position, Velocity)
|
|
880
982
|
|
|
881
983
|
// The query ref is used for fast array-based lookup
|
|
882
984
|
function updateMovement(world) {
|
|
@@ -977,10 +1079,15 @@ function App() {
|
|
|
977
1079
|
|
|
978
1080
|
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.
|
|
979
1081
|
|
|
1082
|
+
Also accepts relation pairs like `ChildOf(parent)` to observe a specific relation's store data.
|
|
1083
|
+
|
|
980
1084
|
```js
|
|
981
1085
|
// Get the position trait from an entity and reactively updates when it changes
|
|
982
1086
|
const position = useTrait(entity, Position)
|
|
983
1087
|
|
|
1088
|
+
// Observe a specific relation pair's store data
|
|
1089
|
+
const childData = useTrait(entity, ChildOf(parent))
|
|
1090
|
+
|
|
984
1091
|
// If position is removed from entity then it will be undefined
|
|
985
1092
|
if (!position) return null
|
|
986
1093
|
|
|
@@ -1035,6 +1142,8 @@ function ActiveIndicator({ entity }) {
|
|
|
1035
1142
|
|
|
1036
1143
|
Observes an entity, or world, for any trait and reactively updates when it is added or removed. Returns `true` when the trait is present or `false` when absent. Unlike `useTrait`, this only tracks presence and not the trait's value.
|
|
1037
1144
|
|
|
1145
|
+
Also accepts relation pairs like `ChildOf(parent)` or `ChildOf('*')` to track the presence of specific or any relation targets.
|
|
1146
|
+
|
|
1038
1147
|
```js
|
|
1039
1148
|
const Health = trait({ amount: 100 })
|
|
1040
1149
|
|
|
@@ -1042,6 +1151,12 @@ function HealthIndicator({ entity }) {
|
|
|
1042
1151
|
// Returns true if the entity has the trait, false otherwise
|
|
1043
1152
|
const hasHealth = useHas(entity, Health)
|
|
1044
1153
|
|
|
1154
|
+
// Track a specific relation pair
|
|
1155
|
+
const isChildOfParent = useHas(entity, ChildOf(parent))
|
|
1156
|
+
|
|
1157
|
+
// Track any ChildOf relation
|
|
1158
|
+
const hasAnyParent = useHas(entity, ChildOf('*'))
|
|
1159
|
+
|
|
1045
1160
|
if (!hasHealth) return null
|
|
1046
1161
|
|
|
1047
1162
|
return <div>❤️ Has Health</div>
|
|
@@ -1050,7 +1165,7 @@ function HealthIndicator({ entity }) {
|
|
|
1050
1165
|
|
|
1051
1166
|
### `useTraitEffect`
|
|
1052
1167
|
|
|
1053
|
-
Subscribes a callback to a trait on an entity. This callback fires as an effect whenever it is added, removed or changes value without rerendering.
|
|
1168
|
+
Subscribes a callback to a trait on an entity. This callback fires as an effect whenever it is added, removed or changes value without rerendering. Also accepts relation pairs.
|
|
1054
1169
|
|
|
1055
1170
|
```js
|
|
1056
1171
|
// Subscribe to position changes on an entity and update a ref without causing a rerender
|
|
@@ -1059,6 +1174,11 @@ useTraitEffect(entity, Position, (position) => {
|
|
|
1059
1174
|
meshRef.current.position.copy(position)
|
|
1060
1175
|
})
|
|
1061
1176
|
|
|
1177
|
+
// Subscribe to a specific relation pair
|
|
1178
|
+
useTraitEffect(entity, ChildOf(parent), (data) => {
|
|
1179
|
+
console.log('ChildOf data changed:', data)
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1062
1182
|
// Subscribe to world-level traits
|
|
1063
1183
|
useTraitEffect(world, GameState, (state) => {
|
|
1064
1184
|
if (!state) return
|
|
@@ -1106,11 +1226,11 @@ function InventoryDisplay({ entity }) {
|
|
|
1106
1226
|
|
|
1107
1227
|
### `useActions`
|
|
1108
1228
|
|
|
1109
|
-
Returns actions bound to the world that is in context. Use actions created by `
|
|
1229
|
+
Returns actions bound to the world that is in context. Use actions created by `createActions`.
|
|
1110
1230
|
|
|
1111
1231
|
```js
|
|
1112
1232
|
// Create actions
|
|
1113
|
-
const actions =
|
|
1233
|
+
const actions = createActions((world) => ({
|
|
1114
1234
|
spawnPlayer: () => world.spawn(IsPlayer).
|
|
1115
1235
|
destroyAllPlayers: () => {
|
|
1116
1236
|
world.query(IsPlayer).forEach((player) => {
|