From 743d662ef9089bb4884d9d83c44c6e0feaeda0cf Mon Sep 17 00:00:00 2001 From: Pablo Ariel Mayobre Date: Tue, 14 Feb 2023 18:14:23 -0300 Subject: [PATCH] Replaced Pools with Filters Filters allow for a Pool constructor (defaults to Lists) that can be used to define Custom Pools. The constructor is a function that takes the Filter Definition and returns a Custom Pool with these functions: :add(e) - Add the Entity to the pool :remove(e) - Remove the Entity from the pool :has(e) boolean - Checks if the Entity exists in the pool :clear() - Clears the Pool from Entities Fixes #40 --- .gitignore | 5 +- concord/filter.lua | 168 +++++++++++++++++++++++++++++++++++++++++++++ concord/list.lua | 14 +++- concord/pool.lua | 115 ------------------------------- concord/system.lua | 66 ++++++------------ concord/type.lua | 16 +++++ 6 files changed, 221 insertions(+), 163 deletions(-) create mode 100644 concord/filter.lua delete mode 100644 concord/pool.lua diff --git a/.gitignore b/.gitignore index 51b8439..989d364 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ luac.out *.app *.i*86 *.x86_64 -*.hex \ No newline at end of file +*.hex + +# VSCode +.vscode/ \ No newline at end of file diff --git a/concord/filter.lua b/concord/filter.lua new file mode 100644 index 0000000..a959609 --- /dev/null +++ b/concord/filter.lua @@ -0,0 +1,168 @@ +--- Used to filter Entities with specific Components +-- A Filter has an associated Pool that can contain any amount of Entities. +-- @classmod Filter + +local PATH = (...):gsub('%.[^%.]+$', '') + +local List = require(PATH..".list") +local Type = require(PATH..".type") +local Components = require(PATH..".components") + + +local Filter = {} +Filter.__mt = { + __index = Filter, +} + +--- Validates a Filter Definition to make sure every component is valid. +-- @string name Name for the Filter. +-- @tparam table definition Table containing the Filter Definition +-- @tparam onComponent Optional function, called when a component is valid. +function Filter.validate (name, def, onComponent) + if type(def) ~= 'table' then + error("invalid component list for filter '"..name.."' (table expected, got "..type(def)..")", 3) + end + + if not onComponent and def.constructor and not Type.isCallable(def.constructor) then + error("invalid pool constructor (callable expected, got "..type(def.constructor)..")", 3) + end + + for n, component in ipairs(def) do + local ok, err, reject = Components.try(component, true) + + if not ok then + error("invalid component for filter '"..name.."' at position #"..n.." ("..err..")", 3) + end + + if onComponent then + onComponent(component, reject) + end + end +end + +--- Parses the Filter Defintion into two tables +-- required: An array of all the required component names. +-- rejected: An array of all the components that will be rejected. +-- @string name Name for the Filter. +-- @tparam table definition Table containing the Filter Definition +-- @treturn table required +-- @treturn table rejected +function Filter.parse (name, def) + local required, rejected = {}, {} + + Filter.validate(name, def, function (component, reject) + if reject then + table.insert(rejected, reject) + else + table.insert(required, component) + end + end) + + return required, rejected +end + +--- Creates a new Filter +-- @string name Name for the Filter. +-- @tparam table definition Table containing the Filter Definition +-- @treturn Filter The new Filter +-- @treturn Pool The associated Pool +function Filter.new (name, def) + local constructor = def.constructor or List + local pool = constructor(def) + + local required, rejected = Filter.parse(name, def) + + local filter = setmetatable({ + pool = pool, + + __required = required, + __rejected = rejected, + + __name = name, + + __isFilter = true, + }, Filter.__mt) + + return filter, pool +end + +--- Checks if an Entity fulfills the Filter requirements. +-- @tparam Entity e Entity to check +-- @treturn boolean +function Filter:eligible(e) + for i=#self.__required, 1, -1 do + local name = self.__required[i] + if not e[name] then return false end + end + + for i=#self.__rejected, 1, -1 do + local name = self.__rejected[i] + if e[name] then return false end + end + + return true +end + +function Filter:evaluate (e) + local has = self.pool:has(e) + local eligible = self:eligible(e) + + if not has and eligible then + self.pool:add(e) + elseif has and not eligible then + self.pool:remove(e) + end + + return self +end + + +-- Adds an Entity to the Pool, if it passes the Filter. +-- @param e Entity to add +-- @param bypass Whether to bypass the Filter or not. +-- @treturn Filter self +-- @treturn boolean Whether the entity was added or not. +function Filter:add (e, bypass) + if not bypass and not self:eligible(e) then + return self, false + end + + self.pool:add(e) + + return self, true +end + +-- Remove an Entity from the Pool associated to this Filter. +-- @param e Entity to remove +-- @treturn Filter self +function Filter:remove (e) + self.pool:remove(e) + return self +end + +-- Clear the Pool associated to this Filter. +-- @param e Entity to remove +-- @treturn Filter self +function Filter:clear (e) + self.pool:clear(e) + return self +end + +-- Check if the Pool bound to this System contains the passed Entity +-- @param e Entity to check +-- @treturn boolean Whether the Entity exists. +function Filter:has (e) + return self.pool:has(e) +end + +--- Gets the name of the Filter +-- @treturn string +function Filter:getName() + return self.__name +end + +return setmetatable(Filter, { + __call = function(_, ...) + return Filter.new(...) + end, +}) \ No newline at end of file diff --git a/concord/list.lua b/concord/list.lua index 88dfc04..7f10c74 100644 --- a/concord/list.lua +++ b/concord/list.lua @@ -16,7 +16,7 @@ end --- Adds an object to the List. -- Object must be of reference type --- Object may not be the string 'size' +-- Object may not be the string 'size', 'onAdded' or 'onRemoved' -- @param obj Object to add -- @treturn List self function List:add(obj) @@ -26,6 +26,7 @@ function List:add(obj) self[obj] = size self.size = size + if self.onAdded then self:onAdded(obj) end return self end @@ -51,6 +52,7 @@ function List:remove(obj) self[obj] = nil self.size = size - 1 + if self.onRemoved then self:onRemoved(obj) end return self end @@ -108,6 +110,16 @@ function List:sort(order) return self end +--- Callback for when an item is added to the List. +-- @param obj Object that was added +function List:onAdded (obj) --luacheck: ignore +end + +--- Callback for when an item is removed to the List. +-- @param obj Object that was removed +function List:onRemoved (obj) --luacheck: ignore +end + return setmetatable(List, { __call = function() return List.new() diff --git a/concord/pool.lua b/concord/pool.lua deleted file mode 100644 index f5cd401..0000000 --- a/concord/pool.lua +++ /dev/null @@ -1,115 +0,0 @@ ---- Used to iterate over Entities with a specific Components --- A Pool contain a any amount of Entities. --- @classmod Pool - -local PATH = (...):gsub('%.[^%.]+$', '') - -local List = require(PATH..".list") - -local Pool = {} -Pool.__mt = { - __index = Pool, -} - ---- Creates a new Pool --- @string name Name for the Pool. --- @tparam table filter Table containing the required BaseComponents --- @treturn Pool The new Pool -function Pool.new(name, filter) - local pool = setmetatable(List(), Pool.__mt) - - pool.__name = name - pool.__filter = filter - - pool.__isPool = true - - return pool -end - ---- Checks if an Entity is eligible for the Pool. --- @tparam Entity e Entity to check --- @treturn boolean -function Pool:eligible(e) - for i=#self.__filter.require, 1, -1 do - local name = self.__filter.require[i] - if not e[name] then return false end - end - - for i=#self.__filter.reject, 1, -1 do - local name = self.__filter.reject[i] - if e[name] then return false end - end - - return true -end - --- Adds an Entity to the Pool, if it can be eligible. --- @param e Entity to add --- @treturn Pool self --- @treturn boolean Whether the entity was added or not -function Pool:add(e, bypass) - if not bypass and not self:eligible(e) then - return self, false - end - - List.add(self, e) - self:onEntityAdded(e) - - return self, true -end - --- Remove an Entity from the Pool. --- @param e Entity to remove --- @treturn Pool self -function Pool:remove(e) - List.remove(self, e) - self:onEntityRemoved(e) - - return self -end - ---- Evaluate whether an Entity should be added or removed from the Pool. --- @param e Entity to add or remove --- @treturn Pool self -function Pool:evaluate(e) - local has = self:has(e) - local eligible = self:eligible(e) - - if not has and eligible then - self:add(e, true) --Bypass the check cause we already checked - elseif has and not eligible then - self:remove(e) - end - - return self -end - ---- Gets the name of the Pool --- @treturn string -function Pool:getName() - return self.__name -end - ---- Gets the filter of the Pool. --- Warning: Do not modify this filter. --- @return Filter of the Pool. -function Pool:getFilter() - return self.__filter -end - ---- Callback for when an Entity is added to the Pool. --- @tparam Entity e Entity that was added. -function Pool:onEntityAdded(e) -- luacheck: ignore -end - --- Callback for when an Entity is removed from the Pool. --- @tparam Entity e Entity that was removed. -function Pool:onEntityRemoved(e) -- luacheck: ignore -end - -return setmetatable(Pool, { - __index = List, - __call = function(_, ...) - return Pool.new(...) - end, -}) diff --git a/concord/system.lua b/concord/system.lua index 3db2a44..2d3a802 100644 --- a/concord/system.lua +++ b/concord/system.lua @@ -5,9 +5,8 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Pool = require(PATH..".pool") +local Filter = require(PATH..".filter") local Utils = require(PATH..".utils") -local Components = require(PATH..".components") local System = { ENABLE_OPTIMIZATION = true, @@ -19,7 +18,7 @@ System.mt = { local system = setmetatable({ __enabled = true, - __pools = {}, + __filters = {}, __world = world, __isSystem = true, @@ -33,11 +32,11 @@ System.mt = { Utils.shallowCopy(systemClass, system) end - for name, filter in pairs(systemClass.__filters) do - local pool = Pool(name, filter) + for name, def in pairs(systemClass.__definition) do + local filter, pool = Filter(name, Utils.shallowCopy(def, {})) system[name] = pool - system.__pools[#system.__pools + 1] = pool + table.insert(system.__filters, filter) end system:init(world) @@ -45,45 +44,20 @@ System.mt = { return system end, } - -local validateFilters = function (definition) - local filters = {} - - for name, componentsList in pairs(definition) do - if type(name) ~= 'string' then - error("invalid name for filter (string key expected, got "..type(name)..")", 3) - end - - if type(componentsList) ~= 'table' then - error("invalid component list for filter '"..name.."' (table expected, got "..type(componentsList)..")", 3) - end - - filters[name] = { require = {}, reject = {} } - - for n, component in ipairs(componentsList) do - local ok, componentClass, rejected = Components.try(component, true) - - if not ok then - error("invalid component for filter '"..name.."' at position #"..n.." ("..componentClass..")", 3) - elseif rejected then - table.insert(filters[name].reject, rejected) - else - table.insert(filters[name].require, component) - end - end - end - - return filters -end - --- Creates a new SystemClass. -- @param table filters A table containing filters (name = {components...}) -- @treturn System A new SystemClass function System.new(definition) - local filters = validateFilters(definition) + for name, def in pairs(definition) do + if type(name) ~= 'string' then + error("invalid name for filter (string key expected, got "..type(name)..")", 2) + end + + Filter.validate(name, def) + end local systemClass = setmetatable({ - __filters = filters, + __definition = definition, __isSystemClass = true, }, System.mt) @@ -103,8 +77,8 @@ end -- @param e The Entity to check -- @treturn System self function System:__evaluate(e) - for _, pool in ipairs(self.__pools) do - pool:evaluate(e) + for _, filter in ipairs(self.__filters) do + filter:evaluate(e) end return self @@ -114,9 +88,9 @@ end -- @param e The Entity to remove -- @treturn System self function System:__remove(e) - for _, pool in ipairs(self.__pools) do - if pool:has(e) then - pool:remove(e) + for _, filter in ipairs(self.__filters) do + if filter:has(e) then + filter:remove(e) end end @@ -126,8 +100,8 @@ end -- Internal: Clears all Entities from the System. -- @treturn System self function System:__clear() - for i = 1, #self.__pools do - self.__pools[i]:clear() + for _, filter in ipairs(self.__filters) do + filter:clear() end return self diff --git a/concord/type.lua b/concord/type.lua index 157a8d4..bf7f52a 100644 --- a/concord/type.lua +++ b/concord/type.lua @@ -3,6 +3,15 @@ local Type = {} +function Type.isCallable(t) + if type(t) == "function" then return true end + + local meta = getmetatable(t) + if meta and type(meta.__call) == "function" then return true end + + return false +end + --- Returns if object is an Entity. -- @param t Object to check -- @treturn boolean @@ -45,4 +54,11 @@ function Type.isWorld(t) return type(t) == "table" and t.__isWorld or false end +--- Returns if object is a Filter. +-- @param t Object to check +-- @treturn boolean +function Type.isFilter(t) + return type(t) == "table" and t.__isFilter or false +end + return Type