mirror of
https://github.com/bakpakin/tiny-ecs.git
synced 2024-11-28 23:54:21 +00:00
Initial Commit.
This commit is contained in:
commit
f3b2e9a0e5
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#### OSX .gitignore
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
7
LICENSE.txt
Normal file
7
LICENSE.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Copyright (c) 2015 Calvin Rose
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Jojo #
|
||||||
|
Jojo is an Entity Component System for lua that's simple, flexible, and useful.
|
||||||
|
Because of lua's tabular nature, Entity Component Systems are a natural choice
|
||||||
|
for simulating large and complex systems. For more explanation on Entity
|
||||||
|
Component Systems, here is some
|
||||||
|
[basic info](http://en.wikipedia.org/wiki/Entity_component_system "Wikipedia").
|
||||||
|
|
||||||
|
## Use It ##
|
||||||
|
Copy paste jojo.lua into your source folder.
|
||||||
|
|
||||||
|
## Overview ##
|
||||||
|
Jojo has four important types: Worlds, Aspects, Systems, and Entities.
|
||||||
|
Entities, however, can be any lua table.
|
||||||
|
|
||||||
|
### Entities ###
|
||||||
|
Entities are simply lua tables of data that gets processed by Systems. Entities
|
||||||
|
should contain primarily data rather that code, as it is the Systems's job to
|
||||||
|
do logic on data. Henceforth, a key-value pair in an Entity will
|
||||||
|
be referred to as a Component.
|
||||||
|
|
||||||
|
### Worlds ###
|
||||||
|
Worlds are the outermost containers in Jojo that contain both Systems
|
||||||
|
and Entities. In typical use, only one World is used at a time.
|
||||||
|
|
||||||
|
### Systems ###
|
||||||
|
Systems in Jojo 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## Example ##
|
||||||
|
```lua
|
||||||
|
local jojo = require("jojo")
|
||||||
|
|
||||||
|
local World = jojo.World
|
||||||
|
local Aspect = jojo.Aspect
|
||||||
|
local System = jojo.System
|
||||||
|
|
||||||
|
local personAspect = Aspect({"name", "mass", "phrase"})
|
||||||
|
local talkingSystem = System(
|
||||||
|
function (delta)
|
||||||
|
print("Talking system has started.")
|
||||||
|
end,
|
||||||
|
function (p, delta)
|
||||||
|
print(p.name .. ", who weighs " .. p.mass .. "pounds, says, " .. p.phrase)
|
||||||
|
end,
|
||||||
|
personAspect
|
||||||
|
)
|
||||||
|
|
||||||
|
local world = World(talkingSystem)
|
||||||
|
|
||||||
|
local joe = {
|
||||||
|
name = "Joe",
|
||||||
|
phrase = "I'm a plumber.",
|
||||||
|
mass = 150,
|
||||||
|
hairColor = "brown"
|
||||||
|
}
|
||||||
|
|
||||||
|
world:add(joe)
|
||||||
|
|
||||||
|
world:update(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing ##
|
||||||
|
To test Jojo, simply clone the project, cd into the project directory, and
|
||||||
|
run `lua jojotest.lua` from command line.
|
||||||
|
|
||||||
|
## TODO ##
|
||||||
|
|
||||||
|
* Dynamically add and remove systems
|
||||||
|
* More testing
|
||||||
|
* Performance testing / optimization
|
||||||
|
* API outside of source code
|
290
jojo.lua
Normal file
290
jojo.lua
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
local jojo = {
|
||||||
|
_VERSION = "0.0.1",
|
||||||
|
_URL = "https://github.com/bakpakin/Jojo"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Simplified class implementation with no inheritance or polymorphism.
|
||||||
|
local setmetatable = setmetatable
|
||||||
|
local function class()
|
||||||
|
local c = {}
|
||||||
|
local mt = {}
|
||||||
|
setmetatable(c, mt)
|
||||||
|
c.__index = c
|
||||||
|
function mt.__call(_, ...)
|
||||||
|
local newobj = {}
|
||||||
|
setmetatable(newobj, c)
|
||||||
|
if c.init then
|
||||||
|
c.init(newobj, ...)
|
||||||
|
end
|
||||||
|
return newobj
|
||||||
|
end
|
||||||
|
return c
|
||||||
|
end
|
||||||
|
|
||||||
|
local World = class()
|
||||||
|
local Aspect = class()
|
||||||
|
local System = class()
|
||||||
|
|
||||||
|
jojo.World = World
|
||||||
|
jojo.Aspect = Aspect
|
||||||
|
jojo.System = System
|
||||||
|
|
||||||
|
local tpack = table.pack
|
||||||
|
local tinsert = table.insert
|
||||||
|
local tremove = table.remove
|
||||||
|
local tconcat = table.concat
|
||||||
|
local pairs = pairs
|
||||||
|
local ipairs = ipairs
|
||||||
|
local print = print
|
||||||
|
|
||||||
|
----- 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 args = tpack(...)
|
||||||
|
local newa = {{}, {}, {}}
|
||||||
|
for j = 1, args.n do
|
||||||
|
local a = args[j]
|
||||||
|
if a[4] then
|
||||||
|
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)
|
||||||
|
if self[4] then return false end
|
||||||
|
local rs, es, os = self[1], self[2], self[3]
|
||||||
|
for i = 1, #rs do
|
||||||
|
local r = rs[i]
|
||||||
|
if entity[r] == nil then return false end
|
||||||
|
end
|
||||||
|
for i = 1, #es do
|
||||||
|
local e = es[i]
|
||||||
|
if entity[e] ~= nil then return false end
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Aspect:trace()
|
||||||
|
|
||||||
|
-- Prints out a description of this Aspect.
|
||||||
|
function Aspect:trace()
|
||||||
|
if self[4] then
|
||||||
|
print("Empty Aspect.")
|
||||||
|
else
|
||||||
|
print("Required Components:", tconcat(self[1], ", "))
|
||||||
|
print("Excluded Components:", tconcat(self[2], ", "))
|
||||||
|
print("One Req. Components:", tconcat(self[3], ", "))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
----- System -----
|
||||||
|
|
||||||
|
-- System(preupdate, update, [aspect])
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
function System:init(preupdate, update, aspect)
|
||||||
|
self.preupdate = preupdate
|
||||||
|
self.update = update
|
||||||
|
self.aspect = aspect or Aspect()
|
||||||
|
self.active = true
|
||||||
|
end
|
||||||
|
|
||||||
|
----- World -----
|
||||||
|
|
||||||
|
-- World(...)
|
||||||
|
|
||||||
|
-- Creates a new World with the given Systems in order. TODO - Add or remove
|
||||||
|
-- Systems after creation.
|
||||||
|
function World:init(...)
|
||||||
|
|
||||||
|
local args = tpack(...)
|
||||||
|
|
||||||
|
-- Table of Entity IDs to status
|
||||||
|
self.status = {}
|
||||||
|
|
||||||
|
-- Table of Entity IDs to Entities
|
||||||
|
self.entities = {}
|
||||||
|
|
||||||
|
-- List of Systems
|
||||||
|
self.systems = args
|
||||||
|
|
||||||
|
-- Accumulated time for each System.
|
||||||
|
self.times = {}
|
||||||
|
|
||||||
|
-- Next available entity ID
|
||||||
|
self.nextID = 0
|
||||||
|
|
||||||
|
-- Table of System indices to Sets of matching Entity IDs
|
||||||
|
local aspectEntities = {}
|
||||||
|
self.aspectEntities = aspectEntities
|
||||||
|
for i, sys in ipairs(args) do
|
||||||
|
aspectEntities[i] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- World:add(...)
|
||||||
|
|
||||||
|
-- Adds Entities to the World. An Entity is just a table of Components.
|
||||||
|
function World:add(...)
|
||||||
|
local args = tpack(...)
|
||||||
|
local status = self.status
|
||||||
|
local entities = self.entities
|
||||||
|
for _, e in ipairs(args) do
|
||||||
|
local id = self.nextID
|
||||||
|
self.nextID = id + 1
|
||||||
|
e._id = id
|
||||||
|
entities[id] = e
|
||||||
|
status[id] = "add"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- World:changed(...)
|
||||||
|
|
||||||
|
-- Call this function on any Entities that have changed such that they would
|
||||||
|
-- now match different systems.
|
||||||
|
function World:changed(...)
|
||||||
|
local args = tpack(...)
|
||||||
|
local status = self.status
|
||||||
|
for _, e in ipairs(args) do
|
||||||
|
status[e._id] = "add"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- World:free(...)
|
||||||
|
|
||||||
|
-- Frees Entities from the World.
|
||||||
|
function World:free(...)
|
||||||
|
local args = tpack(...)
|
||||||
|
local status = self.status
|
||||||
|
for _, e in ipairs(args) do
|
||||||
|
status[e._id] = "free"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- World:update()
|
||||||
|
|
||||||
|
-- Updates the World.
|
||||||
|
function World:update(dt)
|
||||||
|
|
||||||
|
local statuses = self.status
|
||||||
|
local aspectEntities = self.aspectEntities
|
||||||
|
local entities = self.entities
|
||||||
|
local systems = self.systems
|
||||||
|
|
||||||
|
for eid, s in pairs(statuses) do
|
||||||
|
if s == "add" then
|
||||||
|
local e = entities[eid]
|
||||||
|
for sysi, set in pairs(aspectEntities) do
|
||||||
|
local a = systems[sysi].aspect
|
||||||
|
set[eid] = a:matches(e) and true or nil
|
||||||
|
end
|
||||||
|
statuses[eid] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for sysi, s in ipairs(self.systems) do
|
||||||
|
local preupdate = s.preupdate
|
||||||
|
if s.active and preupdate then
|
||||||
|
preupdate(dt)
|
||||||
|
end
|
||||||
|
local eids = aspectEntities[sysi]
|
||||||
|
local u = s.update
|
||||||
|
for eid in pairs(eids) do
|
||||||
|
local status = statuses[eid]
|
||||||
|
if status == "free" then
|
||||||
|
eids[eid] = nil
|
||||||
|
end
|
||||||
|
u(entities[eid], dt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for eid, s in pairs(statuses) do
|
||||||
|
if s == "free" then
|
||||||
|
entities[eid].id = nil
|
||||||
|
entities[eid] = nil
|
||||||
|
end
|
||||||
|
statuses[eid] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
return jojo
|
99
jojotest.lua
Normal file
99
jojotest.lua
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
world:add(e1, e2, e3)
|
||||||
|
world:update(21)
|
||||||
|
assert(e1.xform.x == 21, "e1.xform.x should be 21, but is " .. e1.xform.x)
|
||||||
|
|
||||||
|
world:free(e3, e2)
|
||||||
|
world:update(20)
|
||||||
|
assert(e1.xform.x == 41, "e1.xform.x should be 41, but is " .. e1.xform.x)
|
||||||
|
|
||||||
|
world:add(e3, e2)
|
||||||
|
world:update(19)
|
||||||
|
world:free(e3, e2)
|
||||||
|
e1.vel = nil
|
||||||
|
world:changed(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.")
|
Loading…
Reference in New Issue
Block a user