summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsanine-a <sanine.not@pm.me>2023-05-09 11:31:17 -0500
committersanine-a <sanine.not@pm.me>2023-05-09 11:31:17 -0500
commita2ae7aae8357c8c2684d85fd58b0c5a0563ebab9 (patch)
tree5050161547408c80b521c9f4708bb9f7a9d0fa71
parent2a14abecaee073aef1f1966bb397d6086b2e4785 (diff)
refactor: split ecs systems into multiple files
-rw-r--r--honey/ecs-systems.lua375
-rw-r--r--honey/ecs.lua277
-rw-r--r--honey/ecs.lua.bak228
-rw-r--r--honey/ecs/collision.lua57
-rw-r--r--honey/ecs/ecs.lua274
-rw-r--r--honey/ecs/ecs.test.lua (renamed from honey/ecs.test.lua)0
-rw-r--r--honey/ecs/node.lua47
-rw-r--r--honey/ecs/physics.lua158
-rw-r--r--honey/ecs/render.lua98
-rw-r--r--honey/ecs/script.lua44
-rw-r--r--honey/std.lua1
-rw-r--r--main.lua19
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'
diff --git a/main.lua b/main.lua
index 71ef11b..efee1a2 100644
--- a/main.lua
+++ b/main.lua
@@ -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)