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.
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.
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.
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).
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.
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
- Scene DSL to create entities in Kotlin.
- Scene documents to load entities from a
*.scene.jsonfile. - App lifecycle for fixed-step and per-frame systems.