mirror of
https://github.com/bakpakin/tiny-ecs.git
synced 2024-11-17 04:44:23 +00:00
Switch to 'busted' for testing
Update version, Unify add and remove methods for World Fix small bugs
This commit is contained in:
parent
5b4acb55ee
commit
26ecc4f0a5
@ -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 ##
|
||||
|
||||
|
230
jojo.lua
230
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<systemCount: " ..
|
||||
#self.systems ..
|
||||
self.systemCount ..
|
||||
", entityCount: " ..
|
||||
#self.entityCount ..
|
||||
self.entityCount ..
|
||||
">"
|
||||
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
|
||||
|
111
jojotest.lua
111
jojotest.lua
@ -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.")
|
145
spec/jojo_spec.lua
Normal file
145
spec/jojo_spec.lua
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user