Architectural Discussion: Implementing ECS and Data-Oriented Programming (DOP)

new topic     » topic index » view thread      » older message » newer message

Hey everyone,

Over the last few years, programming patterns in game engines and distributed backend systems have swung heavily away from classic Object-Oriented inheritance toward Data-Oriented Programming (DOP) and Entity-Component-System (ECS) architectures.

The core tenets of DOP (championed by books like Yehonathan Sharvit's Data-Oriented Programming, or the Clojure ecosystem) are:

Separate code from data: Pure functions/procedures operate on separate, passive data structures.

Represent data with generic structures: Instead of rigid classes or fixed structs, pass generic associative maps/dictionaries between functions.

Treat data as immutable: State transitions return new or modified data rather than mutating in place.

Separate schema validation from data representation: Data remains flexible maps; validation happens optionally at boundaries (like Euphoria's custom type system).

In modern ECS, this lines up nicely:

Entities: Pure integer IDs.

Components: Plain data records/maps (e.g., Position, Velocity, Renderable).

Systems: Pure logic/functions that query entities containing specific component signatures and process them.

Lately, I've been transitioning my own architectural style toward passing maps between everything-treating data as flexible dictionaries rather than tightly coupled records. Since OpenEuphoria has native map support via std/map.e, I started exploring how clean an ECS/DOP implementation would actually look in modern Euphoria.

Quick Implementation Spike

In OpenEuphoria, std/map.e implements hash tables where map variables are handles referencing dynamic internal storage. Combining std/map.e with sequence indices gives us a clean way to model entities and systems:

OpenEuphoria: Euphoria v4.0

include std/map.e as map 
 
-- 1. Component Stores: Map component keys to entity data 
constant COMPONENT_POSITION = "pos" 
constant COMPONENT_VELOCITY = "vel" 
 
-- World state can just be a map of component stores 
map:map world = map:new() 
map:put(world, COMPONENT_POSITION, map:new()) 
map:put(world, COMPONENT_VELOCITY, map:new()) 
 
-- 2. Entity Creation: Entities are just unique IDs 
integer next_entity_id = 1 
 
function create_entity() 
    integer id = next_entity_id 
    next_entity_id += 1 
    return id 
end function 
 
-- Helper: attach component map to entity 
procedure add_component(integer entity, sequence comp_name, map:map comp_data) 
    map:map store = map:get(world, comp_name) 
    map:put(store, entity, comp_data) 
end procedure 
 
-- 3. Spawning an entity using generic data maps (pure DOP style) 
integer player = create_entity() 
 
map:map pos = map:new() 
map:put(pos, "x", 10.0) 
map:put(pos, "y", 25.0) 
add_component(player, COMPONENT_POSITION, pos) 
 
map:map vel = map:new() 
vel = map:put(vel, "dx", 1.5) 
vel = map:put(vel, "dy", 0.0) 
add_component(player, COMPONENT_VELOCITY, vel) 
 
-- 4. System: MovementSystem (Pure procedure operating over data) 
procedure movement_system(atom dt) 
    map:map positions  = map:get(world, COMPONENT_POSITION) 
    map:map velocities = map:get(world, COMPONENT_VELOCITY) 
     
    -- Query: entities with velocities 
    sequence moving_entities = map:keys(velocities) 
     
    for i = 1 to length(moving_entities) do 
        integer id = moving_entities[i] 
         
        if map:has(positions, id) then 
            map:map p = map:get(positions, id) 
            map:map v = map:get(velocities, id) 
             
            atom new_x = map:get(p, "x") + (map:get(v, "dx") * dt) 
            atom new_y = map:get(p, "y") + (map:get(v, "dy") * dt) 
             
            map:put(p, "x", new_x) 
            map:put(p, "y", new_y) 
        end if 
    end for 
end procedure 

Questions for the Forum:

DOP & Passing Maps Everywhere:

For those who have leaned into passing std/map.e instances around as the primary payload between routines: how do you manage lifecycle and memory? Since maps in Euphoria use reference handles created via map:new(), are you manually calling map:delete() to reclaim memory, or do you stick to standard Euphoria nested sequences ({"key", value}) to let the garbage collector/reference counter manage disposal automatically?

Map Overhead vs. Flat Sequences: In C++ or Rust, ECS thrives on cache lines and contiguous memory arrays. In Euphoria, std/map.e is convenient for sparse components (e.g., only 10 out of 5,000 entities have a Flying or ParticleEmitter component). But in tight game/processing loops (60 FPS or large batches), does hashing via map:get() and map:put() become too slow compared to indexing flat sequences like positions_x[entity_id]?

Copy-on-Write vs. In-Place Map Mutation: One of the core DOP rules is state immutability, but in Euphoria, passing massive nested sequences and modifying deeply nested nodes can trigger copy-on-write branching. Does using std/map.e mitigate this by acting as a mutable handle, or does it end up being a performance bottleneck of its own?

Schema Validation via type:

DOP advocates checking schemas at boundaries rather than baking them into classes. Has anyone written custom Euphoria type definitions that validate std/map.e structures (e.g., ensuring a map passed to a system contains specific keys like "x" and "y") without burning too many cycles on every function call?

Prior Art: Has anyone built a reusable entity-manager, blackboard, or DOP-style pipeline using std/map.e in 4.1?

Curious to hear how other Euphoria devs approach data-first architecture when building larger apps.

new topic     » topic index » view thread      » older message » newer message

Search



Quick Links

User menu

Not signed in.

Misc Menu