From 8d4b9943297ea0361ebbb110681db88273d0dfdb Mon Sep 17 00:00:00 2001 From: bakpakin Date: Sun, 29 Mar 2015 17:07:39 +0800 Subject: [PATCH] Change API. Remove Aspects for Filters. --- README.md | 34 +++--- spec/tiny_spec.lua | 50 +++++---- tiny.lua | 257 +++++++++++++++------------------------------ 3 files changed, 125 insertions(+), 216 deletions(-) diff --git a/README.md b/README.md index 815c152..8a66476 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ Component Systems, here is some Copy paste tiny.lua into your source folder. ## Overview ## -Tiny-ecs has four important types: Worlds, Aspects, Systems, and Entities. -Entities, however, can be any lua table. +Tiny-ecs has four important types: Worlds, Filters, Systems, and Entities. +Entities, however, can be any lua table, and Filters are just functions that +take an Entity as a parameter. ### 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 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 -parts: a one-time update function, a per Entity update function, and an Aspect. -The one-time update function is called once per World update, and the per Entity -update function is called once per Entity per World update. The Aspect is used -to select which Entities the System will update. +using a Filter, and then only update those select Entities. Some Systems don't +update Entities, and instead just act as function callbacks every update. Tiny-ecs +provides functions for creating Systems easily. -### Aspects ### -Aspects are used to select Entities by the presence or absence of specific -Components. If an Entity contains all Components required by an Aspect, and -doesn't contain Components that are excluded by the Aspect, it is said to match -the Aspect. Aspects can also be composed into more complicated Aspects that -are equivalent to the union of all sub-Aspects. +### Filters ### +Filters are used to select Entities. Filters can be any lua function, but +tiny-ecs provides some functions for generating common ones, like selecting +only Entities that have all required components. ## Example ## ```lua local tiny = require("tiny") -local personAspect = tiny.Aspect({"name", "mass", "phrase"}) - -local talkingSystem = tiny.System( - nil, +local talkingSystem = tiny.processingSystem( + tiny.requireAll("name", "mass", "phrase"), function (p, delta) p.mass = p.mass + delta * 3 print(p.name .. ", who weighs " .. p.mass .. " pounds, says, \"" .. p.phrase .. "\"") - end, - personAspect + end ) local joe = { @@ -59,7 +53,7 @@ local joe = { hairColor = "brown" } -local world = tiny.World(talkingSystem, joe) +local world = tiny.newWorld(talkingSystem, joe) for i = 1, 20 do world:update(1) diff --git a/spec/tiny_spec.lua b/spec/tiny_spec.lua index e280029..135461a 100644 --- a/spec/tiny_spec.lua +++ b/spec/tiny_spec.lua @@ -1,9 +1,5 @@ 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 local function deep_copy(o, seen) seen = seen or {} @@ -39,7 +35,8 @@ local entityTemplate2 = { vel = {x = -1, y = 0}, name = "E2", size = 10, - description = "It does not go to 11." + description = "It does not go to 11.", + onlyTen = true } local entityTemplate3 = { @@ -47,12 +44,13 @@ local entityTemplate3 = { vel = {x = 0, y = 3}, name = "E3", size = 8, - description = "The smallest entity." + description = "The smallest entity.", + littleMan = true } describe('tiny-ecs:', function() - describe('Aspect:', function() + describe('Filters:', function() local entity1, entity2, entity3 @@ -62,21 +60,22 @@ describe('tiny-ecs:', function() entity3 = deep_copy(entityTemplate3) end) - it("Correctly match Aspects and Entities", function() + it("Default Filters", 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) + local ftap = tiny.requireAll("spinalTap") + local fvel = tiny.requireAll("vel") + local fxform = tiny.requireAll("xform") + local fall = tiny.requireOne("spinalTap", "onlyTen", "littleMan") - 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)) + assert.truthy(fall(entity1)) + assert.truthy(ftap(entity1)) + assert.falsy(ftap(entity2)) + assert.truthy(fxform(entity1)) + assert.truthy(fxform(entity2)) + + assert.truthy(fall(entity1)) + assert.truthy(fall(entity2)) + assert.truthy(fall(entity3)) end) @@ -86,20 +85,19 @@ describe('tiny-ecs:', function() local world, entity1, entity2, entity3 - local moveSystem = System( - nil, + local moveSystem = tiny.processingSystem( + tiny.requireAll("xform", "vel"), 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"}) + end ) local timePassed = 0 - local oneTimeSystem = System( + local oneTimeSystem = tiny.emptySystem( function(dt) timePassed = timePassed + dt end @@ -109,7 +107,7 @@ describe('tiny-ecs:', function() entity1 = deep_copy(entityTemplate1) entity2 = deep_copy(entityTemplate2) entity3 = deep_copy(entityTemplate3) - world = World(moveSystem, oneTimeSystem, entity1, entity2, entity3) + world = tiny.newWorld(moveSystem, oneTimeSystem, entity1, entity2, entity3) timePassed = 0 end) diff --git a/tiny.lua b/tiny.lua index 99e1b4f..4b2efc3 100644 --- a/tiny.lua +++ b/tiny.lua @@ -12,13 +12,12 @@ local ipairs = ipairs local setmetatable = setmetatable local getmetatable = getmetatable --- Simplified class implementation with no inheritance or polymorphism. -local function class(name) +-- Simple class implementation with no inheritance or polymorphism. +local function class() local c = {} local mt = {} setmetatable(c, mt) c.__index = c - c.name = name function mt.__call(_, ...) local newobj = {} setmetatable(newobj, c) @@ -30,159 +29,14 @@ local function class(name) return c end -local World = class("World") -local Aspect = class("Aspect") -local System = class("System") +-- --- System --- -- +local System = class() -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" - end -end - ------ System ----- - --- System(preupdate, update, [aspect, addCallback, removeCallback]) - --- 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) +-- Initializes a System. +function System:init(preupdate, filter, update, add, remove) self.preupdate = preupdate self.update = update - self.aspect = aspect or Aspect() + self.filter = filter self.add = add self.remove = remove end @@ -192,16 +46,15 @@ function System:__tostring() self.preupdate .. ", update: " .. self.update .. - ", aspect: " .. - self.aspect .. + ", filter: " .. + self.filter .. ">" end ------ World ----- +-- --- World --- -- +local World = class() --- World(...) - --- Creates a new World with the given Systems and Entities in order. +-- Initializes a World. function World:init(...) -- Table of Entities to status @@ -252,7 +105,7 @@ end -- 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. +-- has had its Components changed, such that it matches different Filters. function World:add(...) local args = {...} local status = self.status @@ -275,11 +128,12 @@ end function World:remove(...) local args = {...} local status = self.status + local entities = self.entities local systemsToRemove = self.systemsToRemove for _, obj in ipairs(args) do if getmetatable(obj) == System then tinsert(systemsToRemove, obj) - else -- Assume obj is an Entity + elseif entities[obj] then -- Assume obj is an Entity status[obj] = "remove" end end @@ -360,9 +214,11 @@ function World:manageSystems() 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 + local a = sys.filter + if a then + for e in pairs(entities) do + es[e] = a(e) and true or nil + end end deltaSystemCount = deltaSystemCount + 1 @@ -391,19 +247,22 @@ function World:manageEntities() if s == "add" then deltaEntityCount = deltaEntityCount + 1 for sys, es in pairs(systemEntities) do - local matches = sys.aspect:matches(e) and true or nil - local addCallback = sys.add - if addCallback and matches and not es[e] then - addCallback(e) + local filter = sys.filter + if filter then + local matches = filter(e) and true or nil + local addCallback = sys.add + if addCallback and matches and not es[e] then + addCallback(e) + end + es[e] = matches end - es[e] = matches end elseif s == "remove" then deltaEntityCount = deltaEntityCount - 1 entities[e] = nil for sys, es in pairs(systemEntities) do local removec = sys.remove - if removec then + if es[e] and removec then removec(e) end es[e] = nil @@ -467,4 +326,62 @@ function World:setSystemActive(system, active) self.activeSystem[system] = active and true or nil 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