Architectural Discussion: Implementing ECS and Data-Oriented Programming (DOP)
- Posted by euphoric (admin) 3 weeks ago
- 297 views
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.

