From bb019b6aad81f3dc80900f8bd6bad9ce046cf11a Mon Sep 17 00:00:00 2001 From: sanine-a Date: Tue, 28 Mar 2023 11:50:23 -0500 Subject: begin rdb-style refactor --- honey/ecs.lua | 236 +++++++---------------------------------------------- honey/ecs.test.lua | 73 ++++++++++------- 2 files changed, 71 insertions(+), 238 deletions(-) diff --git a/honey/ecs.lua b/honey/ecs.lua index 3632f34..b4def84 100644 --- a/honey/ecs.lua +++ b/honey/ecs.lua @@ -3,226 +3,46 @@ setmetatable(module, {__index=_G}) setfenv(1, module) ---===== filters =====-- +--===== Components =====-- +Component = {} -Filter = {} - --- base filter creation -function Filter.new(_, operation, tbl) - local self = {} - self.keys = {} - self.filters = {} - self.op = operation - - for _, v in ipairs(tbl) do - if type(v) == "function" then - table.insert(self.filters, v) - elseif type(v) == "string" then - table.insert(self.keys, v) +function Component.newFactory(name, params, serialize) + local metatable = {} + metatable.__tostring = serialize or Component.serialize + metatable.__newindex = function(tbl, index, value) + if params[index] ~= nil then + tbl[index] = value + else + error(string.format("%q is not a valid key for a component of type %q", index, name)) end end - - return function(entity, _self) - local entity = _self or entity -- able to call as . or : - return Filter.check(self, entity) - end -end -setmetatable(Filter, {__call=Filter.new}) - - --- base filter checking -function Filter.check(self, entity) - local funcname = "check" .. self.op - return Filter[funcname](self, entity) -end - - --- AND filter (all keys and subfilters must match) -function Filter.AND(tbl) - return Filter("AND", tbl) -end -function Filter.checkAND(self, entity) - for _, subfilter in ipairs(self.filters) do - if not subfilter(entity) then return false end - end - for _, key in ipairs(self.keys) do - if entity[key] == nil then return false end - end - return true -end - - --- OR filter (at least one key or subfilter must match) -function Filter.OR(tbl) - return Filter("OR", tbl) -end -function Filter.checkOR(self, entity) - for _, subfilter in ipairs(self.filters) do - if subfilter(entity) then return true end - end - for _, key in ipairs(self.keys) do - if entity[key] ~= nil then return true end - end - return false -end - - --- NAND filter (at least one key or subfilter must NOT match) -function Filter.NAND(tbl) - return Filter("NAND", tbl) -end -function Filter.checkNAND(self, entity) - for _, subfilter in ipairs(self.filters) do - if not subfilter(entity) then return true end - end - for _, key in ipairs(self.keys) do - if entity[key] == nil then return true end - end - return false -end - - --- NOR filter (no keys or subfilters may match) -function Filter.NOR(tbl) - return Filter("NOR", tbl) -end -function Filter.checkNOR(self, entity) - for _, subfilter in ipairs(self.filters) do - if subfilter(entity) then return false end - end - for _, key in ipairs(self.keys) do - if entity[key] ~= nil then return false end - end - return true -end - - ---===== levels =====-- - -Level = {} -Level.__index = Level - -function Level.new(_) - local self = {} - self.systems = {} - self.entities = {} - self.nextId = 1 - setmetatable(self, Level) - return self -end -setmetatable(Level, {__call=Level.new}) - - -local function systemLt(a, b) - return (a.priority or 50) < (b.priority or 50) -end -function Level.addSystem(self, system) - assert(system.update, "systems must have an 'update' key") - assert(system.filter, "systems must have a 'filter' key") - system.entities = {} - table.insert(self.systems, system) - table.sort(self.systems, systemLt) - if system.setup then - system.setup(system) - end -end - - -local function addEntityToSystem(system, id, entity) - -- check if entity is already present - if system.entities[id] then return end - - if system.onAddEntity then - system:onAddEntity(id, entity) - end - system.entities[id] = true -end - - -local function removeEntityFromSystem(system, id, entity) - -- check if entity is already absent - if not system.entities[id] then return end - - if system.onRemoveEntity then - system:onRemoveEntity(id, entity) - end - system.entities[id] = nil -end - - -function Level.addEntity(self, entity) - local id = self.nextId - self.entities[id] = entity - self.nextId = id + 1 - - for _, system in ipairs(self.systems) do - if system.filter(entity) then - addEntityToSystem(system, id, entity) + return function(tbl) + local tbl = tbl or {} + local self = { __type=name } + for k, v in pairs(params) do + self[k] = v end - end - - return id -end - - -function Level.reconfigureEntity(self, id) - local entity = self.entities[id] - - for _, system in ipairs(self.systems) do - if system.filter(entity) then - addEntityToSystem(system, id, entity) - else - removeEntityFromSystem(system, id, entity) + setmetatable(self, metatable) + for k, v in pairs(tbl) do + self[k] = v end + return self end end -function Level.removeEntity(self, id) - local entity = self.entities[id] - if not entity then error("bad id: "..tostring(id)) end - for _, system in ipairs(self.systems) do - removeEntityFromSystem(system, id, entity) - end - self.entities[id] = nil -end - - -function Level.reconfigureAllEntities(self) - for id in ipairs(self.entities) do - self:reconfigureEntity(id) - end -end - - -function Level.update(self, dt, paused) - local paused = paused or false - for _, system in ipairs(self.systems) do - if (not paused) or (paused and system.nopause) then - if system.preUpdate then - system:preUpdate() - end - if system.prepareEntity then - for id in pairs(system.entities) do - local entity = self.entities[id] - if entity then - system:prepareEntity(entity) - end - end - end - for id in pairs(system.entities) do - local entity = self.entities[id] - if entity then - system:update(entity, dt) - end - end - if system.postUpdate then - system:postUpdate() - end +function Component.serialize(self) + local str = "{" + for k, v in pairs(self) do + if type(v) == "string" then + str = str .. string.format("%s=%q,", k, v) + else + str = str .. string.format("%s=%s,", k, tostring(v)) end end + str = string.sub(str, 1, -2) .. "}" + return str end - return module diff --git a/honey/ecs.test.lua b/honey/ecs.test.lua index 7814097..9f7519b 100644 --- a/honey/ecs.test.lua +++ b/honey/ecs.test.lua @@ -10,42 +10,55 @@ end local ecs = require 'ecs' -local Filter = ecs.Filter -test("Filter.AND correctly matches basic keys", function() - local filter = Filter.AND{"hello", "world"} +local Component = ecs.Component - assert(filter{hello=true} == false) - assert(filter{world=true} == false) - assert(filter{hello=true, world=true} == true) - assert(filter{asm=true, hello=true, world=true} == true) + +test("factories work as expected", function() + local factory = Component.newFactory("health", { percent=100 }) + local comp1 = factory() + assert(comp1.__type == "health", "bad component type for comp1") + assert(comp1.percent == 100, "bat percent for comp1") + + local comp2 = factory{ percent=50 } + assert(comp2.__type == "health", "bad component type for comp2") + assert(comp2.percent == 50, "bad percent for comp2") + + local success = pcall(function() + comp2.dne = 5 + end) + assert(not success, "incorrectly succeeded in setting comp2.dne") + + local success = pcall(function() + local comp3 = factory{ percent = 44, something = 2 } + end) + assert(not success, "incorrectly succeeded in creating comp3") end) -test("Filter.AND correctly matches subfilters", function() - local subfilter = Filter.AND{"hello"} - local filter = Filter.AND{subfilter, "world"} - - assert(filter{hello=true} == false) - assert(filter{world=true} == false) - assert(filter{hello=true, world=true} == true) - assert(filter{asm=true, hello=true, world=true} == true) + + +test("components serialize as expected", function() + local position = Component.newFactory("position", { x=0, y=0, z=0 }) + local comp = position{x=10, y=15, z=10} + local str = tostring(comp) + local tbl = (loadstring("return " .. str))() + assert(tbl.__type == "position", "bad type") + assert(tbl.x == 10, "bad x") + assert(tbl.y == 15, "bad y") + assert(tbl.z == 10, "bad z") end) -test("Filter.OR correctly matches basic keys", function() - local filter = Filter.OR{"hello", "world"} +test("components serialize successfully with subcomponents", function() + local position = Component.newFactory("position", { x=0, y=0, z=0 }) + local player = Component.newFactory("player", { name="", position={} }) - assert(filter{hello=true} == true) - assert(filter{world=true} == true) - assert(filter{hello=true, world=true} == true) - assert(filter{asm=true} == false) -end) -test("Filter.OR correctly matches subfilters", function() - local subfilter = Filter.OR{"hello"} - local filter = Filter.OR{subfilter, "world"} - - assert(filter{hello=true} == true) - assert(filter{world=true} == true) - assert(filter{hello=true, world=true} == true) - assert(filter{asm=true} == false) + local p = player{ name="hannah", position=position{x=10, y=9, z=8} } + local tbl = (loadstring("return " .. tostring(p)))() + assert(tbl.__type == "player") + assert(tbl.name == "hannah") + assert(tbl.position.__type == "position") + assert(tbl.position.x == 10) + assert(tbl.position.y == 9) + assert(tbl.position.z == 8) end) -- cgit v1.2.1