Appearance
Entities and traits
Kiln's world is a Koota ECS. An entity is an id. A trait is a piece of data an entity can have. Systems query entities by their traits and change them. Kiln re-exports Koota's trait, relation, Not and Or as they are, and wraps the rest (systems, commands, resources, events) so access can be checked.
Traits
ts
import { trait } from 'kiln'
/** Linear velocity in world units per second. */
export const Velocity = trait({ x: 0, y: 0, z: 0 }) // struct of arrays
/** The enemy's current path, one array per entity. */
export const Path = trait(() => [] as { x: number; z: number }[]) // one object per entity
/** Tag: the entity the input controls. */
export const IsPlayer = trait() // no data| Form | Storage | Use for |
|---|---|---|
trait({ x: 0 }) | Struct of arrays: one array per field. Fast. | Numbers, strings, booleans |
trait(() => obj) | One object per entity | Three.js objects, arrays, maps, class instances |
trait() | Nothing | Tags and markers |
The object passed to a struct trait holds the defaults. Add a trait with its defaults by passing the trait itself, or override fields by calling it:
ts
commands.spawn(Transform, Velocity, IsPlayer)
commands.spawn(Transform({ x: 3, y: 1 }), Velocity({ x: -2 }))Struct traits are what kiln run --snapshot, saves and recordings capture as JSON. Object traits appear in snapshots only when their value is JSON-friendly (or has toJSON); Three.js objects are skipped.
Transform
Transform is the one trait almost every entity has: position x y z, rotation as a quaternion qx qy qz qw, and scale sx sy sz. Gameplay writes it and the render plugin copies it to Three.js, interpolated between fixed steps.
ts
setYaw(t, angle) // rotation about Y, radians
setEuler(t, x, y, z) // XYZ Euler angles, radians
setAngle2D(t, angle) // rotation about Z, for 2D mode
angle2D(t) // read the Z angle backMath helpers come along: vec3, origin, distSq, dist, clamp, lerp. Compare distSq against a squared radius to skip the square root.
Relations
A relation links one entity to another. Kiln ships one, and you can define your own with Koota's relation:
ts
/** Scene hierarchy; the child's Transform is local to the parent. */
ChildOf // relation({ exclusive: true, autoDestroy: 'orphan' })
/** Who this turret is aiming at. */
export const Targets = relation({ exclusive: true })
commands.spawn(Transform({ y: 1 }), Hat, ChildOf(player))
commands.add(turret, Targets(enemy))ChildOf is the scene graph. A child's Transform is relative to its parent, and despawning a parent despawns its children. Unlike Object3D.add, the hierarchy survives snapshots and hot reload.
Queries
A query is a list of traits. An entity matches if it has all of them:
ts
[Mut(Transform), Velocity] // has Transform and Velocity; writes Transform
[Transform, IsEnemy, Not(Dead)] // has Transform and IsEnemy, but not Dead
[Transform, Or(IsCoin, IsGem)] // has Transform and at least one of IsCoin, IsGem
[Transform, Changed(Health)] // Health changed since this system last ran
[Added(IsEnemy)] // gained IsEnemy since this system last ran
[Removed(IsEnemy)] // lost IsEnemy (or was despawned) since this system last ranMut(T)marks a trait the system writes. See Systems.- Tags (
IsEnemy),Not,Orand the change filters narrow the match but give no row entry. Changed,AddedandRemovedget a separate tracker per system, so two systems watching the same trait never steal each other's changes.
Commands
Structural changes (spawning, despawning, adding and removing traits) never happen in the middle of a system. They go through commands, which queue them and apply them in order at the end of the current phase:
ts
const bullet = commands.spawn(Bullet, { at, dir }) // a prefab and its params
commands.spawn(Transform({ x: 1 }), Velocity) // or a list of traits
commands.despawn(entity) // twice, or on a dead entity: a no-op
commands.add(entity, Stunned, Timer({ duration: 1 }))
commands.remove(entity, Stunned)
commands.run((world) => { /* any world change, applied in order with the rest */ })commands.spawn returns a PendingEntity. Pass it to other commands in the same system (they apply in order, so they see it), or to a tween. Its .entity is set once commands have applied; calling .get() before then throws and explains why.
Inside a system, calling entity.destroy(), entity.add() or world.spawn() directly throws in dev builds with the commands call to use instead.
Outside systems
Tests and tools work on the world directly through the app:
ts
const p = app.spawn(Player, { at: origin }) // immediate, not queued
app.set(p, Transform, { x: 4 })
app.get(p, Transform)?.x // a copy of the values
app.has(p, Health)
app.query(IsEnemy, Not(Dead)) // entities, right now
app.despawn(p)Calling these from inside a system throws: systems use their context. See Scenario tests.