summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsanine-a <sanine.not@pm.me>2023-03-28 13:15:27 -0500
committersanine-a <sanine.not@pm.me>2023-03-28 13:15:27 -0500
commit4f450ca94522f5c26a631fc4d19bd6eec420cbec (patch)
tree07ac65ba58f247aea1a43e372adb22c9ec6c9343
parentbb019b6aad81f3dc80900f8bd6bad9ce046cf11a (diff)
implement EntityDb
-rw-r--r--honey/ecs.lua108
-rw-r--r--honey/ecs.test.lua164
2 files changed, 269 insertions, 3 deletions
diff --git a/honey/ecs.lua b/honey/ecs.lua
index b4def84..62acce0 100644
--- a/honey/ecs.lua
+++ b/honey/ecs.lua
@@ -5,28 +5,41 @@ setfenv(1, module)
--===== Components =====--
+-- Components provide a kind-of-type-safe way of storing values, since they are
+-- guaranteed to have a specific set of keys and no other values in them. They also serialize
+-- nicely for storage.
+
Component = {}
function Component.newFactory(name, params, serialize)
+ -- create the metatable for this component type
local metatable = {}
- metatable.__tostring = serialize or Component.serialize
+ metatable.__tostring = serialize or Component.serialize -- nice serialization
metatable.__newindex = function(tbl, index, value)
if params[index] ~= nil then
tbl[index] = value
else
+ -- throw an error if we try to set a key that is not in the valid set
error(string.format("%q is not a valid key for a component of type %q", index, name))
end
end
+
+ -- the factory function for creating components
return function(tbl)
local tbl = tbl or {}
local self = { __type=name }
+
+ -- ensure that all keys are present
for k, v in pairs(params) do
self[k] = v
end
setmetatable(self, metatable)
+
+ -- set the keys from the factory call
for k, v in pairs(tbl) do
self[k] = v
end
+
return self
end
end
@@ -45,4 +58,97 @@ function Component.serialize(self)
return str
end
+
+--===== EntityDb =====--
+
+EntityDb = {}
+EntityDb.__index = EntityDb
+
+
+function EntityDb.new(_)
+ local self = {
+ entities = {},
+ components = {},
+ }
+ setmetatable(self, EntityDb)
+ return self
+end
+setmetatable(EntityDb, {__call=EntityDb.new})
+
+
+function EntityDb.checkIsValid(self, id)
+ if not self.entities[id] then
+ error(string.format("invalid entity id: 0x%x", id))
+ end
+end
+
+
+local guid = (function()
+ local id = 0
+ return function() id=id+1 return id end
+end)()
+function EntityDb.createEntity(self)
+ local id = guid()
+ self.entities[id] = true
+ return id
+end
+
+
+function EntityDb.addComponent(self, id, name, value)
+ self:checkIsValid(id)
+ if not self.components[name] then
+ self.components[name] = { count=0 }
+ end
+
+ local component = self.components[name]
+ component[id] = value
+ component.count = component.count + 1
+end
+
+
+local function shallowCopy(tbl)
+ local copy = {}
+ for k, v in pairs(tbl) do copy[k] = v end
+ return copy
+end
+
+
+function EntityDb.queryComponent(self, name)
+ local query = shallowCopy(self.components[name])
+ query.count = nil
+ return query
+end
+
+
+function EntityDb.queryEntity(self, id)
+ self:checkIsValid(id)
+ local query = {}
+ for name, component in pairs(self.components) do
+ query[name] = component[id]
+ end
+ return query
+end
+
+
+function EntityDb.removeComponent(self, id, name)
+ self:checkIsValid(id)
+ local component = self.components[name]
+ if component[id] ~= nil then
+ component[id] = nil
+ component.count = component.count - 1
+ if component.count == 0 then
+ self.components[name] = nil
+ end
+ end
+end
+
+
+function EntityDb.deleteEntity(self, id)
+ self:checkIsValid(id)
+ for name in pairs(self.components) do
+ self:removeComponent(id, name)
+ end
+ self.entities[id] = nil
+end
+
return module
diff --git a/honey/ecs.test.lua b/honey/ecs.test.lua
index 9f7519b..fbdba30 100644
--- a/honey/ecs.test.lua
+++ b/honey/ecs.test.lua
@@ -1,5 +1,5 @@
local function test(msg, f)
- local success, error = pcall(f)
+ local success, error = xpcall(f, debug.traceback)
if success then
print(msg .. "\t\t[OK]")
else
@@ -12,8 +12,9 @@ end
local ecs = require 'ecs'
-local Component = ecs.Component
+--===== Component tests =====--
+local Component = ecs.Component
test("factories work as expected", function()
local factory = Component.newFactory("health", { percent=100 })
@@ -62,3 +63,162 @@ test("components serialize successfully with subcomponents", function()
assert(tbl.position.y == 9)
assert(tbl.position.z == 8)
end)
+
+
+--===== EntityDb tests =====--
+
+local EntityDb = ecs.EntityDb
+
+
+test("EntityDb.createEntity() always returns a new id", function()
+ local db = EntityDb()
+
+ local ids = {}
+ for i=1,100 do
+ local id = db:createEntity()
+ assert(ids[id] == nil, "id was already returned!")
+ ids[id] = true
+ end
+end)
+
+
+test("EntityDb.queryComponent() gets all entities with a given component", function()
+ local db = EntityDb()
+
+ local ids = {}
+ for i=1,100 do
+ local id = db:createEntity()
+ if i%2==0 then
+ ids[id] = 5*i
+ db:addComponent(id, "number", 5*i)
+ end
+ end
+
+ local query = db:queryComponent("number")
+ local count = 0
+ for id, number in pairs(query) do
+ count = count + 1
+ assert(number == ids[id])
+ end
+ assert(count == 50)
+end)
+
+
+test("EntityDb.queryEntity() gets all components associated with an entity", function()
+ local db = EntityDb()
+
+ local entity
+ for i=1,100 do
+ local id = db:createEntity()
+ if i%2 == 0 then db:addComponent(id, "number", 2) end
+ if i%3 == 0 then db:addComponent(id, "string", "hello") end
+ if i%5 == 0 then db:addComponent(id, "number2", 4) end
+ if i%7 == 0 then db:addComponent(id, "string2", "world") end
+ if i == 30 then entity=id end
+ end
+
+ local query = db:queryEntity(entity)
+ assert(query.number == 2)
+ assert(query.string == "hello")
+ assert(query.number2 == 4)
+ assert(query.string2 == nil)
+end)
+
+
+test("EntityDb.removeComponent() removes components correctly", function()
+ local db = EntityDb()
+
+ local id = db:createEntity()
+ db:addComponent(id, "number", 2)
+ db:addComponent(id, "string", "hello")
+ db:addComponent(id, "number2", 4)
+ db:addComponent(id, "string2", "world")
+
+ local query = db:queryEntity(id)
+ assert(query.number == 2)
+ assert(query.string == "hello")
+ assert(query.number2 == 4)
+ assert(query.string2 == "world")
+
+ db:removeComponent(id, "string2")
+ query = db:queryEntity(id)
+ assert(query.number == 2)
+ assert(query.string == "hello")
+ assert(query.number2 == 4)
+ assert(query.string2 == nil)
+
+ db:removeComponent(id, "number2")
+ query = db:queryEntity(id)
+ assert(query.number == 2)
+ assert(query.string == "hello")
+ assert(query.number2 == nil)
+ assert(query.string2 == nil)
+
+ db:removeComponent(id, "string")
+ query = db:queryEntity(id)
+ assert(query.number == 2)
+ assert(query.string == nil)
+ assert(query.number2 == nil)
+ assert(query.string2 == nil)
+
+ db:removeComponent(id, "number")
+ query = db:queryEntity(id)
+ assert(query.number == nil)
+ assert(query.string == nil)
+ assert(query.number2 == nil)
+ assert(query.string2 == nil)
+end)
+
+
+test("EntityDb.removeComponent() deletes component table when empty", function()
+ local db = EntityDb()
+ local id1 = db:createEntity()
+ local id2 = db:createEntity()
+ db:addComponent(id1, "number", 2)
+ db:addComponent(id2, "number", 3)
+
+ assert(db.components.number ~= nil)
+ db:removeComponent(id1, "number")
+ assert(db.components.number ~= nil)
+ db:removeComponent(id2, "number")
+ assert(db.components.number == nil)
+end)
+
+
+test("EntityDb.removeComponent() does nothing if the component is not present", function()
+ local db = EntityDb()
+ local id1 = db:createEntity()
+ local id2 = db:createEntity()
+ db:addComponent(id1, "number", 2)
+ db:addComponent(id2, "number", 3)
+
+ assert(db.components.number ~= nil)
+ db:removeComponent(id1, "number")
+ assert(db.components.number ~= nil)
+ db:removeComponent(id1, "number")
+ assert(db.components.number ~= nil)
+
+ db:removeComponent(id2, "number")
+ assert(db.components.number == nil)
+
+end)
+
+
+test("EntityDb.deleteEntity() correctly removes an entity", function()
+ local db = EntityDb()
+ local id1 = db:createEntity()
+ local id2 = db:createEntity()
+ db:addComponent(id1, "number", 2)
+ db:addComponent(id2, "number", 3)
+
+ local query = db:queryComponent("number")
+ assert(query[id1] and query[id2])
+ db:deleteEntity(id1)
+ query = db:queryComponent("number")
+ assert(query[id1] == nil)
+ assert(query[id2] ~= nil)
+
+ assert(false == pcall(function()
+ local query = db:queryEntity(id1)
+ end))
+end)