diff options
-rw-r--r-- | honey/ecs.lua | 108 | ||||
-rw-r--r-- | honey/ecs.test.lua | 164 |
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) |