Concord icon indicating copy to clipboard operation
Concord copied to clipboard

Custom Pools

Open pablomayobre opened this issue 4 years ago • 8 comments

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.

pablomayobre avatar Jan 08 '21 06:01 pablomayobre

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.

pablomayobre avatar Jan 08 '21 07:01 pablomayobre

What would modifying the Pool Definition in the constructor look like, and what benefits might that have?

speakk avatar Jan 08 '21 12:01 speakk

I love this change over all! I think you figured out a very neat way of handling this.

speakk avatar Jan 08 '21 13:01 speakk

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.

pablomayobre avatar Jan 08 '21 19:01 pablomayobre

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!

speakk avatar Jan 08 '21 19:01 speakk

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 avatar Jan 08 '21 19:01 pablomayobre

@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

flamendless avatar Jan 10 '21 13:01 flamendless

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",
    }
}

pablomayobre avatar Jan 15 '21 05:01 pablomayobre