Initial Commit.

This commit is contained in:
bakpakin 2015-03-21 16:09:39 +08:00
commit f3b2e9a0e5
5 changed files with 503 additions and 0 deletions

25
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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.")