This is the ECS system used in the demo, it assumes table.remove|.insert|.sort to be available):
-- ** HoverECS **
-- ecsEntities: {eid = {comps = comps_list, cdata = comps_data}, ..}
-- ecsDeadEntities: {eid1, .. }
-- ecsComponent: { name = { data }, .. }
-- ecsSystem: { {proc, ent_buckid}, .. }
-- ecsBucketsList: { bucket_id = comps_list, .. }
-- ecsBuckets: { bucket_id = {ent0id, ent1id, ..}, bucket_id = {ent2id, ent5id, ..}, .. }
-- API:
-- * UpdateECS(): runs all update systems
-- * DrawECS(): runs all draw systems
-- * DefineComponent(name, comp_data)
-- * DefineUpdateSystem(comps_list, system_proc)
-- * DefineDrawSystem(comps_list, system_proc)
-- * SpawnEntity(comps_list)
-- * GetEntComps(eid): returns entity comp_data
-- * GetEntComp(eid, comp_name): returns comp_name data
-- * EntAddComp(eid, comp_name): adds component to existing entity
-- * EntRemComp(eid, comp_name): remove component from existing entity
-- * KillEntity(eid)
-- * KillAllEntities()
-- * IsDeadEntity(eid)
-- * CreateComp(comp_name): creates component without adding to entity
-- * CollectEntsWith(comps_list): collects all entities that have comps_list
-- * CountLiveEnts(): returns number of live entities
--
-- Params guide:
-- * name: a unique string name of a component
-- * comp_data: table that holds all component's properties
-- * comps_list: table-list of components names
-- * system_proc: function call with signature `FuncName(ent)`
-- * eid: entity id number
-- * comp_name: unique string name of a component
--
-- Limitations:
-- * entities can only have one component per component name
-- * KillAllEntities() removes entities instantly which could be problematic
-- * entities that depend on other entities must check IsDeadEntity(other)
-- * CollectEntsWith() is very expensive so don't call several times per frame
-- There are probably more limitations
ecsEntityId = 1
ecsBucketId = 1
ecsEntities = {}
ecsDeadEntities = {}
ecsComponents = {}
ecsUSystems = {}
ecsDSystems = {}
ecsBucketsList = {}
ecsBuckets = {}
function ecsNextEntityId()
ecsEntityId = ecsEntityId + 1
return ecsEntityId - 1
end
function ecsNextBucketId()
ecsBucketId = ecsBucketId + 1
return ecsBucketId - 1
end
-- Returns existing bucket id or new one
function ecsGetBucket(comps_list)
local i, bl, found
local buckid = nil
for i, c in ipairs(ecsBucketsList) do
if ecsCompEq(comps_list, c) then
buckid = i
break
end
end
if buckid == nil then
buckid = ecsNextBucketId()
ecsBuckets[buckid] = {}
ecsBucketsList[buckid] = comps_list
end
return buckid
end
-- Get all compatible buckets to passed components list
function ecsGetCompatBuckets(comps_list)
-- bucket(subset) in comps_list(set)
local buckids = {}
for i, c in ipairs(ecsBucketsList) do
if ecsCompIn(c, comps_list) then
add(buckids, i)
end
end
return buckids
end
-- Compare two component lists for exact match (non-sorted)
function ecsCompEq(comp1, comp2)
if #comp1 ~= #comp2 then return false end
for i = 1, #comp1 do
if comp1[i] ~= comp2[i] then
return false
end
end
return true
end
function ecsCompEqSort(comp1, comp2)
table.sort(comp1)
table.sort(comp2)
return ecsCompEqual(comp1,comp2)
end
function ecsCreateComp(comp)
local newcomp = {}
for i, v in pairs(comp) do
newcomp[i] = v
end
return newcomp
end
-- Return true if subset is in set
function ecsCompIn(subset, set)
if #subset > #set then return false end
local i, j, found
found = 0
for i = 1, #subset do
for j = 1, #set do
if subset[i] == set[j] then
found = found + 1
break
end
end
end
return found == #subset
end
function ecsExecSystem(system)
local i, e
for i, e in ipairs(ecsBuckets[system.ent_buckid]) do
system.proc(e)
end
end
function ecsExecSystems(systems)
local i, s
for i, s in ipairs(systems) do
ecsExecSystem(s)
end
end
function ecsCloneTable(t)
local tt = {}
for i, v in pairs(t) do tt[i] = v end
return tt
end
function ecsRemEntFromBuckets(eid)
local i, j
for i=1,#ecsBuckets do
local cb = ecsBuckets[i]
for j=1,#cb do
if cb[j] == eid then
table.remove(cb, j)
break
end
end
end
end
function ecsAddEntToBuckets(eid, comps_list)
local cbucks = ecsGetCompatBuckets(comps_list)
for i=1,#cbucks do
table.insert(ecsBuckets[cbucks[i]], eid)
end
end
function ecsRebucketEnt(eid, oldcomps, newcomps)
ecsRemEntFromBuckets(eid)
ecsAddEntToBuckets(eid, newcomps)
end
function UpdateECS()
ecsExecSystems(ecsUSystems)
end
function DrawECS()
ecsExecSystems(ecsDSystems)
end
function DefineComponent(name, comp_data)
assert(name, comp_data)
ecsComponents[name] = comp_data
end
function DefineUpdateSystem(comps_list, system_proc)
assert(comps_list)
assert(system_proc)
table.sort(comps_list)
local buckid = ecsGetBucket(comps_list)
add(ecsUSystems, {proc = system_proc, ent_buckid = buckid})
end
function DefineDrawSystem(comps_list, system_proc)
assert(comps_list)
assert(system_proc)
table.sort(comps_list)
local buckid = ecsGetBucket(comps_list)
add(ecsDSystems, {proc = system_proc, ent_buckid = buckid})
end
function SpawnEntity(comps_list)
local eid = ecsNextEntityId()
local comps_data = {}
table.sort(comps_list)
local i
for i = 1, #comps_list do
comps_data[comps_list[i]] = ecsCreateComp(ecsComponents[comps_list[i]])
end
ecsEntities[eid] = {
comps = comps_list,
cdata = comps_data
}
local cbucks = ecsGetCompatBuckets(comps_list)
for i = 1, #cbucks do
add(ecsBuckets[cbucks[i]], eid)
end
return eid
end
-- returns a dict of comp->data
function GetEntComps(eid)
assert(eid)
return ecsEntities[eid].cdata
end
-- returns dict of component data
function GetEntComp(eid, comp_name)
assert(eid)
assert(comp_name)
return ecsEntities[eid].cdata[comp_name]
end
-- Adds new comp to entity, 1 comp/name
function EntAddComp(eid, comp_name)
assert(not ecsEntities[eid].cdata[comp_name] and ecsComponents[comp_name])
oldcomps = GetEntComps(eid)
newcomps = ecsCloneTable(oldcomps)
table.insert(newcomps, comp_name)
table.sort(newcomps)
ecsEntities[eid].comps = newcomps
ecsEntities[eid].cdata[comp_name] = ecsCreateComp(ecsComponents[comp_name])
ecsRebucketEnt(eid, oldcomps, newcomps)
end
-- Removes component from entity
function EntRemComp(eid, comp_name)
assert(ecsEntities[eid].cdata[comp_name])
oldcomps = GetEntComps(eid)
newcomps = ecsCloneTable(oldcomps)
for i=1,#newcomps do
if newcomps[i] == comp_name then
table.remove(newcomps, i)
break
end
end
ecsEntities[eid].comps = newcomps
ecsEntities[eid].cdata[comp_name] = nil
ecsRebucketEnt(eid, oldcomps, newcomps)
end
function KillEntity(eid)
ecsRemEntFromBuckets(eid)
ecsEntities[eid] = nil
table.insert(ecsDeadEntities, eid)
end
function KillAllEntities()
local i, j
for i=1,#ecsBuckets do
ecsBuckets[i] = {}
end
for i in pairs(ecsEntities) do
table.insert(ecsDeadEntities, ecsEntities[i])
end
ecsEntities = {}
end
function IsDeadEntity(eid)
for i=1,#ecsDeadEntities do
if ecsDeadEntities[i] == eid then
return true
end
end
return false
end
-- Create a comp as data
function CreateComp(comp_name)
for cn in pairs(ecsComponents) do
if cn == comp_name then
return ecsCreateComp(ecsComponents[cn])
end
end
assert(false) -- bad comp name
end
-- Expensive, only use for singleton systems
function CollectEntsWith(comps)
local ents={}
for i in pairs(ecsEntities) do
if ecsCompIn(comps,ecsEntities[i].comps) then
add(ents, i)
end
end
return ents
end
-- Count how many entities are alive
function CountLiveEnts()
local c=0
for i in pairs(ecsEntities) do
c=c+1
end
return c
end