math.randomseed(os.time()) local testing, glm = pcall(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) self.entities = {} self.components = {} collectgarbage() local env = { Entity = function(id, components) self:createEntity(id) self:addComponents(id, components) end, } if not testing then env.Vec3 = glm.Vec3 env.Mat4 = glm.Mat4 end 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) local components = self.components[name] if not components then return nil end return components.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 --===== Systems =====-- System = {} System.__index = System function System.new(_, name, f) local self = { name = name, f = f, dependencies = {}, } setmetatable(self, System) return self end setmetatable(System, {__call=System.new}) function System.addDependency(self, system) self.dependencies[system.name] = true return self end function System.addDependencies(self, systems) for _, system in ipairs(systems) do self:addDependency(system) end return self end --===== SystemDb =====-- SystemDb = {} SystemDb.__index = SystemDb function SystemDb.new(_, entityDb) local self = { systems = {}, params = {}, sort = {}, dangling = {}, entityDb = entityDb, } setmetatable(self, SystemDb) return self end setmetatable(SystemDb, {__call=SystemDb.new}) -- depth-first topological sort -- thank god for wikipedia local function tsort(systems) local graph = {} local count = 0 for _, system in pairs(systems) do graph[system.name] = { name=system.name, dependencies=system.dependencies, tmark=false, pmark=false } count = count+1 end local sort = {} local dangling = {} local visit visit = function(node) if node == nil then -- dangling dependency, ignore for now return true end if node.pmark then return false end if node.tmark then error("dependency cycle detected") end node.tmark = true for name in pairs(node.dependencies) do local dangle = visit(graph[name]) if dangle then dangling[name] = true end end node.tmark = false node.pmark = true count = count-1 table.insert(sort, node.name) end for _, node in pairs(graph) do visit(node) if count == 0 then break end end return sort, dangling end function SystemDb.addSystems(self, systems, params) for _, system in ipairs(systems) do self.systems[system.name] = system self.params[system.name] = params end self.sort, self.dangling = tsort(self.systems) return self end function SystemDb.update(self, dt) -- check for dangling dependencies local dangle = "" for dependency in pairs(self.dangling) do dangle = dangle .. string.format( "unresolved dangling dependency: %s\n", dependency ) end if dangle ~= "" then error(dangle) end for _, name in ipairs(self.sort) do local system = self.systems[name] local params = self.params[name] system.f(self.entityDb, dt, params) end end function SystemDb.removeSystems(self, systems) for _, system in pairs(systems) do self.systems[system.name] = nil end self.sort, self.dangling = tsort(self.systems) end --===== Access helper =====-- function Accessor(db, id) local tbl = { __db = db, __id = id, } setmetatable(tbl, { __index=function(self, key) return self.__db:getComponent(self.__id, key) end, __newindex=function(self, key, value) self.__db:addComponent(self.__id, key, value) end, __tostring=function(self) return string.format("Accessor<%s>", self.__id) end, }) return tbl end return module