Core Concepts

Advanced ECS Topics

Dive deeper into Koota’s advanced features for optimization, low-level control, and special ECS workflows.

This section covers advanced features in Koota. While not always required, they can significantly improve performance, enable custom workflows, or simplify complex scenarios.


1. Direct Store Updates with useStore()

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
  }
})

Notes on useStore()

  • Bypasses typical event triggers and change detection.
  • If you rely on onChange(...) or the Changed(...) query modifier, direct store writes might not trigger those events.
  • Best used in performance-critical “hot” loops.

2. Partial Selection of Traits with select()

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:

  1. 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.
  2. Bidirectional or “Friend/Ally”: If each entity references the other with a relation, you can quickly query “allies.”
  3. 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

  1. Store-Level Access: useStore() gives you maximum control over how you read/write data, ideal for heavy numeric updates.
  2. Selective Trait Access: .select() cuts down on unneeded data access in queries.
  3. Lifecycle Events: Listen for additions, removals, or changes to traits at a global level.
  4. Whole-World Queries: world.query() with no arguments returns all queryable entities.
  5. Conditional Scheduling: Run only the systems you need at a given time.
  6. 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.