Page 1 of 3

Is this a good ECS like approach?

Posted: Thu Sep 15, 2016 12:49 pm
by Tjakka5
Heya,

Lately I've been very interested in ECS, and while I've tried my best to learn it, I'm having a hard time understanding it.
Thus I was wondering if you guys could look trough the code, and point me in the right direction both design and performance wise.

I have a few snippets for some components and systems here. How they work internally can be seen in the .love.

Position component:

Code: Select all

local Component = require "modules.component"

--[[
   int x*
   int y*
]]
Component(
   "Position",
   function(entity, x, y)
      -- Constructor
      entity.x = x or 0
      entity.y = y or 0
   end,
   function(entity)
      -- Destructor
      entity.x = nil
      entity.y = nil
   end
)
Size component:

Code: Select all

local Component = require "modules.component"

--[[
   int w*
   int h*
]]
Component(
   "Size",
   function(entity, w, h)
      -- Constructor
      entity.w = w or 0
      entity.h = h or 0
   end,
   function(entity)
      -- Destructor
      entity.w = nil
      entity.h = nil
   end
)
Rectangle system:

Code: Select all

local System = require "modules.system"

--[[
   Draws a rectangle

   Position
   Size
]]
System(
   "Rectangle",
   function(entity, dt)
      -- Update
   end,
   function(entity)
      -- Draw
      if entity.hasComponents({"Position", "Size"}) then
         love.graphics.rectangle("fill", entity.x, entity.y, entity.w, entity.h)
      end
   end
)

Re: Is this a good ECS like approach?

Posted: Thu Sep 15, 2016 1:20 pm
by airstruck
It seems like you're doing more work than you need to.

The simplest approach would be entities as tables, components as fields of those tables, and systems as functions that operate on components. What advantages does your approach have over something like this?

Code: Select all

-- system/draw.lua

local Draw = {}

function Draw.rectangle (entity)
    if not (entity.x and entity.y and entity.w and entity.h) then return end -- check required components
    lg.rectangle('line', entity.x, entity.y, entity.w, entity.h)
end

return Draw

-- main.lua

local Update = require 'system.update'
local Draw = require 'system.draw'

local entities = {}

entities[1] = { x = 0, y = 0, w = 10, h = 10 } -- x,y,w,h are individual components.

function love.update (dt)
    for _, entity in ipairs(entities) do
        Update.someSystem(entity, dt)
        Update.anotherSystem(entity, dt)
    end
end

function love.draw ()
    for _, entity in ipairs(entities) do
        Draw.rectangle(entity)
        Draw.anotherSystem(entity)
        Draw.yetAnotherSystem(entity)
    end
end

Re: Is this a good ECS like approach?

Posted: Thu Sep 15, 2016 2:01 pm
by Tjakka5
Potential advantages I think this has is readability, and making a big project easier to manage in the long run.
I also realize that this is not the "right" way to do ECS, since data would not be stored in the entity itself.

I guess the thing I'm really asking is; will this approach work, and are there ways I can improve it?

Re: Is this a good ECS like approach?

Posted: Thu Sep 15, 2016 3:03 pm
by airstruck
Tjakka5 wrote:Potential advantages I think this has is readability
I guess that's subjective, neither one seems more or less readable to me.
and making a big project easier to manage in the long run.
Why?

