Core Concepts

Traits

Traits store your entity’s data. Each trait is a small piece of state in an ECS architecture.

Defining Traits

In Koota, traits (analogous to "components") can be defined using either:

  1. Schema-based (SoA) — for numeric or simpler data fields:
    import { trait } from 'koota'
    
    export const Health = trait({
      amount: 100,
      max: 100
    })
    

    Each property is stored in its own array internally for performance.
  2. Callback-based (AoS) — for complex objects or classes:
    export const Transform = trait(() => ({
      position: new THREE.Vector3(),
      rotation: new THREE.Euler()
    }))
    

    Each entity’s instance is stored in a single array of objects.

Tag Traits

Use empty traits (no data) for simple tagging:

export const IsPlayer = trait() 
export const IsEnemy = trait()

Initializing Trait Data

When adding a trait to an entity, you can override defaults:

entity.add(Health()) // uses defaults
entity.add(Health({ amount: 50 })) // custom amount

For callback-based traits:

const Model = trait(() => new THREE.Mesh())

const entity = world.spawn(Model()) // Each entity gets its own mesh instance

Typing Traits

If your trait is more complex, you can specify the exact TypeScript interface. For schema-based traits:

interface AttackData {
  continueCombo: boolean | null
  currentStageIndex: number | null
  stages: Array<AttackStage> | null
  startedAt: number | null
}

// Use Pick to avoid TS quirks with interfaces
export const Attacker = trait<Pick<AttackData, keyof AttackData>>({
  continueCombo: null,
  currentStageIndex: null,
  stages: null,
  startedAt: null,
})

Working with Traits

// Retrieve a trait snapshot
const health = entity.get(Health)
health.amount -= 10

// Or update with set, which triggers events/changes
entity.set(Health, (prev) => ({
  ...prev,
  amount: prev.amount - 10
}))

Adding and Removing Traits

// Add trait
entity.add(Health({ amount: 100 }))

// Remove trait
entity.remove(Health)

// Check if a trait is present
if (entity.has(Health)) {
  // ...
}

Debugging Traits

You can create special traits for debugging:

export const Debug = trait({
  label: '',
  enabled: false
})

// Mark an entity for debug
entity.add(Debug({ label: 'Player1', enabled: true }))

Then your debugging or logging systems can query these and provide extra info.


SoA vs AoS

  • Structure of Arrays (SoA) is best for performance when performing the same operation on large sets of numeric data.
  • Array of Structures (AoS) is more convenient when storing class instances, references, or arbitrary objects like THREE.Vector3.

Use whichever style suits your data and performance needs.


Next Steps

  • Go to Entities to see how to spawn and manage them.
  • Learn about Systems for processing trait data.
  • Check out Actions for centralized trait updates.