From 3a94c6b60df10de5b70dc2950127c3ad004a7b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Thu, 18 Feb 2021 17:17:43 +0100 Subject: [PATCH] ecs overhaul Main incompatibility is passing the table entity[system.name] as first argument in a lot of callbacks --- ecs/ecs.can | 208 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 162 insertions(+), 46 deletions(-) diff --git a/ecs/ecs.can b/ecs/ecs.can index 44ac3f7..6268fd8 100644 --- a/ecs/ecs.can +++ b/ecs/ecs.can @@ -5,10 +5,15 @@ if not loaded then scene = nil end --- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include: -- * ability to nest systems; --- * instanciation of systems for each world; --- * adding and removing entities is done instantaneously. +-- * instanciation of systems for each world (no shared state); +-- * adding and removing entities is done instantaneously +-- * ability to add and remove components from entities after they were added to the world. +let ecs -- TODO: Implement a skip list for faster search. +-- better control over system order: process, draw, methods? (for lag reasons and dependencies) +-- more generic events? +-- populate component? --- Recursively remove subsystems from a system. let recDestroySystems = (system) @@ -31,6 +36,17 @@ let recCallOnRemoveFromWorld = (world, systems) end end +--- Recursively get a list of systems with a certain method. +let recGetSystemsWithMethod = (method, systems, l={}) + for _, s in ipairs(systems) do + if s[method] then + table.insert(l, s) + end + recGetSystemsWithMethod(method, s.systems, l) + end + return l +end + --- Iterate through the next entity, based on state s: { previousLinkedListItem } let nextEntity = (s) if s[1] then @@ -42,16 +58,32 @@ let nextEntity = (s) end end +--- Recursively content of a into b if it isn't already present. No cycle detection. +let copy = (a, b) + for k, v in pairs(a) do + if type(v) == "table" then + if b[k] == nil then + b[k] = {} + copy(v, b[k]) + elseif b[k] == "table" then + copy(v, b[k]) + end + elseif b[k] == nil then + b[k] = v + end + end +end + --- System fields and methods. -- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system"). -- Instancied systems can be retrieved in system.s or system.systems. --- Oh, the "world" is just the top-level system. +-- Oh, the "world" is just the top-level system, behaving in exactly the same way as other systems. let system_mt = { --- Read-only after creation system options --- -- I mean, you can try to change them afterwards. But, heh. --- Name of the system (optional). - -- Used to create a field with the system's name in world.system. + -- Used to create a field with the system's name in world.s. name = nil, --- List of subsystems. @@ -59,7 +91,7 @@ let system_mt = { systems = nil, --- Returns true if the entity should be added to this system (and therefore its subsystems). - -- If this is a string, it will be converted to a filter function on instanciation using ecs.all. + -- If this is a string or a table, it will be converted to a filter function on instanciation using ecs.all. -- By default, rejects everything. filter = :(e) return false end, --- Returns true if e1 <= e2. @@ -68,9 +100,9 @@ let system_mt = { --- Modifiable system options --- --- Called when adding an entity to the system. - onAdd = :(e) end, + onAdd = :(s, e) end, --- Called when removing an entity from the system. - onRemove = :(e) end, + onRemove = :(s, e) end, --- Called when the system is instancied, before any call to :onnAddToWorld (including other systems in the world). onInstance = :() end, --- Called when the system is added to a world. @@ -84,9 +116,9 @@ let system_mt = { --- Called when drawing the system. onDraw = :() end, --- Called when updating the system, for every entity the system contains. Called after :onUpdate was called on the system. - process = :(e, dt) end, + process = :(s, e, dt) end, --- Called when drawing the system, for every entity the system contains. Called after :onDraw was called on the system. - render = :(e) end, + render = :(s, e) end, --- If not false, the system will only update every interval seconds. interval = false, @@ -95,50 +127,75 @@ let system_mt = { --- The system and its subsystems will only draw if this is true. visible = true, + --- Defaults value to put into the entities's system table when they are added. Will recursively fill missing values. + default = nil, + --- Read-only system options --- --- The world the system belongs to. world = nil, --- Number of entities in the system. entityCount = 0, - --- Map of named systems in the world (not only subsystems). + --- Map of named systems in the world (not only subsystems). Same for every system from the same world. s = nil, --- Private fields --- - --- First element of the linked list of entities. + --- First element of the linked list of entities: { entity, next_element }. _first = nil, + --- Associative map of entities in the system and their previous linked list element (or true if first element). + -- This make the list effectively a doubly linked list, but with easy access to the previous element using this map (and therefore O(1) deletion). + _previous = nil, --- Amount of time waited since last update (if interval is set). _waited = 0, + --- Metatable of entities' m method table. Same for every system from the same world. + _entity_m_mt = nil, --- Methods --- --- Add entities to the system and its subsystems. + -- Will skip entities that are already in the system. -- Entities are added to subsystems after they were succesfully added to their parent system. -- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters. -- Since :remove will not search for entities in systems where they should have been filtered out, the added entities will not be removed - -- when calling :remove on a parent system or the world. The entity can only be removed by calling :remove on the system :add was called on. + -- when calling :remove on a parent system or the world. The entity can be removed by calling :remove on the system :add was called on. add = :(e, ...) - if e ~= nil and @filter(e) then + if e ~= nil and not @_previous[e] and @filter(e) then + -- setup entity + if not e.m then + e.m = setmetatable({ _entity = e }, @_entity_m_mt) + end + -- add to linked list if @_first == nil then @_first = { e, nil } + @_previous[e] = true elseif @compare(e, @_first[1]) then - @_first = { e, @_first } + let nxt = @_first + @_first = { e, nxt } + @_previous[e] = true + @_previous[nxt[1]] = @_first else let entity = @_first while entity[2] ~= nil do if @compare(e, entity[2][1]) then - entity[2] = { e, entity[2] } + let nxt = entity[2] + entity[2] = { e, nxt } + @_previous[e] = entity + @_previous[nxt[1]] = entity[2] break end entity = entity[2] end if entity[2] == nil then entity[2] = { e, nil } + @_previous[e] = entity end end + -- notify addition @entityCount += 1 - @onAdd(e) + if (@default and e[@name]) copy(@default, e[@name]) + @onAdd(e[@name], e) + -- add to subsystems for _, s in ipairs(@systems) do s:add(e) end @@ -149,35 +206,57 @@ let system_mt = { return e end end, - --- Remove entities to the system and its subsystems. - -- Entities are removed from subsystems after they were succesfully removed from their parent system. - -- If you intend to call this on a subsystem instead of the world, please read the warning in :add. - remove = :(e, ...) - if e ~= nil and @filter(e) then - let found = false - if @_first == nil then - return - elseif @_first[1] == e then - @_first = @_first[2] - found = true - else - let entity = @_first - while entity[2] ~= nil do - if entity[2][1] == e then - entity[2] = entity[2][2] - found = true - break + --- Refresh an entity's systems. + -- Behave similarly to :add, but if the entity is already in the system, instead of skipping it, it + -- will check for new and removed components and add and remove from (sub)systems accordingly. + refresh = :(e, ...) + if e ~= nil then + if not @_previous[e] then + @add(e) + elseif @_previous[e] then + if not @filter(e) then + @remove(e) + else + for _, s in ipairs(@systems) do + s:refresh(e) end - entity = entity[2] end end - if found then - for _, s in ipairs(@systems) do - s:remove(e) - end - @entityCount -= 1 - @onRemove(e) + end + if ... then + return e, @refresh(...) + else + return e + end + end, + --- Remove entities to the system and its subsystems. + -- Will skip entities that are not in the system. + -- Entities are removed from subsystems before they are removed from their parent system. + -- If you intend to call this on a subsystem instead of the world, please read the warning in :add. + -- Returns all removed entities. + remove = :(e, ...) + if e ~= nil and @_previous[e] then + -- remove from subsystems + for _, s in ipairs(@systems) do + s:remove(e) end + -- remove from linked list + let prev = @_previous[e] + if prev == true then + @_first = @_first[2] + if @_first then + @_previous[@_first[1]] = true + end + else + prev[2] = prev[2][2] + if prev[2] then + @_previous[prev[2][1]] = prev + end + end + -- notify removal + @_previous[e] = nil + @entityCount -= 1 + @onRemove(e[@name], e) end if ... then return e, @remove(...) @@ -185,6 +264,15 @@ let system_mt = { return e end end, + --- Returns true if every entity is in the system. + has = :(e, ...) + let has = e == nil or not not @_previous[e] + if ... then + return has and @has(...) + else + return has + end + end, --- Returns an iterator that iterate through the entties in this system. iter = :() return nextEntity, { @_first } @@ -211,7 +299,7 @@ let system_mt = { @onUpdate(dt) if @process ~= system_mt.process then for e in @iter() do - @process(e, dt) + @process(e[@name], e, dt) end end for _, s in ipairs(@systems) do @@ -229,7 +317,7 @@ let system_mt = { @onDraw() if @render ~= system_mt.render then for e in @iter() do - @render(e) + @render(e[@name], e) end end for _, s in ipairs(@systems) do @@ -253,7 +341,9 @@ let recInstanciateSystems = (world, systems) table.insert(t, setmetatable({ systems = recInstanciateSystems(world, s.systems or {}), world = world, - s = world.s + s = world.s, + _previous = {}, + _entity_m_mt = world._entity_m_mt }, { __index = :(k) if s[k] ~= nil then @@ -266,6 +356,8 @@ let recInstanciateSystems = (world, systems) let system = t[#t] if type(s.filter) == "string" then system.filter = (_, e) return e[s.filter] ~= nil end + elseif type(s.filter) == "table" then + system.filter = ecs.all(unpack(s.filter)) end if s.name then world.s[s.name] = system @@ -287,15 +379,38 @@ let alwaysTrue = () return true end let alwaysFalse = () return true end --- ECS module. -let ecs = { +ecs = { --- Create and returns a world system based on a list of systems. -- The systems will be instancied for this world. -- @impl ubiquitousse world = (...) let world = setmetatable({ filter = ecs.all(), - s = {} + s = {}, + _previous = {}, + _entity_m_mt = setmetatable({}, { + __index = (_entity_m_mt, k) + let s = recGetSystemsWithMethod(k, {world}) + _entity_m_mt[k] = (m, ...) + let e = m._entity + let args = {...} + for _, sys in ipairs(s) do + if sys._previous[e] then + let r = { sys[k](sys, e[sys.name], e, unpack(args)) } + if r[1] == false then + break + elseif r[1] == true then + args = { select(2, unpack(r)) } + end + end + end + return unpack(args) + end + return _entity_m_mt[k] + end + }) }, { __index = system_mt }) + world._entity_m_mt.__index = world._entity_m_mt world.world = world world.systems = recInstanciateSystems(world, {...}) recCallOnAddToWorld(world, world.systems) @@ -341,6 +456,7 @@ let ecs = { --- If uqt.scene is available, returns a new scene that will consist of a ECS world with the specified systems and entities. -- @impl ubiquitousse scene = (name, systems={}, entities={}) + assert(scene, "ubiquitousse.scene unavailable") let s = scene.new(name) let w