math.randomseed(os.time()) local module = {} setmetatable(module, {__index=_G}) setfenv(1, module) --===== Components =====-- -- Components provide a kind-of-type-safe way of storing values, since they are -- guaranteed to have a specific set of keys and no other values in them. They also serialize -- nicely for storage. Component = {} function Component.newFactory(name, params, serialize) -- create the metatable for this component type local metatable = {} metatable.__tostring = serialize or Component.serialize -- nice serialization metatable.__newindex = function(tbl, index, value) if params[index] ~= nil then tbl[index] = value else -- throw an error if we try to set a key that is not in the valid set error(string.format("%q is not a valid key for a component of type %q", index, name)) end end -- the factory function for creating components return function(tbl) local tbl = tbl or {} local self = { __type=name } -- ensure that all keys are present for k, v in pairs(params) do self[k] = v end setmetatable(self, metatable) -- set the keys from the factory call for k, v in pairs(tbl) do self[k] = v end return self end 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 --===== 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}) -- 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 } end local component = self.components[name] component[id] = value component.count = component.count + 1 end local function shallowCopy(tbl) local copy = {} for k, v in pairs(tbl) do copy[k] = v end return copy end -- get all entities with a given component function EntityDb.queryComponent(self, name) local query = shallowCopy(self.components[name]) query.count = nil return query 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[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][id] end -- remove a component from an entity function EntityDb.removeComponent(self, id, name) self:checkIsValid(id) local component = self.components[name] if component[id] ~= nil then component[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(_) local self = { systems = {}, sorted = {}, } 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 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