6
\$\begingroup\$

Here's a followup to Minimal entity component system. Please see that question for background.

-- ecs.lua

local unpack = table.unpack or _G.unpack

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

local function getComponentsTable(entity, components)
  local values = {}
  for i, component in ipairs(components) do
    local value = entity[component]
    if value == nil then return end
    table.insert(values, value)
  end
  return values
end

local function getComponents(...)
  return unpack(getComponentsTable(...) or {})
end

local entityPrototype = {
  addComponent = addComponent,
  getComponents = getComponents,
  getComponentsTable = getComponentsTable,
}

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

local function createSystem(components, process)
  local function handle(entity)
    local values = getComponentsTable(entity, components)
    if values then
      process(entity, unpack(values))
    end
  end

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

local function each(entities, components)
  local i = 0
  return function()
    while true do
      i = i + 1
      local entity = entities[i]
      if not entity then return end
      local values = getComponentsTable(entity, components or {})
      if values then return entity, unpack(values) end
    end    
  end
end

return {
  createEntity = createEntity,
  addComponent = addComponent,
  getComponents = getComponents,
  getComponentsTable = getComponentsTable,
  createSystem = createSystem,
  each = each,
}

This version adds the following features:

  • System callbacks now receive not only the entity, but also any components listed in the first argument to createSystem. In other words you can do this now:

    local rectangleRenderingSystem = ecs.createSystem(
      { 'position', 'rectangle' },
      function (entity, pos, rect)
        ui.drawRect(pos, rect)
      end)
    

    This is nice, because if you switch from string-keyed components to table-keyed components (using addComponent), nothing in the function body needs to change.

  • There is a new function getComponents which does something similar.

    local foo, bar = ecs:getComponents(someEntity, { 'foo', 'bar' })
    -- or
    local foo, bar = someEntity:getComponents({ FooComponent, BarComponent })
    
  • There is a new function each which is pretty useful. It takes an array of entities as its first argument, and an array of components as the second argument. It returns a function which iterates over all of the entities having matching components, returning the entity followed by each component in the list, just like the argument lists the systems callbacks receive. In other words you can do something like this:

         -- stop all the monsters
         -- we don't care about the value of the monster component, only that it exists
         for entity, vel in ecs.each(entities, { 'velocity', 'monster' }) do
           vel.vx = 0
           vel.vy = 0
         end
    

For a usage example, see Pong game built on a minimal entity component system.

\$\endgroup\$
4
\$\begingroup\$

I think your code is very well written, and understandable (even for me, although I'm currently only beginning to learn Lua, and I heard about ECS for the first time now :) )

I would suggest some minor improvements:

  • Document your methods, including parameters and return types.
  • In getComponentsTable add a comment explaining, why you break the loop if you hit a nil value (that was not clear for me, how we know that there are for sure no interesting values after this point)
  • Some parameters are ensured against nil (using or {}), but this is not the case for entity/entities. I suggest, either doing the same, or adding an explicit check for nil at the beginning of the method (and raising an error in case of nil).
  • The each function iterates indefinitely, until it finds a nil value in the array (I suppose, this means that it reads past the last element). Instead of this, why not just iterate over all the elements of the array, with a normal for loop? I don't see any good reason, but if there is one, I'd document it in a comment.
  • In some of the for loops, the variable i is not used. The Lua Style Guide recommends using underscores (_) for such cases.

Finally, one more thing: it is really useful that you have a working example for your system. However, I suggest to consider also adding unit tests, for more reasons: - they ensure correctness if you need to do changes - they help other to understand the code better - it is possible them to run/verify them in an automated way (which is not the case for the example)

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.