5.1.34. Entity Component System (DECS)
This tutorial covers the decs module — daslang’s built-in Entity Component
System. DECS provides a lightweight ECS where entities are identified by
EntityId, carry dynamically typed components, and are grouped into
archetypes based on their component sets. All mutations are deferred and
applied on commit().
require daslib/decs_boost
5.1.34.1. Core concepts
Entity — an
EntityIdwith associated component data.Component — a named, typed value attached to an entity (set via
:=on aComponentMap).Archetype — a storage bucket for entities with the same set of component names. Created automatically.
Deferred execution —
create_entity,delete_entity, andupdate_entityare all deferred; callcommit()to apply them.
5.1.34.2. Creating entities
create_entity takes a block that receives the EntityId and a
ComponentMap. Use := to set components:
let player = create_entity() @(eid, cmp) {
cmp.name := "hero"
cmp.hp := 100
cmp.pos := float3(0, 0, 0)
}
commit() // entity becomes visible
5.1.34.3. Querying entities
Global queries iterate all entities matching the listed component types. Component names in the block signature match component names on entities:
query() $(name : string; hp : int; pos : float3) {
print(" {name}: hp={hp} pos={pos}\n")
}
Query a specific entity by passing its EntityId:
query(eid) $(tag : string; val : int) {
print("Found: tag={tag} val={val}\n")
}
5.1.34.4. Mutable queries
Use var and & to modify components in place:
query() $(var pos : float3&; vel : float3) {
pos += vel
}
5.1.34.5. REQUIRE and REQUIRE_NOT
Filter queries to entities that have (or lack) specific components, even when you don’t need those components as block arguments:
// Only entities WITH a "weapon" component
query <| $ [REQUIRE(weapon)] (name : string; hp : int) {
print(" {name} hp={hp}\n")
}
// Exclude entities WITH a "shield" component
query <| $ [REQUIRE_NOT(shield)] (name : string) {
print(" {name}\n")
}
5.1.34.6. find_query
find_query stops iteration when the block returns true, providing
an early-exit search:
let found = find_query() $(idx : int) {
if (idx == 7) {
return true
}
return false
}
5.1.34.8. Updating entities
update_entity lets you modify, add, or remove components. If the component
set changes, the entity moves to a different archetype:
update_entity(eid) @(eid, cmp) {
var hp = 0
hp = get(cmp, "hp", hp)
cmp |> set("hp", hp - 25)
cmp.enraged := true
}
commit()
// Remove a component
update_entity(eid) @(eid, cmp) {
cmp |> remove("enraged")
}
commit()
5.1.34.9. Default values in queries
If an entity lacks a queried component, the default value is used. Parameters
with defaults must be const (no var, no &):
query() $(name : string; alpha : float = 0.5) {
print(" {name}: alpha={alpha}\n")
}
5.1.34.10. Templates
[decs_template] structs map struct fields to components with an automatic
prefix (StructName_ by default). This generates apply_decs_template
and remove_decs_template functions:
[decs_template]
struct Particle {
pos : float3
vel : float3
life : int
}
// Create entity with template
create_entity() @(eid, cmp) {
apply_decs_template(cmp, Particle(
pos = float3(0, 0, 0),
vel = float3(1, 0, 0),
life = 100
))
}
// Query using template struct
query() $(var p : Particle) {
p.pos += p.vel
p.life -= 1
}
5.1.34.11. Stage functions
Stage functions are annotated with [decs(stage=name)] and become queries
that run when you call decs_stage("name"). The stage commits automatically
before and after running all registered functions:
[decs(stage = simulate)]
def simulate_particles(var p : Particle) {
p.pos += p.vel
p.life -= 1
}
// Run 3 simulation steps
for (step in range(3)) {
decs_stage("simulate")
}
5.1.34.12. Nested queries
Queries can be nested — the inner query sees all matching entities:
query() $(name : string) {
var sum = 0
query() $(val : int) {
sum += val
}
print(" {name} sees total val={sum}\n")
}
5.1.34.13. Serialization
The entire ECS state can be saved and restored using the archive module:
require daslib/archive
var data <- mem_archive_save(decsState)
restart()
mem_archive_load(data, decsState)
5.1.34.14. Entity ID recycling
When an entity is deleted, its ID slot is recycled. The generation counter
increments so that stale EntityId values cannot accidentally access the
new entity occupying the same slot:
let eid1 = create_entity() @(eid, cmp) {
cmp.val := 1
}
commit()
delete_entity(eid1)
commit()
let eid2 = create_entity() @(eid, cmp) {
cmp.val := 2
}
commit()
// eid2.id == eid1.id but eid2.generation != eid1.generation
5.1.34.15. Archetype inspection
You can inspect the world state through decsState:
print("Archetypes: {length(decsState.allArchetypes)}\n")
for (arch in decsState.allArchetypes) {
print(" {arch.size} entities, components:")
for (c in arch.components) {
print(" {c.name}")
}
print("\n")
}
5.1.34.16. Utility functions
is_alive checks whether an EntityId still refers to a living entity.
It returns false for INVALID_ENTITY_ID, deleted entities, and stale
generation IDs after slot recycling:
print("alive? {is_alive(hero)}\n") // true
delete_entity(hero)
commit()
print("alive? {is_alive(hero)}\n") // false
entity_count returns the total number of alive entities across all
archetypes:
print("entity count: {entity_count()}\n")
get_component retrieves a single component value by entity ID and name.
The type is inferred from the default value parameter. If the entity is dead
or the component is not present, the default is returned:
let hp = get_component(hero, "hp", 0) // returns int
let pos = get_component(hero, "pos", float3(0)) // returns float3
let missing = get_component(hero, "shield", -1) // returns -1 (not found)
let dead = get_component(deleted_eid, "hp", -999) // returns -999 (dead)
See also
Full source: tutorials/language/34_decs.das
Previous tutorial: Algorithm
Next tutorial: Job Queue (jobque)
Structures — struct language reference.
Iterators — iterator language reference.
Lambda — lambda language reference.