summaryrefslogtreecommitdiff
path: root/honey/ecs/ecs.lua
diff options
context:
space:
mode:
authorsanine-a <sanine.not@pm.me>2023-05-09 11:31:17 -0500
committersanine-a <sanine.not@pm.me>2023-05-09 11:31:17 -0500
commita2ae7aae8357c8c2684d85fd58b0c5a0563ebab9 (patch)
tree5050161547408c80b521c9f4708bb9f7a9d0fa71 /honey/ecs/ecs.lua
parent2a14abecaee073aef1f1966bb397d6086b2e4785 (diff)
refactor: split ecs systems into multiple files
Diffstat (limited to 'honey/ecs/ecs.lua')
-rw-r--r--honey/ecs/ecs.lua274
1 files changed, 274 insertions, 0 deletions
diff --git a/honey/ecs/ecs.lua b/honey/ecs/ecs.lua
new file mode 100644
index 0000000..b0409e4
--- /dev/null
+++ b/honey/ecs/ecs.lua
@@ -0,0 +1,274 @@
+math.randomseed(os.time())
+
+local glm = require 'honey.glm'
+
+local module = {}
+setmetatable(module, {__index=_G})
+setfenv(1, module)
+
+
+--===== EntityDb =====--
+
+
+-- EntityDb is a database of entities and their associated components
+-- it should be quite efficient to query for all entities with a given component, and reasonably
+-- efficient to query for all components of a given entity
+
+
+EntityDb = {}
+EntityDb.__index = EntityDb
+
+
+function EntityDb.new(_)
+ local self = {
+ entities = {},
+ components = {},
+ }
+ setmetatable(self, EntityDb)
+ return self
+end
+setmetatable(EntityDb, {__call=EntityDb.new})
+
+
+local function serialize(tbl)
+ local tostr = function(x, value)
+ if type(x) == "table" then
+ if x.__tostring then
+ return tostring(x)
+ else
+ return serialize(x)
+ end
+ elseif type(x) == "string" then
+ if value then
+ return string.format("\"%s\"", x)
+ else
+ return x
+ end
+ else
+ return tostring(x)
+ end
+ end
+ local str = "{"
+ for key, value in pairs(tbl) do
+ if type(key) == "string" and string.match(key, "^_") then
+ -- ignore keys starting with an underscore
+ else
+ str = str .. string.format("%s=%s,", tostr(key), tostr(value, true))
+ end
+ end
+ str = string.sub(str, 1, -2) .. "}"
+ return str
+end
+
+
+-- save current database to file
+function EntityDb.save(self, filename)
+ local file, err = io.open(filename, "w")
+ if not file then error(err) end
+
+ for entity in pairs(self.entities) do
+ local components = self:queryEntity(entity)
+ file:write(string.format("Entity(\"%s\", %s)\n", entity, serialize(components)))
+ end
+
+ file:close()
+end
+
+
+-- load database from file
+function EntityDb.load(self, filename)
+ print(collectgarbage("count"))
+ self.entities = {}
+ self.components = {}
+ collectgarbage()
+ print(collectgarbage("count"))
+ local env = {
+ Entity = function(id, components)
+ self:createEntity(id)
+ self:addComponents(id, components)
+ end,
+ Vec3 = glm.Vec3,
+ Mat4 = glm.Mat4,
+ }
+ local f, err = loadfile(filename)
+ if not f then error(err) end
+ setfenv(f, env)
+ f()
+end
+
+
+-- check if a given entity id is legitimate
+function EntityDb.checkIsValid(self, id)
+ if not self.entities[id] then
+ error(string.format("invalid entity id: %s", tostring(id)))
+ end
+end
+
+
+local random = math.random
+local function uuid()
+ local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+ return string.gsub(template, '[xy]', function (c)
+ local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
+ return string.format('%x', v)
+ end)
+end
+
+
+-- create a new entity
+function EntityDb.createEntity(self, id)
+ local id = id or uuid() -- allow inserting entities at preset ids for loading
+ self.entities[id] = true
+ return id
+end
+
+
+-- add a component to an entity
+function EntityDb.addComponent(self, id, name, value)
+ self:checkIsValid(id)
+
+ -- create the relevant component table if it doesn't exist
+ if not self.components[name] then
+ self.components[name] = { count=0, data={} }
+ end
+
+ local component = self.components[name]
+ component.data[id] = value
+ component.count = component.count + 1
+end
+
+
+-- add multiple components at once, for convenience
+function EntityDb.addComponents(self, id, components)
+ for name, value in pairs(components) do
+ self:addComponent(id, name, value)
+ end
+end
+
+
+-- create an entity with components
+function EntityDb.createEntityWithComponents(self, components)
+ local id = self:createEntity()
+ self:addComponents(id, components)
+ return id
+end
+
+
+-- get all entities with a given component
+function EntityDb.queryComponent(self, name)
+ local component = self.components[name]
+ if component then
+ return component.data
+ else
+ return {}
+ end
+end
+
+
+-- get all components associated with an entity
+function EntityDb.queryEntity(self, id)
+ self:checkIsValid(id)
+ local query = {}
+ for name, component in pairs(self.components) do
+ query[name] = component.data[id]
+ end
+ return query
+end
+
+
+-- get a specific component from an entity
+function EntityDb.getComponent(self, id, name)
+ self:checkIsValid(id)
+ return self.components[name].data[id]
+end
+
+
+-- remove a component from an entity
+function EntityDb.removeComponent(self, id, name)
+ self:checkIsValid(id)
+ local component = self.components[name]
+ if component.data[id] ~= nil then
+ component.data[id] = nil
+ component.count = component.count - 1
+ if component.count == 0 then
+ self.components[name] = nil
+ end
+ end
+end
+
+
+-- remove an entity from the db
+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
+
+
+--===== SystemDb =====--
+
+SystemDb = {}
+SystemDb.__index = SystemDb
+
+
+function SystemDb.new(_, entityDb)
+ local self = {
+ systems = {},
+ sorted = {},
+ entityDb = entityDb,
+ }
+ setmetatable(self, SystemDb)
+ return self
+end
+setmetatable(SystemDb, {__call=SystemDb.new})
+
+
+local function systemId()
+ local template = "xx:xx:xx"
+ return string.gsub(template, "x", function(c)
+ return string.format("%x", random(0, 0xf))
+ end)
+end
+
+
+function SystemDb.addSystem(self, systemFunc, params)
+ local system
+ if type(systemFunc) == "table" then
+ system = systemFunc
+ else
+ local params = params or {}
+ params.db = self.entityDb
+ system = systemFunc(params)
+ end
+
+ local id = systemId()
+ self.systems[id] = system
+ table.insert(self.sorted, id)
+ self:sort()
+ return id
+end
+
+
+function SystemDb.sort(self)
+ table.sort(self.sorted, function(a, b) return (self.systems[a].priority or 100) < (self.systems[b].priority or 100) end)
+end
+
+
+function SystemDb.update(self, dt)
+ for _, system in ipairs(self.sorted) do
+ self.systems[system]:update(dt)
+ end
+end
+
+
+function SystemDb.removeSystem(self, id)
+ self.systems[id] = nil
+ for i=#self.sorted,1,-1 do
+ if self.sorted[i] == id then table.remove(self.sorted, i) end
+ end
+end
+
+
+return module