Concord
Concord copied to clipboard
Custom Pools
This is a very complex feature but one that can add super powers to Concord.
Current approach
Concord currently has Pools, which inherit from Lists to store Entities based on Filters. This Lists are unordered (see #33) and end up being redundant when used in conjunction with other form of Entity storage, like QuadTrees, Spatial Hashes, or parent-child relationships.
Generally when you need to store your Entities in one of this storage you would use the Pool:onEntityAdded
and Pool:onEntityRemoved
methods to hook into this events and keep your storage up to date with the List.
Problems
The biggest disadvantage with this, is that it's hard to maintain since these methods can only be accessed in the System instance (the first event where these are available is System:init
). It needs to be hooked up on a per-Pool basis, and you need to make sure no two Pools are writing to the same storage, you also have some data duplication (the List and Storage both hold the same group of Entities)...
Proposal
So a proposal that can fix this issue is that the user could provide their custom Pool constructor in the Pool Definition, and this constructor would replace the List constructor that the Pool uses by default.
local SpatialSystem = Concord.system({
pool = { "position", constructor = Spatialhash } --Pool Definition
})
The constructor (Spatialhash
in the example above) would be a function or callable that takes the Pool Definition as argument (which it may ignore entirely or modify if needed) and it would return a Storage instance.
local Spatialhash = function (definition)
-- Construct the Storage
-- Modify definition if needed
-- ...
return Storage
end
Storage is any table (or similar) with the following methods:
local Storage = {}
function Storage:add (Entity)
-- Add Entity to this storage
-- Return value is ignored
end
function Storage:remove(Entity)
-- Remove Entity from this storage
end
function Storage:has(Entity)
-- Checks if the Entity exists in this storage
return exists -- true if the Entity exists, false otherwise
end
function Storage:clear()
-- Clears the entire Storage
end
This is all that Pools need internally, but the end user may add any other methods they deem necessary like a way to query for a group of Entities, or the ability to sort the Storage on request, etc.
A great benefit from this approach is that users can write and share their custom Storage constructors, and people would be able to just plug them into their System definitions without writing any custom logic around their Pools.
What would modifying the Pool Definition in the constructor look like, and what benefits might that have?
I love this change over all! I think you figured out a very neat way of handling this.
What would modifying the Pool Definition in the constructor look like, and what benefits might that have?
Regarding this, you receive the complete table for the pool definition, so in the case above:
definition = {
"position",
constructor = Spacialhash
}
You could use this to pass arguments to the constructor (always using the hash part of the table, since the array is used by the filter).
Or your constructor could modify the table (since Concord hasn't created the Filter by then) and add more entries to the array part so that for example the filter ends up being { "position", "boundingBox" }
for example.
You could also remove a component from it, or negate a component once #32 lands.
This is super powerful functionality. I can actually convert some of my code to use this, as I have a component/system couple that I use to attach other components. This would allow me to do it on the pool itself. Interesting!
I would suggest that if you intend to share a Custom Pool (Storage) then you should try to avoid much coupling with specific Components.
But you could ship both the Storage and the Components together or expose options through the Pool Definition.
So yeah this may be a big deal haha
@pablomayobre can you put here as well the sample code you've send it discord for reference and example if ever other people wants to try it as well
Example LayerList.lua
This Custom Pool inherits from the built-in Lists, but extends the :add
method to filter based on a value in the layer
component the Entity must have.
local List = require("concord.list") -- Built-in List
-- Inherit from List
local LayerList = setmetatable({}, { __index = List })
local meta = { __index = LayerList }
-- Extend the :add method to filter based on layer
function LayerList:add(e)
-- Here we check that the id in the layer component
-- matches the layer property of this LayerList
if e.layer.id == self.layer then
List.add(self, e) -- if so add it
return true
end
return false
end
--Export the LayerList Constructor
return function(def)
-- Create a new LayerList
local self = setmetatable(List(), meta)
-- Save the layer property in the LayerList
self.layer = def.layer
-- Add the layer component as a requirement
table.insert(def, "layer")
-- And don't forget to return the new Pool
return self
end
Usage
local LayerList = require("LayerList")
local System = require("concord.system")
local RenderUI = System {
uiLayer = {
"render", "position"
constructor = LayerList,
layer = "ui",
}
}