## 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
Tjakka5
Party member
Posts: 240
Joined: Thu Dec 26, 2013 12:17 pm

### Is this a good ECS like approach?

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
Check out my portfolio: http://tjakka5.sorunome.de/
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?

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.
Tjakka5
Party member
Posts: 240
Joined: Thu Dec 26, 2013 12:17 pm

### Re: Is this a good ECS like approach?

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?
Check out my portfolio: http://tjakka5.sorunome.de/
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?

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

### Re: Is this a good ECS like approach?

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.
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?

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

### Re: Is this a good ECS like approach?

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

### Re: Is this a good ECS like approach?

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.
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?

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

### Re: Is this a good ECS like approach?

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.

### Who is online

Users browsing this forum: Bing [Bot] and 43 guests