diff options
Diffstat (limited to 'honey/ecs/ecs.lua')
-rw-r--r-- | honey/ecs/ecs.lua | 274 |
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 |