mirror of
https://github.com/bakpakin/tiny-ecs.git
synced 2024-11-17 04:44:23 +00:00
Change API.
Remove Aspects for Filters.
This commit is contained in:
parent
eb1ee4c210
commit
8d4b994329
34
README.md
34
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)
|
||||
|
@ -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)
|
||||
|
||||
|
257
tiny.lua
257
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<Required: {" ..
|
||||
tconcat(self[1], ", ") ..
|
||||
"}, Excluded: {" ..
|
||||
tconcat(self[2], ", ") ..
|
||||
"}, One Req.: {" ..
|
||||
tconcat(self[3], ", ") ..
|
||||
"}>"
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user