diff --git a/README.md b/README.md index b79c030..4ac1eab 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ local Aspect = jojo.Aspect local System = jojo.System local personAspect = Aspect({"name", "mass", "phrase"}) + local talkingSystem = System( nil, function (p, delta) @@ -55,8 +56,6 @@ local talkingSystem = System( personAspect ) -local world = World(talkingSystem) - local joe = { name = "Joe", phrase = "I'm a plumber.", @@ -64,7 +63,7 @@ local joe = { hairColor = "brown" } -world:add(joe) +local world = World(talkingSystem, joe) for i = 1, 20 do world:update(1) @@ -72,8 +71,8 @@ end ``` ## Testing ## -To test Jojo, simply clone the project, cd into the project directory, and -run `lua jojotest.lua` from command line. +Jojo uses [busted](http://olivinelabs.com/busted/) for testing. Install and run +`busted` from the command line to test. ## TODO ## diff --git a/jojo.lua b/jojo.lua index 9d54e97..c6922a5 100644 --- a/jojo.lua +++ b/jojo.lua @@ -1,11 +1,18 @@ local jojo = { - _VERSION = "0.0.4", + _VERSION = "0.1.0", _URL = "https://github.com/bakpakin/Jojo", - _DESCRIPTION = "Entity Component System for lua." + _DESCRIPTION = "jojo - Entity Component System for lua." } --- Simplified class implementation with no inheritance or polymorphism. +local tinsert = table.insert +local tremove = table.remove +local tconcat = table.concat +local pairs = pairs +local ipairs = ipairs local setmetatable = setmetatable +local getmetatable = getmetatable + +-- Simplified class implementation with no inheritance or polymorphism. local function class(name) local c = {} local mt = {} @@ -31,12 +38,6 @@ jojo.World = World jojo.Aspect = Aspect jojo.System = System -local tinsert = table.insert -local tremove = table.remove -local tconcat = table.concat -local pairs = pairs -local ipairs = ipairs - ----- Aspect ----- -- Aspect(required, excluded, oneRequired) @@ -200,12 +201,9 @@ end -- World(...) --- Creates a new World with the given Systems in order. TODO - Add or remove --- Systems after creation. +-- Creates a new World with the given Systems and Entities in order. function World:init(...) - local args = {...} - -- Table of Entities to status self.status = {} @@ -215,26 +213,20 @@ function World:init(...) -- Number of Entities in World. self.entityCount = 0 + -- Number of Systems in World. + self.systemCount = 0 + -- List of Systems - self.systems = args + self.systems = {} -- Table of Systems to whether or not they are active. - local activeSystems = {} - self.activeSystems = activeSystems + self.activeSystems = {} -- Table of Systems to System Indices - local systemIndices = {} - self.systemIndices = systemIndices + self.systemIndices = {} -- Table of Systems to Sets of matching Entities - local systemEntities = {} - self.systemEntities = systemEntities - - for i, sys in ipairs(args) do - activeSystems[sys] = true - systemEntities[sys] = {} - systemIndices[sys] = i - end + self.systemEntities = {} -- List of Systems to add next update self.systemsToAdd = {} @@ -242,73 +234,63 @@ function World:init(...) -- List of Systems to remove next update self.systemsToRemove = {} + -- Add Systems and Entities + self:add(...) + self:manageSystems() + self:manageEntities() end function World:__tostring() return "JojoWorld" end --- World:addSystems(...) - --- Appends Systems in order to the World. Systems will be added after the next --- call to World:update(dt) -function World:addSystems(...) - local args = {...} - local systemsToAdd = self.systemsToAdd - for i, sys in ipairs(args) do - tinsert(systemsToAdd, sys) - end -end - --- World:freeSystems(...) - --- Appends Systems in order to the World. Systems will be added after the next --- call to World:update(dt) -function World:removeSystems(...) - local args = {...} - local systemsToRemove = self.systemsToRemove - for i, sys in ipairs(args) do - tinsert(systemsToRemove, sys) - end -end - -- World:add(...) --- Adds Entities to the World. Entities will enter the World the next time --- World:update(dt) is called. Also call this method when an Entity has had its --- Components changed, such that it matches different Aspects. +-- Adds Entities and Systems to the World. New objects will enter the World the +-- next time World:update(dt) is called. Also call this method when an Entity +-- has had its Components changed, such that it matches different Aspects. function World:add(...) local args = {...} local status = self.status local entities = self.entities - for _, e in ipairs(args) do - entities[e] = true - status[e] = "add" + local systemsToAdd = self.systemsToAdd + for _, obj in ipairs(args) do + if getmetatable(obj) == System then + tinsert(systemsToAdd, obj) + else -- Assume obj is an Entity + entities[obj] = true + status[obj] = "add" + end end end -- World:free(...) --- Removes Entities from the World. Entities will exit the World the next time --- World:update(dt) is called. +-- Removes Entities and Systems from the World. Objects will exit the World the +-- next time World:update(dt) is called. function World:remove(...) local args = {...} local status = self.status - for _, e in ipairs(args) do - status[e] = "remove" + local systemsToRemove = self.systemsToRemove + for _, obj in ipairs(args) do + if getmetatable(obj) == System then + tinsert(systemsToRemove, obj) + else -- Assume obj is an Entity + status[obj] = "remove" + end end end -- World:updateSystem(system, dt) +-- Updates a System function World:updateSystem(system, dt) local preupdate = system.preupdate local update = system.update - local systemEntities = self.systemEntities if preupdate then preupdate(dt) @@ -316,7 +298,7 @@ function World:updateSystem(system, dt) if update then local entities = self.entities - local es = systemEntities[system] + local es = self.systemEntities[system] if es then for e in pairs(es) do update(e, dt) @@ -324,61 +306,84 @@ function World:updateSystem(system, dt) end end end --- World:update() --- Updates the World, frees Entities that have been marked for freeing, adds --- entities that have been marked for adding, etc. -function World:update(dt) +-- World:manageSystems() - local statuses = self.status - local systemEntities = self.systemEntities - local systemIndices = self.systemIndices - local entities = self.entities - local systems = self.systems - local systemsToAdd = self.systemsToAdd - local systemsToRemove = self.systemsToRemove - local activeSystems = self.activeSystems +-- Adds and removes Systems that have been marked from the world. The user of +-- this library should seldom if ever call this. +function World:manageSystems() - -- Remove all Systems queued for removal - for i = #systemsToRemove, 1, -1 do - -- Pop system off the remove stack - local sys = systemsToRemove[i] - systemsToRemove[i] = nil + local systemEntities = self.systemEntities + local systemIndices = self.systemIndices + local entities = self.entities + local systems = self.systems + local systemsToAdd = self.systemsToAdd + local systemsToRemove = self.systemsToRemove + local activeSystems = self.activeSystems - local sysIndex = systemIndices[sys] - tremove(systems, sysIndex) + -- Keep track of the number of Systems in the world + local deltaSystemCount = 0 - local removeCallback = sys.remove - if removeCallback then -- call 'remove' on all entities in the System - for e in pairs(systemEntities[sys]) do - removeCallback(e) + -- Remove all Systems queued for removal + for i = 1, #systemsToRemove do + -- Pop system off the remove queue + local sys = systemsToRemove[i] + systemsToRemove[i] = nil + + local sysID = systemIndices[sys] + if sysID then + tremove(systems, sysID) + + local removeCallback = sys.remove + if removeCallback then -- call 'remove' on all entities in the System + for e in pairs(systemEntities[sys]) do + removeCallback(e) + end + end + + systemEntities[sys] = nil + activeSystems[sys] = nil + deltaSystemCount = deltaSystemCount - 1 end end - systemEntities[sys] = nil - activeSystems[sys] = nil - end + -- Add Systems queued for addition + for i = 1, #systemsToAdd do + -- Pop system off the add queue + local sys = systemsToAdd[i] + systemsToAdd[i] = nil - -- Add Systems queued for addition - for i = 1, #systemsToAdd do - -- Pop system off the add queue - local sys = systemsToAdd[i] - systemsToAdd[i] = nil + -- Add system to world + local es = {} + systemEntities[sys] = es + tinsert(systems, sys) + systemIndices[sys] = #systems + activeSystems[sys] = true - -- Add system to world - local es = {} - systemEntities[sys] = es - tinsert(systems, sys) - systemIndices[sys] = #systems - activeSystems[sys] = true + local a = sys.aspect + for e in pairs(entities) do + es[e] = a:matches(e) and true or nil + end - local a = sys.aspect - for e in pairs(entities) do - es[e] = a:matches(e) and true or nil + deltaSystemCount = deltaSystemCount + 1 end - end - -- Kepp track of number of Entities in World + -- Update the number of Systems in the World + self.systemCount = self.systemCount + deltaSystemCount +end + +-- World:manageEntities() + +-- Adds and removes Entities that have been marked. The user of this library +-- should seldom if ever call this. +function World:manageEntities() + + local statuses = self.status + local systemEntities = self.systemEntities + local entities = self.entities + local systems = self.systems + + -- Keep track of the number of Entities in the World local deltaEntityCount = 0 -- Add, remove, or change Entities @@ -410,9 +415,20 @@ function World:update(dt) -- Update Entity count self.entityCount = self.entityCount + deltaEntityCount +end + +-- World:update() + +-- Updates the World, frees Entities that have been marked for freeing, adds +-- entities that have been marked for adding, etc. +function World:update(dt) + + self:manageSystems() + self:manageEntities() + -- Iterate through Systems IN ORDER for _, s in ipairs(self.systems) do - if activeSystems[s] then + if self.activeSystems[s] then self:updateSystem(s, dt) end end diff --git a/jojotest.lua b/jojotest.lua deleted file mode 100644 index 8794bb1..0000000 --- a/jojotest.lua +++ /dev/null @@ -1,111 +0,0 @@ -local jojo = require "jojo" - -local World = jojo.World -local Aspect = jojo.Aspect -local System = jojo.System - --- Aspect Test -- - -local e1 = { - xform = { - x = 0, - y = 0 - }, - vel = { - x = 1, - y = 2, - }, - name = "E1", - size = 11, - description = "It goes to 11.", - spinalTap = true -} - -local e2 = { - xform = { - x = 2, - y = 2 - }, - vel = { - x = -1, - y = 0, - }, - name = "E2", - size = 10, - description = "It does not go to 11." -} - -local e3 = { - xform = { - x = 4, - y = 5 - }, - vel = { - x = 0, - y = 3, - }, - name = "E3", - size = 8, - description = "The smallest entity." -} - -local atap = Aspect({"spinalTap"}) -local avel = Aspect({"vel"}) -local axform = Aspect({"xform"}) -local aall = Aspect({}) -local aboth = Aspect.compose(atap, axform) -local amove = Aspect.compose(axform, avel) - -assert(atap:matches(e1), "Aspect atap should match e1.") -assert(not atap:matches(e2), "Aspect atap should not match e2.") -assert(axform:matches(e1), "Aspect axform should match e1.") -assert(axform:matches(e2), "Aspect axform should match e2.") -assert(aboth:matches(e1), "Aspect aboth should match e1.") -assert(not aboth:matches(e2), "Aspect aboth should not match e2.") - -local moves = System( - nil, - function(e, dt) - local xform = e.xform - local vel = e.vel - local x, y = xform.x, xform.y - local xvel, yvel = vel.x, vel.y - xform.x, xform.y = x + xvel * dt, y + yvel * dt - end, - amove -) - -local world = World(moves, System(nil, nil, aall)) - -world:add(e1, e2, e3) -world:update(21) -assert(e1.xform.x == 21, "e1.xform.x should be 21, but is " .. e1.xform.x) -assert(e2.xform.x == -19, "e2.xform.x should be -19, but is " .. e2.xform.x) -assert(e3.xform.y == 68, "e3.xform.y should be 68, but is " .. e3.xform.y) - -world:removeSystems(moves) -world:update(1234567890) -world:addSystems(moves) - -world:remove(e3, e2) -world:update(20) -assert(e1.xform.x == 41, "e1.xform.x should be 41, but is " .. e1.xform.x) -assert(e2.xform.x == -19, "e2.xform.x should be -19, but is " .. e2.xform.x) -assert(e3.xform.y == 68, "e3.xform.y should be 68, but is " .. e3.xform.y) - -world:removeSystems(moves) -world:update(12345) -world:addSystems(moves) - -world:add(e3, e2) -world:update(19) -world:remove(e3, e2) -e1.vel = nil -world:add(e1) -world:update(11) - -assert(e1.xform.x == 60, "e1.xform.x should be 60, but is " .. e1.xform.x) - --- TODO add some more tests, add some better tests. - -print("Passed all tests.") diff --git a/spec/jojo_spec.lua b/spec/jojo_spec.lua new file mode 100644 index 0000000..0286e00 --- /dev/null +++ b/spec/jojo_spec.lua @@ -0,0 +1,145 @@ +local jojo = require "jojo" + +local World = jojo.World +local Aspect = jojo.Aspect +local System = jojo.System + +-- Taken from answer at http://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value +local function deep_copy(o, seen) + seen = seen or {} + if o == nil then return nil end + if seen[o] then return seen[o] end + + local no + if type(o) == 'table' then + no = {} + seen[o] = no + + for k, v in next, o, nil do + no[deep_copy(k, seen)] = deep_copy(v, seen) + end + setmetatable(no, deep_copy(getmetatable(o), seen)) + else -- number, string, boolean, etc + no = o + end + return no +end + +local entityTemplate1 = { + xform = {x = 0, y = 0}, + vel = {x = 1, y = 2}, + name = "E1", + size = 11, + description = "It goes to 11.", + spinalTap = true +} + +local entityTemplate2 = { + xform = {x = 2, y = 2}, + vel = {x = -1, y = 0}, + name = "E2", + size = 10, + description = "It does not go to 11." +} + +local entityTemplate3 = { + xform = {x = 4, y = 5}, + vel = {x = 0, y = 3}, + name = "E3", + size = 8, + description = "The smallest entity." +} + +describe('Jojo:', function() + + describe('Aspect:', function() + + local entity1, entity2, entity3 + + before_each(function() + entity1 = deep_copy(entityTemplate1) + entity2 = deep_copy(entityTemplate2) + entity3 = deep_copy(entityTemplate3) + end) + + it("Correctly match Aspects and Entities", function() + + local atap = Aspect({"spinalTap"}) + local avel = Aspect({"vel"}) + local axform = Aspect({"xform"}) + local aall = Aspect({}) + local aboth = Aspect.compose(atap, axform) + local amove = Aspect.compose(axform, avel) + + assert.truthy(atap:matches(entity1)) + assert.falsy(atap:matches(entity2)) + assert.truthy(axform:matches(entity1)) + assert.truthy(axform:matches(entity2)) + assert.truthy(aboth:matches(entity1)) + assert.falsy(aboth:matches(entity2)) + + end) + + end) + + describe('World:', function() + + local world, entity1, entity2, entity3 + + local moveSystem = System( + nil, + function(e, dt) + local xform = e.xform + local vel = e.vel + local x, y = xform.x, xform.y + local xvel, yvel = vel.x, vel.y + xform.x, xform.y = x + xvel * dt, y + yvel * dt + end, + Aspect({"xform", "vel"}) + ) + + local timePassed = 0 + local oneTimeSystem = System( + function(dt) + timePassed = timePassed + dt + end + ) + + before_each(function() + entity1 = deep_copy(entityTemplate1) + entity2 = deep_copy(entityTemplate2) + entity3 = deep_copy(entityTemplate3) + world = World(moveSystem, oneTimeSystem, entity1, entity2, entity3) + timePassed = 0 + end) + + it("Create World", function() + assert.equals(world.entityCount, 3) + assert.equals(world.systemCount, 2) + end) + + it("Run simple simulation", function() + world:update(1) + assert.equals(timePassed, 1) + assert.equals(entity1.xform.x, 1) + assert.equals(entity1.xform.y, 2) + end) + + it("Remove Entities", function() + world:remove(entity1, entity2) + world:update(1) + assert.equals(timePassed, 1) + assert.equals(entity1.xform.x, entityTemplate1.xform.x) + assert.equals(entity2.xform.x, entityTemplate2.xform.x) + assert.equals(entity3.xform.y, 8) + end) + + it("Remove Systems", function() + world:remove(oneTimeSystem, moveSystem) + world:update(1) + assert.equals(timePassed, 0) + assert.equals(entity1.xform.x, entityTemplate1.xform.x) + end) + end) + +end)