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 EntityId with associated component data.

  • Component — a named, typed value attached to an entity (set via := on a ComponentMap).

  • Archetype — a storage bucket for entities with the same set of component names. Created automatically.

  • Deferred executioncreate_entity, delete_entity, and update_entity are all deferred; call commit() 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.7. Deleting entities

delete_entity is deferred until commit():

delete_entity(eid)
commit()

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.