Change API.

Remove Aspects for Filters.
This commit is contained in:
bakpakin 2015-03-29 17:07:39 +08:00
parent eb1ee4c210
commit 8d4b994329
3 changed files with 125 additions and 216 deletions

View File

@ -9,8 +9,9 @@ Component Systems, here is some
Copy paste tiny.lua into your source folder. Copy paste tiny.lua into your source folder.
## Overview ## ## Overview ##
Tiny-ecs has four important types: Worlds, Aspects, Systems, and Entities. Tiny-ecs has four important types: Worlds, Filters, Systems, and Entities.
Entities, however, can be any lua table. Entities, however, can be any lua table, and Filters are just functions that
take an Entity as a parameter.
### Entities ### ### Entities ###
Entities are simply lua tables of data that gets processed by Systems. Entities Entities are simply lua tables of data that gets processed by Systems. Entities
@ -24,32 +25,25 @@ and Entities. In typical use, only one World is used at a time.
### Systems ### ### Systems ###
Systems in tiny-ecs describe how to update Entities. Systems select certain Entities Systems in tiny-ecs describe how to update Entities. Systems select certain Entities
using an aspect, and then only update those select Entities. Systems have three using a Filter, and then only update those select Entities. Some Systems don't
parts: a one-time update function, a per Entity update function, and an Aspect. update Entities, and instead just act as function callbacks every update. Tiny-ecs
The one-time update function is called once per World update, and the per Entity provides functions for creating Systems easily.
update function is called once per Entity per World update. The Aspect is used
to select which Entities the System will update.
### Aspects ### ### Filters ###
Aspects are used to select Entities by the presence or absence of specific Filters are used to select Entities. Filters can be any lua function, but
Components. If an Entity contains all Components required by an Aspect, and tiny-ecs provides some functions for generating common ones, like selecting
doesn't contain Components that are excluded by the Aspect, it is said to match only Entities that have all required components.
the Aspect. Aspects can also be composed into more complicated Aspects that
are equivalent to the union of all sub-Aspects.
## Example ## ## Example ##
```lua ```lua
local tiny = require("tiny") local tiny = require("tiny")
local personAspect = tiny.Aspect({"name", "mass", "phrase"}) local talkingSystem = tiny.processingSystem(
tiny.requireAll("name", "mass", "phrase"),
local talkingSystem = tiny.System(
nil,
function (p, delta) function (p, delta)
p.mass = p.mass + delta * 3 p.mass = p.mass + delta * 3
print(p.name .. ", who weighs " .. p.mass .. " pounds, says, \"" .. p.phrase .. "\"") print(p.name .. ", who weighs " .. p.mass .. " pounds, says, \"" .. p.phrase .. "\"")
end, end
personAspect
) )
local joe = { local joe = {
@ -59,7 +53,7 @@ local joe = {
hairColor = "brown" hairColor = "brown"
} }
local world = tiny.World(talkingSystem, joe) local world = tiny.newWorld(talkingSystem, joe)
for i = 1, 20 do for i = 1, 20 do
world:update(1) world:update(1)

View File

@ -1,9 +1,5 @@
local tiny = require "tiny" local tiny = require "tiny"
local World = tiny.World
local Aspect = tiny.Aspect
local System = tiny.System
-- Taken from answer at http://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value -- Taken from answer at http://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value
local function deep_copy(o, seen) local function deep_copy(o, seen)
seen = seen or {} seen = seen or {}
@ -39,7 +35,8 @@ local entityTemplate2 = {
vel = {x = -1, y = 0}, vel = {x = -1, y = 0},
name = "E2", name = "E2",
size = 10, size = 10,
description = "It does not go to 11." description = "It does not go to 11.",
onlyTen = true
} }
local entityTemplate3 = { local entityTemplate3 = {
@ -47,12 +44,13 @@ local entityTemplate3 = {
vel = {x = 0, y = 3}, vel = {x = 0, y = 3},
name = "E3", name = "E3",
size = 8, size = 8,
description = "The smallest entity." description = "The smallest entity.",
littleMan = true
} }
describe('tiny-ecs:', function() describe('tiny-ecs:', function()
describe('Aspect:', function() describe('Filters:', function()
local entity1, entity2, entity3 local entity1, entity2, entity3
@ -62,21 +60,22 @@ describe('tiny-ecs:', function()
entity3 = deep_copy(entityTemplate3) entity3 = deep_copy(entityTemplate3)
end) end)
it("Correctly match Aspects and Entities", function() it("Default Filters", function()
local atap = Aspect({"spinalTap"}) local ftap = tiny.requireAll("spinalTap")
local avel = Aspect({"vel"}) local fvel = tiny.requireAll("vel")
local axform = Aspect({"xform"}) local fxform = tiny.requireAll("xform")
local aall = Aspect({}) local fall = tiny.requireOne("spinalTap", "onlyTen", "littleMan")
local aboth = Aspect.compose(atap, axform)
local amove = Aspect.compose(axform, avel)
assert.truthy(atap:matches(entity1)) assert.truthy(fall(entity1))
assert.falsy(atap:matches(entity2)) assert.truthy(ftap(entity1))
assert.truthy(axform:matches(entity1)) assert.falsy(ftap(entity2))
assert.truthy(axform:matches(entity2)) assert.truthy(fxform(entity1))
assert.truthy(aboth:matches(entity1)) assert.truthy(fxform(entity2))
assert.falsy(aboth:matches(entity2))
assert.truthy(fall(entity1))
assert.truthy(fall(entity2))
assert.truthy(fall(entity3))
end) end)
@ -86,20 +85,19 @@ describe('tiny-ecs:', function()
local world, entity1, entity2, entity3 local world, entity1, entity2, entity3
local moveSystem = System( local moveSystem = tiny.processingSystem(
nil, tiny.requireAll("xform", "vel"),
function(e, dt) function(e, dt)
local xform = e.xform local xform = e.xform
local vel = e.vel local vel = e.vel
local x, y = xform.x, xform.y local x, y = xform.x, xform.y
local xvel, yvel = vel.x, vel.y local xvel, yvel = vel.x, vel.y
xform.x, xform.y = x + xvel * dt, y + yvel * dt xform.x, xform.y = x + xvel * dt, y + yvel * dt
end, end
Aspect({"xform", "vel"})
) )
local timePassed = 0 local timePassed = 0
local oneTimeSystem = System( local oneTimeSystem = tiny.emptySystem(
function(dt) function(dt)
timePassed = timePassed + dt timePassed = timePassed + dt
end end
@ -109,7 +107,7 @@ describe('tiny-ecs:', function()
entity1 = deep_copy(entityTemplate1) entity1 = deep_copy(entityTemplate1)
entity2 = deep_copy(entityTemplate2) entity2 = deep_copy(entityTemplate2)
entity3 = deep_copy(entityTemplate3) entity3 = deep_copy(entityTemplate3)
world = World(moveSystem, oneTimeSystem, entity1, entity2, entity3) world = tiny.newWorld(moveSystem, oneTimeSystem, entity1, entity2, entity3)
timePassed = 0 timePassed = 0
end) end)

243
tiny.lua
View File

@ -12,13 +12,12 @@ local ipairs = ipairs
local setmetatable = setmetatable local setmetatable = setmetatable
local getmetatable = getmetatable local getmetatable = getmetatable
-- Simplified class implementation with no inheritance or polymorphism. -- Simple class implementation with no inheritance or polymorphism.
local function class(name) local function class()
local c = {} local c = {}
local mt = {} local mt = {}
setmetatable(c, mt) setmetatable(c, mt)
c.__index = c c.__index = c
c.name = name
function mt.__call(_, ...) function mt.__call(_, ...)
local newobj = {} local newobj = {}
setmetatable(newobj, c) setmetatable(newobj, c)
@ -30,159 +29,14 @@ local function class(name)
return c return c
end end
local World = class("World")
local Aspect = class("Aspect")
local System = class("System")
tiny.World = World
tiny.Aspect = Aspect
tiny.System = System
----- Aspect -----
-- Aspect(required, excluded, oneRequired)
-- Creates an Aspect. Aspects are used to select which Entities are inlcuded
-- in each system. An Aspect has three fields, namely all required Components,
-- all excluded Components, and Components of which a system requires only one.
-- If an Entitiy has all required Components, none of the excluded Components,
-- and at least one of the oneRequired Components, it matches the Aspects.
-- This method expects at least one and up to three lists of strings, the names
-- of components. If no arguments are supplied, the Aspect will not match any
-- Entities. Will not mutate supplied arguments.
function Aspect:init(required, excluded, oneRequired)
local r, e, o = {}, {}, {}
self[1], self[2], self[3] = r, e, o
-- Check for empty Aspect
if not required and not oneRequired then
self[4] = true
return
end
self[4] = false
local excludeSet, requiredSet = {}, {}
-- Iterate through excluded Components
for _, v in ipairs(excluded or {}) do
tinsert(e, v)
excludeSet[v] = true
end
-- Iterate through required Components
for _, v in ipairs(required or {}) do
if excludeSet[v] then -- If Comp. is required and excluded, empty Aspect
self[1], self[2], self[3], self[4] = {}, {}, {}, true
return
else
tinsert(r, v)
requiredSet[v] = true
end
end
-- Iterate through one-required Components
for _, v in ipairs(oneRequired or {}) do
if requiredSet[v] then -- If one-required Comp. is also required,
-- don't need one required Components
self[3] = {}
return
end
if not excludeSet[v] then
tinsert(o, v)
end
end
end
-- Aspect.compose(...)
-- Composes multiple Aspects into one Aspect. The resulting Aspect will match
-- any Entity that matches all sub Aspects.
function Aspect.compose(...)
local newa = {{}, {}, {}}
for _, a in ipairs{...} do
if a[4] then -- Aspect must be empty Aspect
return Aspect()
end
for i = 1, 3 do
for _, c in ipairs(a[i]) do
tinsert(newa[i], c)
end
end
end
return Aspect(newa[1], newa[2], newa[3])
end
-- Aspect:matches(entity)
-- Returns boolean indicating if an Entity matches the Aspect.
function Aspect:matches(entity)
-- Aspect is the empty Aspect
if self[4] then return false end
local rs, es, os = self[1], self[2], self[3]
-- Assert Entity has all required Components
for i = 1, #rs do
local r = rs[i]
if entity[r] == nil then return false end
end
-- Assert Entity has no excluded Components
for i = 1, #es do
local e = es[i]
if entity[e] ~= nil then return false end
end
-- if Aspect has at least one Component in the one-required
-- field, assert that the Entity has at least one of these.
if #os >= 1 then
for i = 1, #os do
local o = os[i]
if entity[o] ~= nil then return true end
end
return false
end
return true
end
function Aspect:__tostring()
if self[4] then
return "TinyAspect<>"
else
return "TinyAspect<Required: {" ..
tconcat(self[1], ", ") ..
"}, Excluded: {" ..
tconcat(self[2], ", ") ..
"}, One Req.: {" ..
tconcat(self[3], ", ") ..
"}>"
end
end
-- --- System --- -- -- --- System --- --
local System = class()
-- System(preupdate, update, [aspect, addCallback, removeCallback]) -- Initializes a System.
function System:init(preupdate, filter, update, add, remove)
-- Creates a new System with the given aspect and update callback. The update
-- callback should be a function of one parameter, an entity. If no aspect is
-- provided the empty Aspect, which matches no Entity, is used. Preupdate is a
-- function of no arguments that is called once per system update before the
-- entities are updated. The add and remove callbacks are optional functions
-- that are called when entities are added or removed from the system. They
-- should each take one argument - an Entity.
function System:init(preupdate, update, aspect, add, remove)
self.preupdate = preupdate self.preupdate = preupdate
self.update = update self.update = update
self.aspect = aspect or Aspect() self.filter = filter
self.add = add self.add = add
self.remove = remove self.remove = remove
end end
@ -192,16 +46,15 @@ function System:__tostring()
self.preupdate .. self.preupdate ..
", update: " .. ", update: " ..
self.update .. self.update ..
", aspect: " .. ", filter: " ..
self.aspect .. self.filter ..
">" ">"
end end
-- --- World --- -- -- --- World --- --
local World = class()
-- World(...) -- Initializes a World.
-- Creates a new World with the given Systems and Entities in order.
function World:init(...) function World:init(...)
-- Table of Entities to status -- Table of Entities to status
@ -252,7 +105,7 @@ end
-- Adds Entities and Systems to the World. New objects will enter the World the -- 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 -- 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. -- has had its Components changed, such that it matches different Filters.
function World:add(...) function World:add(...)
local args = {...} local args = {...}
local status = self.status local status = self.status
@ -275,11 +128,12 @@ end
function World:remove(...) function World:remove(...)
local args = {...} local args = {...}
local status = self.status local status = self.status
local entities = self.entities
local systemsToRemove = self.systemsToRemove local systemsToRemove = self.systemsToRemove
for _, obj in ipairs(args) do for _, obj in ipairs(args) do
if getmetatable(obj) == System then if getmetatable(obj) == System then
tinsert(systemsToRemove, obj) tinsert(systemsToRemove, obj)
else -- Assume obj is an Entity elseif entities[obj] then -- Assume obj is an Entity
status[obj] = "remove" status[obj] = "remove"
end end
end end
@ -360,9 +214,11 @@ function World:manageSystems()
systemIndices[sys] = #systems systemIndices[sys] = #systems
activeSystems[sys] = true activeSystems[sys] = true
local a = sys.aspect local a = sys.filter
if a then
for e in pairs(entities) do for e in pairs(entities) do
es[e] = a:matches(e) and true or nil es[e] = a(e) and true or nil
end
end end
deltaSystemCount = deltaSystemCount + 1 deltaSystemCount = deltaSystemCount + 1
@ -391,19 +247,22 @@ function World:manageEntities()
if s == "add" then if s == "add" then
deltaEntityCount = deltaEntityCount + 1 deltaEntityCount = deltaEntityCount + 1
for sys, es in pairs(systemEntities) do for sys, es in pairs(systemEntities) do
local matches = sys.aspect:matches(e) and true or nil local filter = sys.filter
if filter then
local matches = filter(e) and true or nil
local addCallback = sys.add local addCallback = sys.add
if addCallback and matches and not es[e] then if addCallback and matches and not es[e] then
addCallback(e) addCallback(e)
end end
es[e] = matches es[e] = matches
end end
end
elseif s == "remove" then elseif s == "remove" then
deltaEntityCount = deltaEntityCount - 1 deltaEntityCount = deltaEntityCount - 1
entities[e] = nil entities[e] = nil
for sys, es in pairs(systemEntities) do for sys, es in pairs(systemEntities) do
local removec = sys.remove local removec = sys.remove
if removec then if es[e] and removec then
removec(e) removec(e)
end end
es[e] = nil es[e] = nil
@ -467,4 +326,62 @@ function World:setSystemActive(system, active)
self.activeSystem[system] = active and true or nil self.activeSystem[system] = active and true or nil
end end
-- --- Top Level module functions --- --
--- Creates a new tiny-ecs World.
function tiny.newWorld(...)
return World(...)
end
--- Makes a Filter that filters Entities with specified Components.
-- An Entity must have all Components to match the filter.
function tiny.requireAll(...)
local components = {...}
local len = #components
return function(e)
local c
for i = 1, len do
c = components[i]
if e[c] == nil then
return false
end
end
return true
end
end
--- Makes a Filter that filters Entities with specified Components.
-- An Entity must have at least one specified Component to match the filter.
function tiny.requireOne(...)
local components = {...}
local len = #components
return function(e)
local c
for i = 1, len do
c = components[i]
if e[c] ~= nil then
return true
end
end
return false
end
end
--- Creates a System that doesn't update any Entities, but executes a callback
-- once per update.
function tiny.emptySystem(callback)
return System(callback)
end
--- Creates a System that processes Entities every update. Also provides
-- optional callbacks for when Entities are added or removed from the System.
function tiny.processingSystem(filter, entityCallback, onAdd, onRemove)
return System(nil, filter, entityCallback, onAdd, onRemove)
end
--- Creates a System.
function tiny.system(callback, filter, entityCallback, onAdd, onRemove)
return System(callback, filter, entityCallback, onAdd, onRemove)
end
return tiny return tiny