Advanced ECS Topics
This section covers advanced features in Koota. While not always required, they can significantly improve performance, enable custom workflows, or simplify complex scenarios.
useStore()
1. Direct Store Updates with
In most cases, you’ll read and update trait data via .updateEach()
, entity.set()
, or the React hooks. However, for performance-critical operations on many entities, you can directly modify the underlying store arrays.
world.query(Position, Velocity).useStore(([pos, vel], entities) => {
// pos, vel are SoA (Structure of Arrays) stores if you used a schema-based trait
// If it's AoS (callback-based trait), they’ll be arrays of objects instead.
for (let i = 0; i < entities.length; i++) {
const e = entities[i] // an Entity handle
const eid = e.id() // numeric ID to index into SoA arrays
pos.x[eid] += vel.x[eid] * delta
pos.y[eid] += vel.y[eid] * delta
}
})
useStore()
Notes on
- Bypasses typical event triggers and change detection.
- If you rely on
onChange(...)
or theChanged(...)
query modifier, direct store writes might not trigger those events. - Best used in performance-critical “hot” loops.
select()
2. Partial Selection of Traits with
You can limit which traits to retrieve or update during a query. If your system only needs to modify Health
(but still wants the entity to have Transform
, Velocity
, etc.), you can do:
world.query(Transform, Velocity, Health)
.select(Health)
.updateEach(([health]) => {
health.amount -= 1
})
Why use it:
- Performance: Minimizes overhead by only pulling the data you actually need.
- Clarity: Emphasizes which traits you’re reading/updating.
3. Trait Lifecycle Events
Subscribe to trait additions, removals, or changes at the world level:
const unsubAdd = world.onAdd(Position, (entity) => {
console.log('Entity added Position:', entity.id())
})
const unsubRemove = world.onRemove(Health, (entity) => {
console.log('Entity removed Health:', entity.id())
})
const unsubChange = world.onChange(Health, (entity) => {
console.log('Entity changed Health:', entity.id(), entity.get(Health))
})
// Later, to stop listening:
unsubAdd()
unsubRemove()
unsubChange()
Multi-Trait Events
You can also pass an array or query-like structure:
// Fired when an entity gains or loses BOTH Position & Velocity
world.onAdd([Position, Velocity], (entity) => { /* ... */ })
world.onRemove([Position, Velocity], (entity) => { /* ... */ })
4. Querying All Entities
If you call world.query()
without any traits, you get all queryable entities in the world:
const everything = world.query()
everything.forEach((entity) => {
console.log('Entity:', entity.id())
})
Koota excludes certain internal or “excluded” entities by default, so you won’t see world-level entities or other hidden items. If you want literally every single entity (including system-level ones), you can do world.entities
, but that’s rarely needed.
5. Conditional System Scheduling
Not all systems need to run every frame. In many games, you want different subsets of systems to run in different game states (e.g. “paused,” “main menu,” “combat,” etc.):
function updateWorld(world: World) {
const state = world.get(GameState)
if (!state) return
if (state.phase === 'Menu') {
menuSystems.forEach(sys => sys(world))
} else if (state.phase === 'Combat') {
combatSystems.forEach(sys => sys(world))
} else {
normalSystems.forEach(sys => sys(world))
}
}
Benefits:
- Save performance by not running irrelevant systems.
- Keep logic organized for different states or phases of the game.
6. Additional Relationship Patterns
You’ve seen the basics of relation()
, including exclusive: true
or autoRemoveTarget: true
. Consider these patterns:
- Multi-level Parenting: A parent can also have a parent, forming a hierarchical tree. If you set
autoRemoveTarget: true
, destroying a top-level parent cascades removal down the chain. - Bidirectional or “Friend/Ally”: If each entity references the other with a relation, you can quickly query “allies.”
- Filtering with Relationship Data: If you store fields like
{ amount: 10 }
, you can filter the query to find relationships with specific amounts or states.
Summary
- Store-Level Access:
useStore()
gives you maximum control over how you read/write data, ideal for heavy numeric updates. - Selective Trait Access:
.select()
cuts down on unneeded data access in queries. - Lifecycle Events: Listen for additions, removals, or changes to traits at a global level.
- Whole-World Queries:
world.query()
with no arguments returns all queryable entities. - Conditional Scheduling: Run only the systems you need at a given time.
- Advanced Relationship Usage: Use relationships for complex data structures or parent-child hierarchies.
By combining these advanced features with the core ECS docs, you can build extremely optimized, maintainable game logic in Viber3D.