What do you think the disadvantages are? How do you think they weigh against the advantages? Don't answer that right away, just think about it. I think I see a few disadvantages, but they might not be a problem in your particular use cases.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 2:07 am
by raidho36
Small nitpicking; unless the code is not performance-critical you shouldn't use ipairs, instead use a for loop. Performance difference can be pretty big even for complex computations, and for something trivial it may be as large as 15 times over.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 4:32 am
by airstruck
That's true. I went with ipairs in this little example for readability (which apparently didn't do much to help my case anyway). I'd personally use numeric "for" here in my own code, but I'm not sure how much it really matters. In "real code" I've never been able to notice a difference (probably another bottleneck eclipsing it), and even in performance tests I don't remember ever getting more than 3x performance out of numeric "for" over ipairs. I do remember custom generic iterators being reaaaaally slow compared to ipairs, though. At least, I wasn't able to write one that got anywhere near the speed of ipairs.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 5:19 am
by raidho36
The ipairs iterator checks table state every iteration? There's a function call overhead? Generic for loops get compiled and initializing statements get computed in the beginning only. Also static length short reasonably simple loops may get unrolled, it can't run any faster than that. It depends a lot on particular case. However, as the saying goes, he who laughs at ounces cries over pounds. If you can save few cycles just by doing things slightly differently, you shouldn't waste such opportunity.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 5:32 am
by ivan
Hello,
I believe there is a simpler approach to components that doesn't create as many tables and doesn't require checks like "if not (entity.x and entity.y and entity.w and entity.h) then return end -- check required components".
Here is the approach that I use:

Code: Select all

local ecs = {}
local _components = {}

function ecs.getComponent(c, e)
  local _c = _components[c]
  if e == nil then
    return _c
  end
  return _c and _c[e] or nil
end

function ecs.setComponent(c, e, v)
  local _c = _components[c]
  if _c == nil then
    _c = {}
    _components[c] = _c
  end
  _c[e] = v
end

return ecs
That's all you need really.
All you have to do is set/unset the components whenever you create a new entity.
Note that entities are simply represented as ids, and there is no table associated with each entity:

Code: Select all

function createObject(id, x, y, w, h)
  local pt = { x = x, y = y }
  ecs.setComponent('position', id, pt)
  ecs.setComponent('width', id, w)
  ecs.setComponent('height', id, h)
end

function destroyObject(id)
  ecs.setComponent('position', id, nil)
  ecs.setComponent('width', id, nil)
  ecs.setComponent('height', id, nil)
end
For your "systems" class all you have to do is:

Code: Select all

function drawSystemSync()
  -- get all entities with a position
  local drawable = ecs.getComponent('position')
  for id, pt in pairs(drawable) do
    local w = ecs.getComponent('width', id)
    local h = ecs.getComponent('height', id)
    love.graphics.rectangle("fill", pt.x, pt.y, w, h)
  end
end
Note that this approach uses one table per component TYPE which is more efficient than creating one table per object.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 5:33 am
by airstruck
ivan wrote:for id, pt in pairs(drawable) do
I don't get it, you do all that for performance and then use "pairs," aren't you just killing all your other performance gains there?
local drawable = ecs.getComponent('position')
What do you do if you have a system that requires two components, like "position" and "velocity?"

What do you do about systems that require a component *not* be present?

What do you about optional components, that aren't required by a system but are used if present?

This seems like a lot of trouble to go through in the name of performance, have you actually perf'd this against the "simple approach?" Note that if components are primitive values they don't create *any* extra tables in the "simple approach" (obviously). Although, entities will need to be tables, where I guess they can just be ids in your example. I'm skeptical about pairs ruining everything, though.

@raidho, just to clarify, generic "for" is for..in, like what's used with ipairs, numeric "for" is the other thing, usually used with #. What I was saying was numeric "for" is a bit faster than generic "for," but generic "for" with ipairs is much faster than generic "for" with any hand-rolled ipairs-like-thing I could muster (i.e. write or find on the internet). I guess all that's a bit off-topic, anyway. The main point of the example was to suggest that not much is to be gained from all the Entity, Component, and System classes in the OP over just using plain old table constructors and functions.

Re: Is this a good ECS like approach?

Posted: Fri Sep 16, 2016 5:44 am
by raidho36
Well if you shove it all in the single contiguous table, update iteration process should be faster due to better locality. Adding and removing components will be painful though, if you attempt to move the whole table. But you can simply leave unused components in there, eating up update cycles, and move in-use components from the end of the table into the holes. I guess you can limit this operation to few moves per cycle, otherwise performance wouldn't be a whole lot better than if you move the whole table. And of course it's a good bit more elaborate than just keeping a hash-table of references to your objects, with which you simply insert/remove.

I guess it should be noted that it should be a FFI array of FFI structs, there is no locality to speak of if you use Lua tables allocated on heap.