local jojo = { _VERSION = "0.0.2", _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 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 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 "JojoAspect<>" else return "JojoAspect" end end ----- System ----- System.nextID = 0 -- 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 local id = System.nextID self.id = id System.nextID = id + 1 end function System:__tostring() return "JojoSystem" 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 = {...} -- Table of Entity IDs to status self.status = {} -- Table of Entity IDs to Entities self.entities = {} -- Number of Entities in World. self.entityCount = 0 -- List of Systems self.systems = args -- Table of System IDs to System Indices local systemIndices = {} self.systemIndices = systemIndices -- Next available entity ID self.nextID = 0 -- Table of System IDs to Sets of matching Entity IDs local systemEntities = {} self.systemEntities = systemEntities for i, sys in ipairs(args) do local id = sys.id systemEntities[id] = {} systemIndices[id] = i end -- List of Systems to add next update self.systemsToAdd = {} -- List of System ids to remove next update self.systemsToRemove = {} end function World:__tostring() return "JojoWorld" end -- World:addSystems(...) -- Appends Systems in order to the World. Systems will be added after the next -- call to World:update(dt) function World:addSystems(...) local args = {...} local systemsToAdd = self.systemsToAdd for i, sys in ipairs(args) do tinsert(systemsToAdd, sys) end end -- World:freeSystems(...) -- Appends Systems in order to the World. Systems will be added after the next -- call to World:update(dt) function World:removeSystems(...) local args = {...} local systemsToRemove = self.systemsToRemove for i, sys in ipairs(args) do tinsert(systemsToRemove, sys) end end -- World:add(...) -- Adds Entities to the World. Entities will enter the World the next time -- World:update(dt) is called. function World:add(...) local args = {...} 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. Entities will be updated in the world the next -- time World:update(dt) is called. function World:change(...) local args = {...} local status = self.status for _, e in ipairs(args) do status[e.id] = "add" end end -- World:free(...) -- Removes Entities from the World. Entities will exit the World the next time -- World:update(dt) is called. function World:remove(...) local args = {...} local status = self.status for _, e in ipairs(args) do status[e.id] = "remove" end end -- World:update() -- Updates the World, frees Entities that have been marked for freeing, adds -- entities that have been marked for adding, etc. function World:update(dt) local statuses = self.status local systemEntities = self.systemEntities local systemIndices = self.systemIndices local entities = self.entities local systems = self.systems local systemsToAdd = self.systemsToAdd local systemsToRemove = self.systemsToRemove -- Remove all Systems queued for removal for i = #systemsToRemove, 1, -1 do -- Pop system off the remove stack local sys = systemsToRemove[i] systemsToRemove[i] = nil local id = sys.id local sysIndex = systemIndices[id] tremove(systems, sysIndex) systemEntities[id] = nil end -- Add Systems queued for addition for i = 1, #systemsToAdd do -- Pop system off the add queue local sys = systemsToAdd[i] systemsToAdd[i] = nil -- Add system to world local id = sys.id local eids = {} systemEntities[id] = eids tinsert(systems, sys) systemIndices[id] = #systems local a = sys.aspect for eid, e in pairs(entities) do eids[eid] = a:matches(e) and true or nil end end -- Kepp track of number of Entities in World local deltaEntityCount = 0 -- Add, remove, or change Entities for eid, s in pairs(statuses) do local e = entities[eid] if s == "add" then deltaEntityCount = deltaEntityCount + 1 for sysID, eids in pairs(systemEntities) do local a = systems[systemIndices[sysID]].aspect eids[eid] = a:matches(e) and true or nil end elseif s == "remove" then deltaEntityCount = deltaEntityCount - 1 entities[eid] = nil for _, eids in pairs(systemEntities) do eids[eid] = nil end end statuses[eid] = nil end -- Update Entity count self.entityCount = self.entityCount + deltaEntityCount -- Iterate through Systems IN ORDER for _, s in ipairs(self.systems) do if s.active then local preupdate = s.preupdate local update = s.update if preupdate then preupdate(dt) end if update then local eids = systemEntities[s.id] for eid in pairs(eids) do update(entities[eid], dt) end end end end end return jojo