summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--honey/ecs.lua236
-rw-r--r--honey/ecs.test.lua73
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)