diff options
author | sanine-a <sanine.not@pm.me> | 2023-05-09 11:31:17 -0500 |
---|---|---|
committer | sanine-a <sanine.not@pm.me> | 2023-05-09 11:31:17 -0500 |
commit | a2ae7aae8357c8c2684d85fd58b0c5a0563ebab9 (patch) | |
tree | 5050161547408c80b521c9f4708bb9f7a9d0fa71 | |
parent | 2a14abecaee073aef1f1966bb397d6086b2e4785 (diff) |
refactor: split ecs systems into multiple files
-rw-r--r-- | honey/ecs-systems.lua | 375 | ||||
-rw-r--r-- | honey/ecs.lua | 277 | ||||
-rw-r--r-- | honey/ecs.lua.bak | 228 | ||||
-rw-r--r-- | honey/ecs/collision.lua | 57 | ||||
-rw-r--r-- | honey/ecs/ecs.lua | 274 | ||||
-rw-r--r-- | honey/ecs/ecs.test.lua (renamed from honey/ecs.test.lua) | 0 | ||||
-rw-r--r-- | honey/ecs/node.lua | 47 | ||||
-rw-r--r-- | honey/ecs/physics.lua | 158 | ||||
-rw-r--r-- | honey/ecs/render.lua | 98 | ||||
-rw-r--r-- | honey/ecs/script.lua | 44 | ||||
-rw-r--r-- | honey/std.lua | 1 | ||||
-rw-r--r-- | main.lua | 19 |
12 files changed, 693 insertions, 885 deletions
diff --git a/honey/ecs-systems.lua b/honey/ecs-systems.lua deleted file mode 100644 index 1213763..0000000 --- a/honey/ecs-systems.lua +++ /dev/null @@ -1,375 +0,0 @@ -local ecs = require 'honey.ecs' -local glm = require 'honey.glm' -local Mat4 = glm.Mat4 -local Vec3 = glm.Vec3 -local Quaternion = glm.Quaternion -local gl = honey.gl -local glfw = honey.glfw -local ode = honey.ode - - -local module = {} -setmetatable(module, {__index=_G}) -setfenv(1, module) - - --- helper function for retrieving script functions -local function getFunction(script) - local f = require(script.script) - if script.func then - return f[script.func] - else - return f - end -end - - ---===== dispatch messages to handlers =====-- - -dispatch = function(entities, msg, data) - local query = entities:queryComponent(msg) - for id, handler in pairs(query) do - local f = getFunction(handler) - f(entities, id, data) - end -end - - - ---===== transform cascading =====-- - -node = function(params) - return { - db = params.db, - - priority = 2, - update = function(self, dt) - local nodes = self.db:queryComponent("node") - - -- prepare nodes - for id, node in pairs(nodes) do - node._visited = false - end - - -- helper function - local function recursiveTransform(node) - if node._visited then - return node._matrix - end - - if not node.parent then - node._matrix = node.matrix - else - local parentTransform = self.db:getComponent(node.parent, "node") - local parentMatrix = recursiveTransform(parentTransform) - node._matrix = parentMatrix * node.matrix - end - node._visited = true - return node._matrix - end - - -- compute nodes - for id, node in pairs(nodes) do - recursiveTransform(node) - end - end, - } -end - - - ---===== rendering =====-- - -function draw(model, view, projection, textures, shader, mesh) - shader:use() - - -- bind textures - local texOffset = 0 - for name, texTbl in pairs(textures or {}) do - local texture = honey.image.loadImage(texTbl.filename, texTbl.params) - gl.BindTexture(gl.TEXTURE_2D + texOffset, texture.texture) - shader:setInt(name, texOffset) - texOffset = texOffset + 1 - end - - -- configure default uniforms - shader:configure{ - float={ - time=glfw.GetTime(), - }, - matrix={ - view=view, - projection=projection, - model=model, - }, - } - - -- draw mesh - mesh:drawElements() - - -- unbind textures - for i=0,texOffset-1 do - gl.BindTexture(gl.TEXTURE_2D + i, 0) - end -end - -function renderCamera(params) - return { - db = params.db, - priority = params.priority or 99, - update = function(self, dt) - for id, camera in pairs(self.db:queryComponent("camera")) do - local projection = camera.projection - local cameraTransform = self.db:getComponent(id, "node") - local view = Mat4() - if cameraTransform then - honey.glm.mat4_inv(cameraTransform._matrix.data, view.data) - else - view:identity() - end - - local entities = self.db:queryComponent("renderMesh") - for entity, tbl in pairs(entities) do - -- get model - local node = self.db:getComponent(entity, "node") - local model = - (node and node._matrix) or - Mat4():identity() - -- get shader - local shader = honey.shader.loadShader( - tbl.shader.vertex, tbl.shader.fragment - ) - -- get mesh - local mesh = honey.mesh.loadCached( - tbl.mesh.filename, tbl.mesh.index - ) - draw(model, view, projection, tbl.textures, shader, mesh) - end - - entities = self.db:queryComponent("renderQuad") - local quadmesh = honey.mesh.loadCached("builtin.quad", 1) - for entity, tbl in pairs(entities) do - -- get model - local model = Mat4():identity() - -- get shader - local shader = honey.shader.loadShader( - tbl.shader.vertex, tbl.shader.fragment - ) - draw(model, view, projection, tbl.textures, shader, quadmesh) - end - end - end, - } -end - - - ---===== script system =====-- - -script = function(params) - return { - db=params.db, - update=function(self, dt) - local entities = self.db:queryComponent("script") - for id, script in pairs(entities) do - local f = getFunction(script) - f(self.db, id, dt) - end - end - } -end - - ---===== physics =====-- - - -physics = function(params) - local interval = params.interval or 0.016 - local groupSize = params.groupSize or 20 - local refs = {} - return { - db=params.db, - space=params.space, - world=params.world, - contactGroup=ode.JointGroupCreate(groupSize), - time=interval, - - priority=1, - update=function(self, dt) - for i, ref in ipairs(refs) do - print(i, ref.tbl, ref.physics) - end - local query = self.db:queryComponent("physics") - - for id, physics in pairs(query) do - if not physics._body then - print("add physics body for "..id) - local body = ode.BodyCreate(self.world) - physics._gc = honey.util.gc_canary(function() - print("releasing physics body for " .. id) - ode.BodyDestroy(body) - body = nil - end) - - local collision = self.db:getComponent(id, "collision") - if collision then - print(id, collision.class) - ode.GeomSetBody(collision._geom, body) - end - - local mass = ode.MassCreate() - local class = physics.mass.class - if not class then - -- configure mass manually - elseif class == "sphere" then - ode.MassSetSphere( - mass, - physics.mass.density, - physics.mass.radius - ) - elseif class == "capsule" then - ode.MassSetCapsule( - mass, - physics.mass.density, - physics.mass.direction, - physics.mass.radius, - physics.mass.length - ) - end - ode.BodySetMass(body, mass) - local m = self.db:getComponent(id, "node").matrix - ode.BodySetPosition( - body, - m[1][4], m[2][4], m[3][4] - ) - ode.BodySetRotation( - body, - m[1][1], m[1][2], m[1][3], - m[2][1], m[2][2], m[2][3], - m[3][1], m[3][2], m[3][3] - ) - local vel = physics.velocity or Vec3{0,0,0} - ode.BodySetLinearVel( - body, vel[1], vel[2], vel[3] - ) - physics.velocity = vel - - local avel = physics.angularVelocity or Vec3{0,0,0} - ode.BodySetAngularVel( - body, avel[1], avel[2], avel[3] - ) - physics.angularVelocity = avel - - if physics.maxAngularSpeed then - ode.BodySetMaxAngularSpeed(body, physics.maxAngularSpeed) - end - - physics._body = body - end - end - - self.time = self.time + dt - -- only run the update every [interval] seconds - if self.time > interval then - self.time = self.time - interval - - -- check for near collisions between geoms - ode.SpaceCollide(self.space, function(a, b) - -- check for actual collisions - local collisions = ode.Collide(a, b, 1) - if #collisions > 0 then - -- set up the joint params - local contact = ode.CreateContact{ surface={ - mode = ode.ContactBounce + ode.ContactSoftCFM, - mu = ode.Infinity, - bounce = 0.90, - bounce_vel = 0.1, - soft_cfm = 0.001, - }} - ode.ContactSetGeom(contact, collisions[1]) - -- create the joint - local joint = ode.JointCreateContact( - self.world, - self.contactGroup, - contact - ) - -- attach the two bodies - local bodyA = ode.GeomGetBody(a) - local bodyB = ode.GeomGetBody(b) - ode.JointAttach(joint, bodyA, bodyB) - end - end) - -- update the world - ode.WorldQuickStep(self.world, interval) - -- remove all contact joints - ode.JointGroupEmpty(self.contactGroup) - - -- update entity nodes - for id, physics in pairs(query) do - local x,y,z = ode.BodyGetPosition(physics._body) - local d,a,b,c = ode.BodyGetQuaternion(physics._body) - local node = self.db:getComponent(id, "node") - local q = Quaternion{a,b,c,d} - node.matrix - :identity() - :translate(Vec3{x,y,z}) - :mul(Quaternion{a,b,c,d}:toMat4()) - - local vel = physics.velocity - vel[1], vel[2], vel[3] = ode.BodyGetLinearVel(physics._body) - local avel = physics.angularVelocity - avel[1], avel[2], avel[3] = ode.BodyGetAngularVel(physics._body) - end - end - end, - } -end - - ---===== collision space =====-- - - -local function createGeom(self, id, collision) - local geom - if collision.class == "sphere" then - geom = ode.CreateSphere(self.space, collision.radius) - elseif collision.class == "capsule" then - geom = ode.CreateCapsule(self.space, collision.radius, collision.length) - elseif collision.class == "plane" then - local node = self.db:getComponent(id, "node") - local m = node.matrix - local normal = node.matrix:mulv3(Vec3{0,1,0}):normalize() - local position = Vec3{m[1][4], m[2][4], m[3][4]} - print(position) - local d = normal:dot(position) - print(normal, d) - geom = ode.CreatePlane(self.space, normal[1], normal[2], normal[3], d) - end - collision._geom = geom - collision._gc = honey.util.gc_canary(function() - print("release geom for id"..id) - ode.GeomDestroy(geom) - end) -end - -function collision(params) - local db = params.db - local space = params.space - return { - db=db, - space=space, - priority=0, - update = function(self, dt) - local query = self.db:queryComponent("collision") - for id, collision in pairs(query) do - if not collision._geom then - createGeom(self, id, collision) - print(id, collision._geom) - end - end - end - } -end - - -return module diff --git a/honey/ecs.lua b/honey/ecs.lua index b0409e4..82fd877 100644 --- a/honey/ecs.lua +++ b/honey/ecs.lua @@ -1,274 +1,9 @@ -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 +local module = require 'honey.ecs.ecs' +module.collision = require 'honey.ecs.collision' +module.node = require 'honey.ecs.node' +module.physics = require 'honey.ecs.physics' +module.render = require 'honey.ecs.render' +module.script = require 'honey.ecs.script' return module diff --git a/honey/ecs.lua.bak b/honey/ecs.lua.bak deleted file mode 100644 index 3632f34..0000000 --- a/honey/ecs.lua.bak +++ /dev/null @@ -1,228 +0,0 @@ -local module = {} -setmetatable(module, {__index=_G}) -setfenv(1, module) - - ---===== filters =====-- - - -Filter = {} - --- base filter creation -function Filter.new(_, operation, tbl) - local self = {} - self.keys = {} - self.filters = {} - self.op = operation - - for _, v in ipairs(tbl) do - if type(v) == "function" then - table.insert(self.filters, v) - elseif type(v) == "string" then - table.insert(self.keys, v) - end - end - - return function(entity, _self) - local entity = _self or entity -- able to call as . or : - return Filter.check(self, entity) - end -end -setmetatable(Filter, {__call=Filter.new}) - - --- base filter checking -function Filter.check(self, entity) - local funcname = "check" .. self.op - return Filter[funcname](self, entity) -end - - --- AND filter (all keys and subfilters must match) -function Filter.AND(tbl) - return Filter("AND", tbl) -end -function Filter.checkAND(self, entity) - for _, subfilter in ipairs(self.filters) do - if not subfilter(entity) then return false end - end - for _, key in ipairs(self.keys) do - if entity[key] == nil then return false end - end - return true -end - - --- OR filter (at least one key or subfilter must match) -function Filter.OR(tbl) - return Filter("OR", tbl) -end -function Filter.checkOR(self, entity) - for _, subfilter in ipairs(self.filters) do - if subfilter(entity) then return true end - end - for _, key in ipairs(self.keys) do - if entity[key] ~= nil then return true end - end - return false -end - - --- NAND filter (at least one key or subfilter must NOT match) -function Filter.NAND(tbl) - return Filter("NAND", tbl) -end -function Filter.checkNAND(self, entity) - for _, subfilter in ipairs(self.filters) do - if not subfilter(entity) then return true end - end - for _, key in ipairs(self.keys) do - if entity[key] == nil then return true end - end - return false -end - - --- NOR filter (no keys or subfilters may match) -function Filter.NOR(tbl) - return Filter("NOR", tbl) -end -function Filter.checkNOR(self, entity) - for _, subfilter in ipairs(self.filters) do - if subfilter(entity) then return false end - end - for _, key in ipairs(self.keys) do - if entity[key] ~= nil then return false end - end - return true -end - - ---===== levels =====-- - -Level = {} -Level.__index = Level - -function Level.new(_) - local self = {} - self.systems = {} - self.entities = {} - self.nextId = 1 - setmetatable(self, Level) - return self -end -setmetatable(Level, {__call=Level.new}) - - -local function systemLt(a, b) - return (a.priority or 50) < (b.priority or 50) -end -function Level.addSystem(self, system) - assert(system.update, "systems must have an 'update' key") - assert(system.filter, "systems must have a 'filter' key") - system.entities = {} - table.insert(self.systems, system) - table.sort(self.systems, systemLt) - if system.setup then - system.setup(system) - end -end - - -local function addEntityToSystem(system, id, entity) - -- check if entity is already present - if system.entities[id] then return end - - if system.onAddEntity then - system:onAddEntity(id, entity) - end - system.entities[id] = true -end - - -local function removeEntityFromSystem(system, id, entity) - -- check if entity is already absent - if not system.entities[id] then return end - - if system.onRemoveEntity then - system:onRemoveEntity(id, entity) - end - system.entities[id] = nil -end - - -function Level.addEntity(self, entity) - local id = self.nextId - self.entities[id] = entity - self.nextId = id + 1 - - for _, system in ipairs(self.systems) do - if system.filter(entity) then - addEntityToSystem(system, id, entity) - end - end - - return id -end - - -function Level.reconfigureEntity(self, id) - local entity = self.entities[id] - - for _, system in ipairs(self.systems) do - if system.filter(entity) then - addEntityToSystem(system, id, entity) - else - removeEntityFromSystem(system, id, entity) - end - end -end - - -function Level.removeEntity(self, id) - local entity = self.entities[id] - if not entity then error("bad id: "..tostring(id)) end - for _, system in ipairs(self.systems) do - removeEntityFromSystem(system, id, entity) - end - self.entities[id] = nil -end - - -function Level.reconfigureAllEntities(self) - for id in ipairs(self.entities) do - self:reconfigureEntity(id) - end -end - - -function Level.update(self, dt, paused) - local paused = paused or false - for _, system in ipairs(self.systems) do - if (not paused) or (paused and system.nopause) then - if system.preUpdate then - system:preUpdate() - end - if system.prepareEntity then - for id in pairs(system.entities) do - local entity = self.entities[id] - if entity then - system:prepareEntity(entity) - end - end - end - for id in pairs(system.entities) do - local entity = self.entities[id] - if entity then - system:update(entity, dt) - end - end - if system.postUpdate then - system:postUpdate() - end - end - end -end - - -return module diff --git a/honey/ecs/collision.lua b/honey/ecs/collision.lua new file mode 100644 index 0000000..722c256 --- /dev/null +++ b/honey/ecs/collision.lua @@ -0,0 +1,57 @@ +local glm = require 'honey.glm' +local Vec3 = glm.Vec3 +local ode = honey.ode + +local module = {} +setmetatable(module, {__index=_G}) +setfenv(1, module) + + +--===== collision space =====-- + + +local function createGeom(self, id, collision) + local geom + if collision.class == "sphere" then + geom = ode.CreateSphere(self.space, collision.radius) + elseif collision.class == "capsule" then + geom = ode.CreateCapsule(self.space, collision.radius, collision.length) + elseif collision.class == "plane" then + local node = self.db:getComponent(id, "node") + local m = node.matrix + local normal = node.matrix:mulv3(Vec3{0,1,0}):normalize() + local position = Vec3{m[1][4], m[2][4], m[3][4]} + print(position) + local d = normal:dot(position) + print(normal, d) + geom = ode.CreatePlane(self.space, normal[1], normal[2], normal[3], d) + end + collision._geom = geom + collision._gc = honey.util.gc_canary(function() + print("release geom for id"..id) + ode.GeomDestroy(geom) + end) +end + +system = function(params) + local db = params.db + local space = params.space + return { + db=db, + space=space, + priority=0, + update = function(self, dt) + local query = self.db:queryComponent("collision") + for id, collision in pairs(query) do + if not collision._geom then + createGeom(self, id, collision) + print(id, collision._geom) + end + end + end + } +end + + + +return module 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 diff --git a/honey/ecs.test.lua b/honey/ecs/ecs.test.lua index 74e27dc..74e27dc 100644 --- a/honey/ecs.test.lua +++ b/honey/ecs/ecs.test.lua diff --git a/honey/ecs/node.lua b/honey/ecs/node.lua new file mode 100644 index 0000000..39f1898 --- /dev/null +++ b/honey/ecs/node.lua @@ -0,0 +1,47 @@ +local module = {} +setmetatable(module, {__index=_G}) +setfenv(1, module) + +--===== transform cascading =====-- + +system = function(params) + return { + db = params.db, + + priority = 2, + update = function(self, dt) + local nodes = self.db:queryComponent("node") + + -- prepare nodes + for id, node in pairs(nodes) do + node._visited = false + end + + -- helper function + local function recursiveTransform(node) + if node._visited then + return node._matrix + end + + if not node.parent then + node._matrix = node.matrix + else + local parentTransform = self.db:getComponent(node.parent, "node") + local parentMatrix = recursiveTransform(parentTransform) + node._matrix = parentMatrix * node.matrix + end + node._visited = true + return node._matrix + end + + -- compute nodes + for id, node in pairs(nodes) do + recursiveTransform(node) + end + end, + } +end + + + +return module diff --git a/honey/ecs/physics.lua b/honey/ecs/physics.lua new file mode 100644 index 0000000..eac3846 --- /dev/null +++ b/honey/ecs/physics.lua @@ -0,0 +1,158 @@ +local glm = require 'honey.glm' +local Vec3 = glm.Vec3 +local Mat4 = glm.Mat4 +local Quaternion = glm.Quaternion +local ode = honey.ode + +local module = {} +setmetatable(module, {__index=_G}) +setfenv(1, module) + +--===== physics =====-- + + +system = function(params) + local interval = params.interval or 0.016 + local groupSize = params.groupSize or 20 + local refs = {} + return { + db=params.db, + space=params.space, + world=params.world, + contactGroup=ode.JointGroupCreate(groupSize), + time=interval, + + priority=1, + update=function(self, dt) + for i, ref in ipairs(refs) do + print(i, ref.tbl, ref.physics) + end + local query = self.db:queryComponent("physics") + + for id, physics in pairs(query) do + if not physics._body then + print("add physics body for "..id) + local body = ode.BodyCreate(self.world) + physics._gc = honey.util.gc_canary(function() + print("releasing physics body for " .. id) + ode.BodyDestroy(body) + body = nil + end) + + local collision = self.db:getComponent(id, "collision") + if collision then + print(id, collision.class) + ode.GeomSetBody(collision._geom, body) + end + + local mass = ode.MassCreate() + local class = physics.mass.class + if not class then + -- configure mass manually + elseif class == "sphere" then + ode.MassSetSphere( + mass, + physics.mass.density, + physics.mass.radius + ) + elseif class == "capsule" then + ode.MassSetCapsule( + mass, + physics.mass.density, + physics.mass.direction, + physics.mass.radius, + physics.mass.length + ) + end + ode.BodySetMass(body, mass) + local m = self.db:getComponent(id, "node").matrix + ode.BodySetPosition( + body, + m[1][4], m[2][4], m[3][4] + ) + ode.BodySetRotation( + body, + m[1][1], m[1][2], m[1][3], + m[2][1], m[2][2], m[2][3], + m[3][1], m[3][2], m[3][3] + ) + local vel = physics.velocity or Vec3{0,0,0} + ode.BodySetLinearVel( + body, vel[1], vel[2], vel[3] + ) + physics.velocity = vel + + local avel = physics.angularVelocity or Vec3{0,0,0} + ode.BodySetAngularVel( + body, avel[1], avel[2], avel[3] + ) + physics.angularVelocity = avel + + if physics.maxAngularSpeed then + ode.BodySetMaxAngularSpeed(body, physics.maxAngularSpeed) + end + + physics._body = body + end + end + + self.time = self.time + dt + -- only run the update every [interval] seconds + if self.time > interval then + self.time = self.time - interval + + -- check for near collisions between geoms + ode.SpaceCollide(self.space, function(a, b) + -- check for actual collisions + local collisions = ode.Collide(a, b, 1) + if #collisions > 0 then + -- set up the joint params + local contact = ode.CreateContact{ surface={ + mode = ode.ContactBounce + ode.ContactSoftCFM, + mu = ode.Infinity, + bounce = 0.90, + bounce_vel = 0.1, + soft_cfm = 0.001, + }} + ode.ContactSetGeom(contact, collisions[1]) + -- create the joint + local joint = ode.JointCreateContact( + self.world, + self.contactGroup, + contact + ) + -- attach the two bodies + local bodyA = ode.GeomGetBody(a) + local bodyB = ode.GeomGetBody(b) + ode.JointAttach(joint, bodyA, bodyB) + end + end) + -- update the world + ode.WorldQuickStep(self.world, interval) + -- remove all contact joints + ode.JointGroupEmpty(self.contactGroup) + + -- update entity nodes + for id, physics in pairs(query) do + local x,y,z = ode.BodyGetPosition(physics._body) + local d,a,b,c = ode.BodyGetQuaternion(physics._body) + local node = self.db:getComponent(id, "node") + local q = Quaternion{a,b,c,d} + node.matrix + :identity() + :translate(Vec3{x,y,z}) + :mul(Quaternion{a,b,c,d}:toMat4()) + + local vel = physics.velocity + vel[1], vel[2], vel[3] = ode.BodyGetLinearVel(physics._body) + local avel = physics.angularVelocity + avel[1], avel[2], avel[3] = ode.BodyGetAngularVel(physics._body) + end + end + end, + } +end + + + +return module diff --git a/honey/ecs/render.lua b/honey/ecs/render.lua new file mode 100644 index 0000000..4217422 --- /dev/null +++ b/honey/ecs/render.lua @@ -0,0 +1,98 @@ +local glm = require 'honey.glm' +local Vec3 = glm.Vec3 +local Mat4 = glm.Mat4 + +local gl = honey.gl +local glfw = honey.glfw + +local module = {} +setmetatable(module, {__index=_G}) +setfenv(1, module) + +--===== rendering =====-- + +function draw(model, view, projection, textures, shader, mesh) + shader:use() + + -- bind textures + local texOffset = 0 + for name, texTbl in pairs(textures or {}) do + local texture = honey.image.loadImage(texTbl.filename, texTbl.params) + gl.BindTexture(gl.TEXTURE_2D + texOffset, texture.texture) + shader:setInt(name, texOffset) + texOffset = texOffset + 1 + end + + -- configure default uniforms + shader:configure{ + float={ + time=glfw.GetTime(), + }, + matrix={ + view=view, + projection=projection, + model=model, + }, + } + + -- draw mesh + mesh:drawElements() + + -- unbind textures + for i=0,texOffset-1 do + gl.BindTexture(gl.TEXTURE_2D + i, 0) + end +end + +system = function(params) + return { + db = params.db, + priority = params.priority or 99, + update = function(self, dt) + for id, camera in pairs(self.db:queryComponent("camera")) do + local projection = camera.projection + local cameraTransform = self.db:getComponent(id, "node") + local view = Mat4() + if cameraTransform then + honey.glm.mat4_inv(cameraTransform._matrix.data, view.data) + else + view:identity() + end + + local entities = self.db:queryComponent("renderMesh") + for entity, tbl in pairs(entities) do + -- get model + local node = self.db:getComponent(entity, "node") + local model = + (node and node._matrix) or + Mat4():identity() + -- get shader + local shader = honey.shader.loadShader( + tbl.shader.vertex, tbl.shader.fragment + ) + -- get mesh + local mesh = honey.mesh.loadCached( + tbl.mesh.filename, tbl.mesh.index + ) + draw(model, view, projection, tbl.textures, shader, mesh) + end + + entities = self.db:queryComponent("renderQuad") + local quadmesh = honey.mesh.loadCached("builtin.quad", 1) + for entity, tbl in pairs(entities) do + -- get model + local model = Mat4():identity() + -- get shader + local shader = honey.shader.loadShader( + tbl.shader.vertex, tbl.shader.fragment + ) + draw(model, view, projection, tbl.textures, shader, quadmesh) + end + end + end, + } +end + + + +return module diff --git a/honey/ecs/script.lua b/honey/ecs/script.lua new file mode 100644 index 0000000..9ae7d72 --- /dev/null +++ b/honey/ecs/script.lua @@ -0,0 +1,44 @@ +local module = {} +setmetatable(module, {__index=_G}) +setfenv(1, module) + + +-- helper function for retrieving script functions +getFunction = function(script) + local f = require(script.script) + if script.func then + return f[script.func] + else + return f + end +end + + +--===== dispatch messages to handlers =====-- + +dispatch = function(entities, msg, data) + local query = entities:queryComponent(msg) + for id, handler in pairs(query) do + local f = getFunction(handler) + f(entities, id, data) + end +end + +--===== script system =====-- + +system = function(params) + return { + db=params.db, + update=function(self, dt) + local entities = self.db:queryComponent("script") + for id, script in pairs(entities) do + local f = getFunction(script) + f(self.db, id, dt) + end + end + } +end + + + +return module diff --git a/honey/std.lua b/honey/std.lua index b16708a..b0b6a15 100644 --- a/honey/std.lua +++ b/honey/std.lua @@ -5,7 +5,6 @@ honey.loop = init.loop honey.terminate = init.terminate honey.ecs = require 'honey.ecs' -honey.standardSystems = require 'honey.ecs-systems' honey.mesh = require 'honey.mesh' honey.shader = require 'honey.shader' honey.Window = require 'honey.window' @@ -7,7 +7,6 @@ local Mat4 = honey.Mat4 local Quaternion = honey.Quaternion local ecs = honey.ecs local sys = honey.standardSystems -local dispatch = sys.dispatch local ode = honey.ode local nvg = honey.nvg @@ -35,12 +34,12 @@ end) -- setup ecs local entities = ecs.EntityDb() local systems = ecs.SystemDb(entities) - -systems:addSystem(sys.node) -systems:addSystem(sys.renderCamera) -systems:addSystem(sys.script) -systems:addSystem(sys.collision, {space=space}) -systems:addSystem(sys.physics, {space=space, world=world}) +local script = ecs.script +systems:addSystem(ecs.node.system) +systems:addSystem(ecs.render.system) +systems:addSystem(ecs.script.system) +systems:addSystem(ecs.collision.system, {space=space}) +systems:addSystem(ecs.physics.system, {space=space, world=world}) package.loaded['baseRotationScript'] = function(entities, id, dt) local node = entities:getComponent(id, "node") node.matrix:rotateZ(math.pi * dt) @@ -299,7 +298,7 @@ setupEntities() -- close window on ESCAPE key window:setKeyCallback(function(_, key, scancode, action) - dispatch(entities, "onKey", {key=key, scancode=scancode, action=action}) + script.dispatch(entities, "onKey", {key=key, scancode=scancode, action=action}) if action == glfw.PRESS then if key == glfw.KEY_ESCAPE then window:setShouldClose(true) @@ -313,14 +312,14 @@ end) window:setCursorPosCallback(function(_, xpos, ypos) - dispatch(entities, "onCursorPos", {xpos=xpos, ypos=ypos}) + script.dispatch(entities, "onCursorPos", {xpos=xpos, ypos=ypos}) end) -- resize window correctly window:setFramebufferSizeCallback(function(_, width, height) gl.Viewport(0, 0, width, height) vw, vh = width, height - dispatch(entities, "onWindowResize", { width=width, height=height }) + script.dispatch(entities, "onWindowResize", { width=width, height=height }) end) package.loaded["cameraHandleResize"] = function(entities, id, data) |