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
This commit is contained in:
Pablo Ariel Mayobre 2023-02-14 18:14:23 -03:00
parent 07bd5d0f28
commit 743d662ef9
6 changed files with 221 additions and 163 deletions

3
.gitignore vendored
View file

@ -38,3 +38,6 @@ luac.out
*.i*86 *.i*86
*.x86_64 *.x86_64
*.hex *.hex
# VSCode
.vscode/

168
concord/filter.lua Normal file
View file

@ -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,
})

View file

@ -16,7 +16,7 @@ end
--- Adds an object to the List. --- Adds an object to the List.
-- Object must be of reference type -- 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 -- @param obj Object to add
-- @treturn List self -- @treturn List self
function List:add(obj) function List:add(obj)
@ -26,6 +26,7 @@ function List:add(obj)
self[obj] = size self[obj] = size
self.size = size self.size = size
if self.onAdded then self:onAdded(obj) end
return self return self
end end
@ -51,6 +52,7 @@ function List:remove(obj)
self[obj] = nil self[obj] = nil
self.size = size - 1 self.size = size - 1
if self.onRemoved then self:onRemoved(obj) end
return self return self
end end
@ -108,6 +110,16 @@ function List:sort(order)
return self return self
end 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, { return setmetatable(List, {
__call = function() __call = function()
return List.new() return List.new()

View file

@ -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,
})

View file

@ -5,9 +5,8 @@
local PATH = (...):gsub('%.[^%.]+$', '') local PATH = (...):gsub('%.[^%.]+$', '')
local Pool = require(PATH..".pool") local Filter = require(PATH..".filter")
local Utils = require(PATH..".utils") local Utils = require(PATH..".utils")
local Components = require(PATH..".components")
local System = { local System = {
ENABLE_OPTIMIZATION = true, ENABLE_OPTIMIZATION = true,
@ -19,7 +18,7 @@ System.mt = {
local system = setmetatable({ local system = setmetatable({
__enabled = true, __enabled = true,
__pools = {}, __filters = {},
__world = world, __world = world,
__isSystem = true, __isSystem = true,
@ -33,11 +32,11 @@ System.mt = {
Utils.shallowCopy(systemClass, system) Utils.shallowCopy(systemClass, system)
end end
for name, filter in pairs(systemClass.__filters) do for name, def in pairs(systemClass.__definition) do
local pool = Pool(name, filter) local filter, pool = Filter(name, Utils.shallowCopy(def, {}))
system[name] = pool system[name] = pool
system.__pools[#system.__pools + 1] = pool table.insert(system.__filters, filter)
end end
system:init(world) system:init(world)
@ -45,45 +44,20 @@ System.mt = {
return system return system
end, 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. --- Creates a new SystemClass.
-- @param table filters A table containing filters (name = {components...}) -- @param table filters A table containing filters (name = {components...})
-- @treturn System A new SystemClass -- @treturn System A new SystemClass
function System.new(definition) 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({ local systemClass = setmetatable({
__filters = filters, __definition = definition,
__isSystemClass = true, __isSystemClass = true,
}, System.mt) }, System.mt)
@ -103,8 +77,8 @@ end
-- @param e The Entity to check -- @param e The Entity to check
-- @treturn System self -- @treturn System self
function System:__evaluate(e) function System:__evaluate(e)
for _, pool in ipairs(self.__pools) do for _, filter in ipairs(self.__filters) do
pool:evaluate(e) filter:evaluate(e)
end end
return self return self
@ -114,9 +88,9 @@ end
-- @param e The Entity to remove -- @param e The Entity to remove
-- @treturn System self -- @treturn System self
function System:__remove(e) function System:__remove(e)
for _, pool in ipairs(self.__pools) do for _, filter in ipairs(self.__filters) do
if pool:has(e) then if filter:has(e) then
pool:remove(e) filter:remove(e)
end end
end end
@ -126,8 +100,8 @@ end
-- Internal: Clears all Entities from the System. -- Internal: Clears all Entities from the System.
-- @treturn System self -- @treturn System self
function System:__clear() function System:__clear()
for i = 1, #self.__pools do for _, filter in ipairs(self.__filters) do
self.__pools[i]:clear() filter:clear()
end end
return self return self

View file

@ -3,6 +3,15 @@
local Type = {} 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. --- Returns if object is an Entity.
-- @param t Object to check -- @param t Object to check
-- @treturn boolean -- @treturn boolean
@ -45,4 +54,11 @@ function Type.isWorld(t)
return type(t) == "table" and t.__isWorld or false return type(t) == "table" and t.__isWorld or false
end 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 return Type