Is this a good ECS like approach?

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Tjakka5
Party member
Posts: 243
Joined: Thu Dec 26, 2013 12:17 pm

Is this a good ECS like approach?

Post 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
)
Attachments
ECS.love
(4.52 KiB) Downloaded 211 times
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Is this a good ECS like approach?

Post 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
Last edited by airstruck on Thu Sep 15, 2016 4:23 pm, edited 1 time in total.
User avatar
Tjakka5
Party member
Posts: 243
Joined: Thu Dec 26, 2013 12:17 pm

Re: Is this a good ECS like approach?

Post 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?
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Is this a good ECS like approach?

Post 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.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Is this a good ECS like approach?

Post 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.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Is this a good ECS like approach?

Post 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.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Is this a good ECS like approach?

Post 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.
User avatar
ivan
Party member
Posts: 1911
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Is this a good ECS like approach?

Post 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.
Last edited by ivan on Fri Sep 16, 2016 5:54 am, edited 5 times in total.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Is this a good ECS like approach?

Post 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.
Last edited by airstruck on Fri Sep 16, 2016 6:00 am, edited 2 times in total.
User avatar
raidho36
Party member
Posts: 2063
Joined: Mon Jun 17, 2013 12:00 pm

Re: Is this a good ECS like approach?

Post 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.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot] and 65 guests