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:
- 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. - 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.