Skip to content

ECS

The entity component system (ECS) holds your game's state: a World of entities, the components attached to them, and the systems that update them every frame.

module: com.awakekt.awake:ecs Desktop · Android · iOS · Web

Everything else in a scene sits on top of this. The scene DSL and scene documents both create entities and components in a World, and the scene runtime calls your systems on it. The ECS itself has no dependencies on rendering, physics or UI.

Define components

A component is any Kotlin object. There is no base class and no registration step. Use a class with var fields for data a system changes, and a data object implementing EcsTag for a marker with no data.

Kotlin
data class Position(var x: Float = 0f, var y: Float = 0f)

data class Velocity(var dx: Float = 0f, var dy: Float = 0f)

data object Frozen : EcsTag

Create entities

An Entity is a handle. World.create() makes one; add, get, has and remove work with one component type at a time. add replaces a component of the same type that is already there.

Kotlin
val world = World()
val player = world.create()
world.add(player, Position(0f, 0f))
world.add(player, Velocity(dx = 2f))

val position = world.get<Position>(player) // null when absent
val moving = world.has<Velocity>(player)
world.remove<Velocity>(player)

world.destroy(player)
val alive = world.isAlive(player) // false: this handle never comes back

A destroyed entity's id is reused later, but with a new generation, so an old handle keeps reading as not alive instead of pointing at the new entity.

Write a system

A System has one method, update(world, delta). queryEach visits every entity that has all the listed component types (one or two), and hands you the components directly.

Kotlin
class MovementSystem : System {
    override fun update(world: World, delta: Float) {
        world.queryEach<Position, Velocity> { _, position, velocity ->
            position.x += velocity.dx * delta
            position.y += velocity.dy * delta
        }
    }
}

The ECS has no scheduler. Call update yourself, or register the system with the scene runtime, which calls it at the right point in the frame (see App lifecycle).

Kotlin
val world = World()
val ball = world.create()
world.add(ball, Position())
world.add(ball, Velocity(dx = 1f, dy = 2f))

val movement = MovementSystem()
movement.update(world, 0.5f)

Query more than two types

world.family { } builds a set of entities that the world keeps current as components come and go. Use all for types an entity must have, one for "at least one of", and exclude for types it must not have. A general family hands back entities only, so read components with world.get.

Kotlin
class FrozenAwareMovementSystem(world: World) : System {
    // Built once and kept up to date by the world, so update() does no matching.
    private val movers = world.family {
        all(Position::class, Velocity::class)
        exclude(Frozen::class)
    }

    override fun update(world: World, delta: Float) {
        movers.forEach { entity ->
            val position = world.get<Position>(entity) ?: return@forEach
            val velocity = world.get<Velocity>(entity) ?: return@forEach
            position.x += velocity.dx * delta
        }
    }
}

World methods

Call Returns What it does
create() Entity A new entity with no components.
destroy(entity) Boolean Removes the entity and all its components.
isAlive(entity) Boolean Whether this exact handle is still live.
add(entity, component) previous component or null Attaches or replaces a component.
get<T>(entity) T? The component, or null.
has<T>(entity) Boolean Whether the entity has a T.
remove<T>(entity) removed component or null Detaches a component.
query(vararg types) List<Entity> Entities that have every listed type. Allocates a list.
queryEach<A> { entity, a -> } Visits every entity with an A, without allocating.
queryEach<A, B> { entity, a, b -> } Visits every entity with an A and a B.
family<A>(), family<A, B>() Family1, Family2 A kept, typed set you can hold as a field and iterate each frame.
family { all(…); one(…); exclude(…) } Family A kept set for any combination of types.
componentTypes(entity) List<KClass> What the entity carries, for inspectors and debug views.
registerPool<T> { … } A factory for pooled instances, used by add<T>(entity) with no value.

How it works

Each component type has its own store. Families are updated when a component is added or removed, so iterating one is a walk over a packed array with no matching work. The cost moves to structural changes instead: adding and removing components is slower than reading them, which suits games that read every frame and change structure rarely.

When a type has a pool (registerPool), a removed instance goes back to the pool, and if it implements Poolable its reset() runs first, so it does not carry old state into its next use.

One thread

The ECS is not thread-safe. Drive a World from one thread, the one the scene runtime runs on.

Don't allocate in update

update runs every frame. Prefer queryEach or a family you keep as a field over query, which builds a new list each call, and reuse scratch vectors in fields rather than creating new ones.

See also