mirror of
https://github.com/bakpakin/tiny-ecs.git
synced 2024-10-12 19:44:17 +00:00
291 lines
7.3 KiB
Lua
291 lines
7.3 KiB
Lua
|
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
|