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