5
\$\begingroup\$

I was trying to figure out what a minimal ECS might look like in Lua; here's what I came up with. This is based very loosely on darkf/microecs.

local function createSystem(components, process)
  local function handle(entity)
    for i, component in ipairs(components) do
      if entity[component] == nil then return end
    end
    process(entity)
  end

  return function(entities)
    for i, entity in ipairs(entities) do
      handle(entity)
    end
  end
end

local function addComponent(entity, component, instance)
  local meta = { __index = component }
  entity[component] = setmetatable(instance or {}, meta)
  return entity
end

local function createEntity(instance)
  local meta = { __index = { addComponent = addComponent } }
  return setmetatable(instance or {}, meta)
end

return {
  createEntity = createEntity,
  addComponent = addComponent,
  createSystem = createSystem,
}

Here's how you could use it:

local ecs = require 'ecs'

local thing = ecs.createEntity()

local positionComponent = { x = 0, y = 0 }
local velocityComponent = { vx = 0, vy = 0 }

thing:addComponent(positionComponent, { x = 50, y = 60 })
thing:addComponent(velocityComponent, { vx = 5, vy = 6 })

local velocitySystem = ecs.createSystem(
  { positionComponent, velocityComponent },
  function (entity)
    local pos = entity[positionComponent]
    local vel = entity[velocityComponent]
    pos.x = pos.x + vel.vx
    pos.y = pos.y + vel.vy
  end)

Interestingly, the entities and components aren't nearly as important as the systems. The createEntity function only exists so that you can use addComponent in an object-oriented way, but you can just as easily write it like this:

local thing = {}
-- ...
ecs.addComponent(thing, positionComponent, { x = 50, y = 60 })
ecs.addComponent(thing, velocityComponent, { vx = 5, vy = 6 })

You can also ignore addComponent if you don't care about the "safety" of having "typed" components.

local thing = {
  position = { x = 50, y = 60 },
  velocity = { vx = 5, vy = 6 },
}

local velocitySystem = ecs.createSystem(
  { 'position', 'velocity' },
  function (entity)
    local pos = entity.position
    local vel = entity.velocity
    pos.x = pos.x + vel.vx
    pos.y = pos.y + vel.vy
  end)

This means systems are useful on arbitrary data structures, with the table representing the entity and its properties representing components. Keying properties with strings like this seems more idiomatic from a Lua standpoint.

To have a system process entities, simply call the function returned from createSystem, passing it a table of entities to process. Only the entities having the components specified in the first argument to createSystem will be processed.

function tick()
   velocitySystem({ thing, anotherThing })
   anotherSystem({ thing, somethingElse })
end

All comments are welcome, but I'm more interested in hearing about the implementation itself than stylistic choices (like whether I should have used _ in place of i, or 4 space indents instead of 2).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.