mirror of
https://github.com/Keyslam-Group/Concord.git
synced 2025-09-02 20:33:54 -04:00
rename folder src to concord
This commit is contained in:
parent
451b88cdea
commit
f502f1b9f6
14 changed files with 0 additions and 0 deletions
37
concord/assemblage.lua
Normal file
37
concord/assemblage.lua
Normal file
|
@ -0,0 +1,37 @@
|
|||
--- Assemblage
|
||||
-- An Assemblage is a function that 'makes' an entity something.
|
||||
-- It does this by :give'ing or :ensure'ing Components, or by :assemble'ing the Entity.
|
||||
|
||||
local Assemblage = {}
|
||||
Assemblage.__mt = {
|
||||
__index = Assemblage,
|
||||
}
|
||||
|
||||
--- Creates a new Assemblage.
|
||||
-- @param assemble Function that assembles an Entity
|
||||
-- @return A new Assemblage
|
||||
function Assemblage.new(assemble)
|
||||
local assemblage = setmetatable({
|
||||
__assemble = assemble,
|
||||
|
||||
__isAssemblage = true,
|
||||
}, Assemblage.__mt)
|
||||
|
||||
return assemblage
|
||||
end
|
||||
|
||||
--- Assembles an Entity.
|
||||
-- @param e Entity to assemble
|
||||
-- @param ... Varargs to pass to the assemble function
|
||||
-- @ return self
|
||||
function Assemblage:assemble(e, ...)
|
||||
self.__assemble(e, ...)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return setmetatable(Assemblage, {
|
||||
__call = function(_, ...)
|
||||
return Assemblage.new(...)
|
||||
end,
|
||||
})
|
33
concord/assemblages.lua
Normal file
33
concord/assemblages.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--- Assemblages
|
||||
-- Container for registered Assemblages
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
|
||||
local Assemblages = {}
|
||||
|
||||
--- Registers an Assemblage.
|
||||
-- @param name Name to register under
|
||||
-- @param assemblage Assemblage to register
|
||||
function Assemblages.register(name, assemblage)
|
||||
if (type(name) ~= "string") then
|
||||
error("bad argument #1 to 'Assemblages.register' (string expected, got "..type(name)..")", 3)
|
||||
end
|
||||
|
||||
if (not Type.isAssemblage(assemblage)) then
|
||||
error("bad argument #2 to 'Assemblages.register' (assemblage expected, got "..type(assemblage)..")", 3)
|
||||
end
|
||||
|
||||
if (rawget(Assemblages, name)) then
|
||||
error("bad argument #2 to 'Assemblages.register' (Assemblage with name '"..name.."' was already registerd)", 3)
|
||||
end
|
||||
|
||||
Assemblages[name] = assemblage
|
||||
end
|
||||
|
||||
return setmetatable(Assemblages, {
|
||||
__index = function(_, name)
|
||||
error("Attempt to index assemblage '"..tostring(name).."' that does not exist / was not registered", 2)
|
||||
end
|
||||
})
|
55
concord/component.lua
Normal file
55
concord/component.lua
Normal file
|
@ -0,0 +1,55 @@
|
|||
--- Component
|
||||
-- A Component is a pure data container.
|
||||
-- A Component is contained by a single entity.
|
||||
|
||||
local Component = {}
|
||||
Component.__mt = {
|
||||
__index = Component,
|
||||
}
|
||||
|
||||
--- Creates a new ComponentClass.
|
||||
-- @param populate Function that populates a Component with values
|
||||
-- @return A new ComponentClass
|
||||
function Component.new(populate)
|
||||
if (type(populate) ~= "function" and type(populate) ~= "nil") then
|
||||
error("bad argument #1 to 'Component.new' (function/nil expected, got "..type(populate)..")", 2)
|
||||
end
|
||||
|
||||
local componentClass = setmetatable({
|
||||
__populate = populate,
|
||||
|
||||
__isComponentClass = true,
|
||||
}, Component.__mt)
|
||||
|
||||
componentClass.__mt = {
|
||||
__index = componentClass
|
||||
}
|
||||
|
||||
return componentClass
|
||||
end
|
||||
|
||||
--- Internal: Populates a Component with values
|
||||
function Component:__populate() -- luacheck: ignore
|
||||
end
|
||||
|
||||
--- Internal: Creates and populates a new Component.
|
||||
-- @param ... Varargs passed to the populate function
|
||||
-- @return A new populated Component
|
||||
function Component:__initialize(...)
|
||||
local component = setmetatable({
|
||||
__componentClass = self,
|
||||
|
||||
__isComponent = true,
|
||||
__isComponentClass = false,
|
||||
}, self)
|
||||
|
||||
self.__populate(component, ...)
|
||||
|
||||
return component
|
||||
end
|
||||
|
||||
return setmetatable(Component, {
|
||||
__call = function(_, ...)
|
||||
return Component.new(...)
|
||||
end,
|
||||
})
|
33
concord/components.lua
Normal file
33
concord/components.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--- Components
|
||||
-- Container for registered ComponentClasss
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
|
||||
local Components = {}
|
||||
|
||||
--- Registers a ComponentClass.
|
||||
-- @param name Name to register under
|
||||
-- @param componentClass ComponentClass to register
|
||||
function Components.register(name, componentClass)
|
||||
if (type(name) ~= "string") then
|
||||
error("bad argument #1 to 'Components.register' (string expected, got "..type(name)..")", 3)
|
||||
end
|
||||
|
||||
if (not Type.isComponentClass(componentClass)) then
|
||||
error("bad argument #2 to 'Components.register' (ComponentClass expected, got "..type(componentClass)..")", 3)
|
||||
end
|
||||
|
||||
if (rawget(Components, name)) then
|
||||
error("bad argument #2 to 'Components.register' (ComponentClass with name '"..name.."' was already registerd)", 3)
|
||||
end
|
||||
|
||||
Components[name] = componentClass
|
||||
end
|
||||
|
||||
return setmetatable(Components, {
|
||||
__index = function(_, name)
|
||||
error("Attempt to index ComponentClass '"..tostring(name).."' that does not exist / was not registered", 2)
|
||||
end
|
||||
})
|
180
concord/entity.lua
Normal file
180
concord/entity.lua
Normal file
|
@ -0,0 +1,180 @@
|
|||
--- Entity
|
||||
-- Entities are the concrete objects that exist in your project.
|
||||
-- An Entity have Components and are processed by Systems.
|
||||
-- An Entity is contained by a maximum of 1 World.
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
|
||||
local Entity = {}
|
||||
Entity.__mt = {
|
||||
__index = Entity,
|
||||
}
|
||||
|
||||
--- Creates a new Entity. Optionally adds it to a World.
|
||||
-- @param world Optional World to add the entity to
|
||||
-- @return A new Entity
|
||||
function Entity.new(world)
|
||||
if (world ~= nil and not Type.isWorld(world)) then
|
||||
error("bad argument #1 to 'Entity.new' (world/nil expected, got "..type(world)..")", 2)
|
||||
end
|
||||
|
||||
local e = setmetatable({
|
||||
__world = nil,
|
||||
__components = {},
|
||||
|
||||
__isEntity = true,
|
||||
}, Entity.__mt)
|
||||
|
||||
if (world) then
|
||||
world:addEntity(e)
|
||||
end
|
||||
|
||||
return e
|
||||
end
|
||||
|
||||
local function give(e, componentClass, ...)
|
||||
local component = componentClass:__initialize(...)
|
||||
|
||||
e[componentClass] = component
|
||||
e.__components[componentClass] = component
|
||||
|
||||
e:__dirty()
|
||||
end
|
||||
|
||||
local function remove(e, componentClass)
|
||||
e[componentClass] = nil
|
||||
e.__components[componentClass] = nil
|
||||
|
||||
e:__dirty()
|
||||
end
|
||||
|
||||
--- Gives an Entity a Component.
|
||||
-- If the Component already exists, it's overridden by this new Component
|
||||
-- @param componentClass ComponentClass to add an instance of
|
||||
-- @param ... varargs passed to the Component's populate function
|
||||
-- @return self
|
||||
function Entity:give(componentClass, ...)
|
||||
if not Type.isComponentClass(componentClass) then
|
||||
error("bad argument #1 to 'Entity:give' (ComponentClass expected, got "..type(componentClass)..")", 2)
|
||||
end
|
||||
|
||||
give(self, componentClass, ...)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Ensures an Entity to have a Component.
|
||||
-- If the Component already exists, no action is taken
|
||||
-- @param componentClass ComponentClass to add an instance of
|
||||
-- @param ... varargs passed to the Component's populate function
|
||||
-- @return self
|
||||
function Entity:ensure(componentClass, ...)
|
||||
if not Type.isComponentClass(componentClass) then
|
||||
error("bad argument #1 to 'Entity:ensure' (ComponentClass expected, got "..type(componentClass)..")", 2)
|
||||
end
|
||||
|
||||
if self[componentClass] then
|
||||
return self
|
||||
end
|
||||
|
||||
give(self, componentClass, ...)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes a Component from an Entity.
|
||||
-- @param componentClass ComponentClass of the Component to remove
|
||||
-- @return self
|
||||
function Entity:remove(componentClass)
|
||||
if not Type.isComponentClass(componentClass) then
|
||||
error("bad argument #1 to 'Entity:remove' (ComponentClass expected, got "..type(componentClass)..")")
|
||||
end
|
||||
|
||||
remove(self, componentClass)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Assembles an Entity.
|
||||
-- @param assemblage Assemblage to assemble with
|
||||
-- @param ... Varargs to pass to the Assemblage's assemble function.
|
||||
function Entity:assemble(assemblage, ...)
|
||||
if not Type.isAssemblage(assemblage) then
|
||||
error("bad argument #1 to 'Entity:assemble' (Assemblage expected, got "..type(assemblage)..")")
|
||||
end
|
||||
|
||||
assemblage:assemble(self, ...)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Destroys the Entity.
|
||||
-- Removes the Entity from it's World if it's in one.
|
||||
-- @return self
|
||||
function Entity:destroy()
|
||||
if self.__world then
|
||||
self.__world:removeEntity(self)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Internal: Tells the World it's in that this Entity is dirty.
|
||||
-- @return self
|
||||
function Entity:__dirty()
|
||||
if self.__world then
|
||||
self.__world:__dirtyEntity(self)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Returns true if the Entity has a Component.
|
||||
-- @param componentClass ComponentClass of the Component to check
|
||||
-- @return True if the Entity has the Component, false otherwise
|
||||
function Entity:has(componentClass)
|
||||
if not Type.isComponentClass(componentClass) then
|
||||
error("bad argument #1 to 'Entity:has' (ComponentClass expected, got "..type(componentClass)..")")
|
||||
end
|
||||
|
||||
return self[componentClass] ~= nil
|
||||
end
|
||||
|
||||
--- Gets a Component from the Entity.
|
||||
-- @param componentClass ComponentClass of the Component to get
|
||||
-- @return The Component
|
||||
function Entity:get(componentClass)
|
||||
if not Type.isComponentClass(componentClass) then
|
||||
error("bad argument #1 to 'Entity:get' (ComponentClass expected, got "..type(componentClass)..")")
|
||||
end
|
||||
|
||||
return self[componentClass]
|
||||
end
|
||||
|
||||
--- Returns a table of all Components the Entity has.
|
||||
-- Warning: Do not modify this table.
|
||||
-- Use Entity:give/ensure/remove instead
|
||||
-- @return Table of all Components the Entity has
|
||||
function Entity:getComponents()
|
||||
return self.__components
|
||||
end
|
||||
|
||||
--- Returns true if the Entity is in a World.
|
||||
-- @return True if the Entity is in a World, false otherwise
|
||||
function Entity:inWorld()
|
||||
return self.__world and true or false
|
||||
end
|
||||
|
||||
--- Returns the World the Entity is in.
|
||||
-- @return The World the Entity is in.
|
||||
function Entity:getWorld()
|
||||
return self.__world
|
||||
end
|
||||
|
||||
return setmetatable(Entity, {
|
||||
__call = function(_, ...)
|
||||
return Entity.new(...)
|
||||
end,
|
||||
})
|
113
concord/init.lua
Normal file
113
concord/init.lua
Normal file
|
@ -0,0 +1,113 @@
|
|||
--- init
|
||||
|
||||
local PATH = (...):gsub('%.init$', '')
|
||||
|
||||
local Concord = {
|
||||
_VERSION = "2.0 Beta",
|
||||
_DESCRIPTION = "A feature-complete ECS library",
|
||||
_LICENCE = [[
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2018 Justin van der Leij / Tjakka5
|
||||
|
||||
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.
|
||||
]]
|
||||
}
|
||||
|
||||
Concord.entity = require(PATH..".entity")
|
||||
|
||||
Concord.component = require(PATH..".component")
|
||||
Concord.components = require(PATH..".components")
|
||||
|
||||
Concord.system = require(PATH..".system")
|
||||
Concord.systems = require(PATH..".systems")
|
||||
|
||||
Concord.world = require(PATH..".world")
|
||||
Concord.worlds = require(PATH..".worlds")
|
||||
|
||||
Concord.assemblage = require(PATH..".assemblage")
|
||||
Concord.assemblages = require(PATH..".assemblages")
|
||||
|
||||
local function load(pathOrFiles, namespace)
|
||||
if (type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table") then
|
||||
error("bad argument #1 to 'load' (string/table of strings expected, got "..type(pathOrFiles)..")", 3)
|
||||
end
|
||||
|
||||
if (type(pathOrFiles) == "string") then
|
||||
local info = love.filesystem.getInfo(pathOrFiles) -- luacheck: ignore
|
||||
if (info == nil or info.type ~= "directory") then
|
||||
error("bad argument #1 to 'load' (path '"..pathOrFiles.."' not found)", 3)
|
||||
end
|
||||
|
||||
local files = love.filesystem.getDirectoryItems(pathOrFiles)
|
||||
|
||||
for _, file in ipairs(files) do
|
||||
local name = file:sub(1, #file - 4)
|
||||
local path = pathOrFiles.."."..name
|
||||
|
||||
namespace.register(name, require(path))
|
||||
end
|
||||
elseif (type(pathOrFiles == "table")) then
|
||||
for _, path in ipairs(pathOrFiles) do
|
||||
if (type(path) ~= "string") then
|
||||
error("bad argument #2 to 'load' (string/table of strings expected, got table containing "..type(path)..")", 3) -- luacheck: ignore
|
||||
end
|
||||
|
||||
local name = path
|
||||
|
||||
local dotIndex, slashIndex = path:match("^.*()%."), path:match("^.*()%/")
|
||||
if (dotIndex or slashIndex) then
|
||||
name = path:sub((dotIndex or slashIndex) + 1)
|
||||
end
|
||||
|
||||
namespace.register(name, require(path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Loads ComponentClasses and puts them in the Components container.
|
||||
-- Accepts a table of paths to files: {"component_1", "component_2", "etc"}
|
||||
-- Accepts a path to a directory with ComponentClasses: "components"
|
||||
function Concord.loadComponents(pathOrFiles)
|
||||
load(pathOrFiles, Concord.components)
|
||||
end
|
||||
|
||||
--- Loads SystemClasses and puts them in the Systems container.
|
||||
-- Accepts a table of paths to files: {"system_1", "system_2", "etc"}
|
||||
-- Accepts a path to a directory with SystemClasses: "systems"
|
||||
function Concord.loadSystems(pathOrFiles)
|
||||
load(pathOrFiles, Concord.systems)
|
||||
end
|
||||
|
||||
--- Loads Worlds and puts them in the Worlds container.
|
||||
-- Accepts a table of paths to files: {"world_1", "world_2", "etc"}
|
||||
-- Accepts a path to a directory with Worlds: "worlds"
|
||||
function Concord.loadWorlds(pathOrFiles)
|
||||
load(pathOrFiles, Concord.worlds)
|
||||
end
|
||||
|
||||
--- Loads Assemblages and puts them in the Assemblages container.
|
||||
-- Accepts a table of paths to files: {"assemblage_1", "assemblage_2", "etc"}
|
||||
-- Accepts a path to a directory with Assemblages: "assemblages"
|
||||
function Concord.loadAssemblages(pathOrFiles)
|
||||
load(pathOrFiles, Concord.assemblages)
|
||||
end
|
||||
|
||||
return Concord
|
101
concord/list.lua
Normal file
101
concord/list.lua
Normal file
|
@ -0,0 +1,101 @@
|
|||
--- List
|
||||
-- Data structure that allows for fast removal at the cost of containing order.
|
||||
|
||||
local List = {}
|
||||
List.__mt = {
|
||||
__index = List
|
||||
}
|
||||
|
||||
--- Creates a new List.
|
||||
-- @return A new List
|
||||
function List.new()
|
||||
return setmetatable({
|
||||
size = 0,
|
||||
}, List.__mt)
|
||||
end
|
||||
|
||||
--- Adds an object to the List.
|
||||
-- Object must be of reference type
|
||||
-- Object may not be the string 'size'
|
||||
-- @param obj Object to add
|
||||
-- @return self
|
||||
function List:__add(obj)
|
||||
local size = self.size + 1
|
||||
|
||||
self[size] = obj
|
||||
self[obj] = size
|
||||
self.size = size
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes an object from the List.
|
||||
-- @param obj Object to remove
|
||||
-- @return self
|
||||
function List:__remove(obj)
|
||||
local index = self[obj]
|
||||
if not index then return end
|
||||
local size = self.size
|
||||
|
||||
if index == size then
|
||||
self[size] = nil
|
||||
else
|
||||
local other = self[size]
|
||||
|
||||
self[index] = other
|
||||
self[other] = index
|
||||
|
||||
self[size] = nil
|
||||
end
|
||||
|
||||
self[obj] = nil
|
||||
self.size = size - 1
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Clears the List completely.
|
||||
-- @return self
|
||||
function List:__clear()
|
||||
for i = 1, self.size do
|
||||
local o = self[i]
|
||||
|
||||
self[o] = nil
|
||||
self[i] = nil
|
||||
end
|
||||
|
||||
self.size = 0
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Returns true if the List has the object.
|
||||
-- @param obj Object to check for
|
||||
-- @return True if the List has the object, false otherwise
|
||||
function List:has(obj)
|
||||
return self[obj] and true or false
|
||||
end
|
||||
|
||||
--- Returns the object at an index.
|
||||
-- @param i Index to get from
|
||||
-- @return Object at the index
|
||||
function List:get(i)
|
||||
return self[i]
|
||||
end
|
||||
|
||||
--- Returns the index of an object in the List.
|
||||
-- @param obj Object to get index of
|
||||
-- @return index of object in the List.
|
||||
function List:indexOf(obj)
|
||||
if (not self[obj]) then
|
||||
error("bad argument #1 to 'List:indexOf' (Object was not in List)", 2)
|
||||
end
|
||||
|
||||
return self[obj]
|
||||
end
|
||||
|
||||
return setmetatable(List, {
|
||||
__call = function()
|
||||
return List.new()
|
||||
end,
|
||||
})
|
90
concord/pool.lua
Normal file
90
concord/pool.lua
Normal file
|
@ -0,0 +1,90 @@
|
|||
--- Pool
|
||||
-- A Pool is used to iterate over Entities with a specific Components
|
||||
-- A Pool contain a any amount of Entities.
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local List = require(PATH..".list")
|
||||
|
||||
local Pool = {}
|
||||
Pool.__mt = {
|
||||
__index = Pool,
|
||||
}
|
||||
|
||||
--- Creates a new Pool
|
||||
-- @param name Name for the Pool.
|
||||
-- @param filter Table containing the required BaseComponents
|
||||
-- @return The new Pool
|
||||
function Pool.new(name, filter)
|
||||
local pool = setmetatable(List(), Pool.__mt)
|
||||
|
||||
pool.__name = name
|
||||
pool.__filter = filter
|
||||
|
||||
pool.__isPool = true
|
||||
|
||||
return pool
|
||||
end
|
||||
|
||||
--- Checks if an Entity is eligible for the Pool.
|
||||
-- @param e Entity to check
|
||||
-- @return True if the entity is eligible, false otherwise
|
||||
function Pool:__eligible(e)
|
||||
for _, component in ipairs(self.__filter) do
|
||||
if not e[component] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Internal: Adds an Entity to the Pool.
|
||||
-- @param e Entity to add
|
||||
-- @return self
|
||||
function Pool:__add(e)
|
||||
List.__add(self, e)
|
||||
self:onEntityAdded(e)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Internal: Removed an Entity from the Pool.
|
||||
-- @param e Entity to remove
|
||||
-- @return self
|
||||
function Pool:__remove(e)
|
||||
List.__remove(self, e)
|
||||
self:onEntityRemoved(e)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Gets the name of the Pool
|
||||
-- @return Name of the Pool.
|
||||
function Pool:getName()
|
||||
return self.__name
|
||||
end
|
||||
|
||||
--- Gets the filter of the Pool.
|
||||
-- Warning: Do not modify this filter.
|
||||
-- @return Filter of the Pool.
|
||||
function Pool:getFilter()
|
||||
return self.__filter
|
||||
end
|
||||
|
||||
--- Callback for when an Entity is added to the Pool.
|
||||
-- @param e Entity that was added.
|
||||
function Pool:onEntityAdded(e) -- luacheck: ignore
|
||||
end
|
||||
|
||||
-- Callback for when an Entity is removed from the Pool.
|
||||
-- @param e Entity that was removed.
|
||||
function Pool:onEntityRemoved(e) -- luacheck: ignore
|
||||
end
|
||||
|
||||
return setmetatable(Pool, {
|
||||
__index = List,
|
||||
__call = function(_, ...)
|
||||
return Pool.new(...)
|
||||
end,
|
||||
})
|
198
concord/system.lua
Normal file
198
concord/system.lua
Normal file
|
@ -0,0 +1,198 @@
|
|||
--- System
|
||||
-- A System iterates over Entities. From these Entities its get Components and modify them.
|
||||
-- A System contains 1 or more Pools.
|
||||
-- A System is contained by 1 World.
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Pool = require(PATH..".pool")
|
||||
local Utils = require(PATH..".utils")
|
||||
|
||||
local System = {
|
||||
ENABLE_OPTIMIZATION = true,
|
||||
}
|
||||
|
||||
System.mt = {
|
||||
__index = System,
|
||||
__call = function(systemClass, world)
|
||||
local system = setmetatable({
|
||||
__enabled = true,
|
||||
|
||||
__pools = {},
|
||||
__world = world,
|
||||
|
||||
__isSystem = true,
|
||||
__isSystemClass = false, -- Overwrite value from systemClass
|
||||
}, systemClass)
|
||||
|
||||
-- Optimization: We deep copy the World class into our instance of a world.
|
||||
-- This grants slightly faster access times at the cost of memory.
|
||||
-- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
|
||||
if (System.ENABLE_OPTIMIZATION) then
|
||||
Utils.shallowCopy(systemClass, system)
|
||||
end
|
||||
|
||||
for _, filter in pairs(systemClass.__filter) do
|
||||
local pool = system.__buildPool(filter)
|
||||
if not system[pool.__name] then
|
||||
system[pool.__name] = pool
|
||||
system.__pools[#system.__pools + 1] = pool
|
||||
else
|
||||
error("Pool with name '"..pool.name.."' already exists.")
|
||||
end
|
||||
end
|
||||
|
||||
system:init(world)
|
||||
|
||||
return system
|
||||
end,
|
||||
}
|
||||
|
||||
--- Creates a new SystemClass.
|
||||
-- @param ... Variable amounts of filters
|
||||
-- @return A new SystemClass
|
||||
function System.new(...)
|
||||
local systemClass = setmetatable({
|
||||
__isSystemClass = true,
|
||||
__filter = {...},
|
||||
}, System.mt)
|
||||
systemClass.__index = systemClass
|
||||
|
||||
-- Optimization: We deep copy the World class into our instance of a world.
|
||||
-- This grants slightly faster access times at the cost of memory.
|
||||
-- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
|
||||
if (System.ENABLE_OPTIMIZATION) then
|
||||
Utils.shallowCopy(System, systemClass)
|
||||
end
|
||||
|
||||
return systemClass
|
||||
end
|
||||
|
||||
--- Internal: Builds a Pool for the System.
|
||||
-- @param baseFilter The 'raw' Filter
|
||||
-- @return A new Pool
|
||||
function System.__buildPool(baseFilter)
|
||||
local name = "pool"
|
||||
local filter = {}
|
||||
|
||||
for _, value in ipairs(baseFilter) do
|
||||
if type(value) == "table" then
|
||||
filter[#filter + 1] = value
|
||||
elseif type(value) == "string" then
|
||||
name = value
|
||||
end
|
||||
end
|
||||
|
||||
return Pool(name, filter)
|
||||
end
|
||||
|
||||
--- Internal: Evaluates an Entity for all the System's Pools.
|
||||
-- @param e The Entity to check
|
||||
-- @return self
|
||||
function System:__evaluate(e)
|
||||
for _, pool in ipairs(self.__pools) do
|
||||
local has = pool:has(e)
|
||||
local eligible = pool:__eligible(e)
|
||||
|
||||
if not has and eligible then
|
||||
pool:__add(e)
|
||||
elseif has and not eligible then
|
||||
pool:__remove(e)
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Internal: Removes an Entity from the System.
|
||||
-- @param e The Entity to remove
|
||||
-- @return self
|
||||
function System:__remove(e)
|
||||
for _, pool in ipairs(self.__pools) do
|
||||
if pool:has(e) then
|
||||
pool:__remove(e)
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Internal: Clears all Entities from the System.
|
||||
-- @return self
|
||||
function System:clear()
|
||||
for i = 1, #self.__pools do
|
||||
self.__pools[i]:__clear()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Enables the System.
|
||||
-- @return self
|
||||
function System:enable()
|
||||
self:setEnabled(true)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Disables the System.
|
||||
-- @return self
|
||||
function System:disable()
|
||||
self:setEnabled(false)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Toggles if the System is enabled.
|
||||
-- @return self
|
||||
function System:toggleEnable()
|
||||
self:setEnabled(not self.__enabled)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Sets if the System is enabled
|
||||
-- @param enable Enable
|
||||
-- @return self
|
||||
function System:setEnabled(enable)
|
||||
if (not self.__enabled and enable) then
|
||||
self.__enabled = true
|
||||
self:onEnabled()
|
||||
elseif (self.__enabled and not enable) then
|
||||
self.__enabled = false
|
||||
self:onDisabled()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Returns is the System is enabled
|
||||
-- @return True if the System is enabled, false otherwise
|
||||
function System:isEnabled()
|
||||
return self.__enabled
|
||||
end
|
||||
|
||||
--- Returns the World the System is in.
|
||||
-- @return The World the System is in
|
||||
function System:getWorld()
|
||||
return self.__world
|
||||
end
|
||||
|
||||
--- Callback for system initialization.
|
||||
-- @param world The World the System was added to
|
||||
function System:init(world) -- luacheck: ignore
|
||||
end
|
||||
|
||||
-- Callback for when a System is enabled.
|
||||
function System:onEnabled() -- luacheck: ignore
|
||||
end
|
||||
|
||||
-- Callback for when a System is disabled.
|
||||
function System:onDisabled() -- luacheck: ignore
|
||||
end
|
||||
|
||||
return setmetatable(System, {
|
||||
__call = function(_, ...)
|
||||
return System.new(...)
|
||||
end,
|
||||
})
|
33
concord/systems.lua
Normal file
33
concord/systems.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--- Systems
|
||||
-- Container for registered SystemClasses
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
|
||||
local Systems = {}
|
||||
|
||||
--- Registers a SystemClass.
|
||||
-- @param name Name to register under
|
||||
-- @param systemClass SystemClass to register
|
||||
function Systems.register(name, systemClass)
|
||||
if (type(name) ~= "string") then
|
||||
error("bad argument #1 to 'Systems.register' (string expected, got "..type(name)..")", 3)
|
||||
end
|
||||
|
||||
if (not Type.isSystemClass(systemClass)) then
|
||||
error("bad argument #2 to 'Systems.register' (systemClass expected, got "..type(systemClass)..")", 3)
|
||||
end
|
||||
|
||||
if (rawget(Systems, name)) then
|
||||
error("bad argument #2 to 'Systems.register' (System with name '"..name.."' is already registerd)", 3)
|
||||
end
|
||||
|
||||
Systems[name] = systemClass
|
||||
end
|
||||
|
||||
return setmetatable(Systems, {
|
||||
__index = function(_, name)
|
||||
error("Attempt to index system '"..tostring(name).."' that does not exist / was not registered", 2)
|
||||
end
|
||||
})
|
55
concord/type.lua
Normal file
55
concord/type.lua
Normal file
|
@ -0,0 +1,55 @@
|
|||
--- Type
|
||||
-- Helper module to do easy type checking for Concord types
|
||||
|
||||
local Type = {}
|
||||
|
||||
--- Returns if object is an Entity.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an Entity, false otherwise
|
||||
function Type.isEntity(t)
|
||||
return type(t) == "table" and t.__isEntity or false
|
||||
end
|
||||
|
||||
--- Returns if object is a ComponentClass.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an ComponentClass, false otherwise
|
||||
function Type.isComponentClass(t)
|
||||
return type(t) == "table" and t.__isComponentClass or false
|
||||
end
|
||||
|
||||
--- Returns if object is a Component.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an Component, false otherwise
|
||||
function Type.isComponent(t)
|
||||
return type(t) == "table" and t.__isComponent or false
|
||||
end
|
||||
|
||||
--- Returns if object is a SystemClass.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an SystemClass, false otherwise
|
||||
function Type.isSystemClass(t)
|
||||
return type(t) == "table" and t.__isSystemClass or false
|
||||
end
|
||||
|
||||
--- Returns if object is a System.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an System, false otherwise
|
||||
function Type.isSystem(t)
|
||||
return type(t) == "table" and t.__isSystem or false
|
||||
end
|
||||
|
||||
--- Returns if object is a World.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an World, false otherwise
|
||||
function Type.isWorld(t)
|
||||
return type(t) == "table" and t.__isWorld or false
|
||||
end
|
||||
|
||||
--- Returns if object is an Assemblage.
|
||||
-- @param t Object to check
|
||||
-- @return True if object is an Assemblage, false otherwise
|
||||
function Type.isAssemblage(t)
|
||||
return type(t) == "table" and t.__isAssemblage or false
|
||||
end
|
||||
|
||||
return Type
|
15
concord/utils.lua
Normal file
15
concord/utils.lua
Normal file
|
@ -0,0 +1,15 @@
|
|||
--- Utils
|
||||
-- Helper module for misc operations
|
||||
|
||||
local Utils = {}
|
||||
|
||||
--- Does a shallow copy of a table and appends it to a target table.
|
||||
-- @param orig Table to copy
|
||||
-- @param target Table to append to
|
||||
function Utils.shallowCopy(orig, target)
|
||||
for key, value in pairs(orig) do
|
||||
target[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return Utils
|
291
concord/world.lua
Normal file
291
concord/world.lua
Normal file
|
@ -0,0 +1,291 @@
|
|||
--- World
|
||||
-- A World is a collection of Systems and Entities
|
||||
-- A world emits to let Systems iterate
|
||||
-- A World contains any amount of Systems
|
||||
-- A World contains any amount of Entities
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
local List = require(PATH..".list")
|
||||
local Utils = require(PATH..".utils")
|
||||
|
||||
local World = {
|
||||
ENABLE_OPTIMIZATION = true,
|
||||
}
|
||||
World.__mt = {
|
||||
__index = World,
|
||||
}
|
||||
|
||||
--- Creates a new World.
|
||||
-- @return The new World
|
||||
function World.new()
|
||||
local world = setmetatable({
|
||||
entities = List(),
|
||||
systems = List(),
|
||||
|
||||
events = {},
|
||||
|
||||
__added = List(), __backAdded = List(),
|
||||
__removed = List(), __backRemoved = List(),
|
||||
__dirty = List(), __backDirty = List(),
|
||||
|
||||
__systemLookup = {},
|
||||
|
||||
__isWorld = true,
|
||||
}, World.__mt)
|
||||
|
||||
-- Optimization: We deep copy the World class into our instance of a world.
|
||||
-- This grants slightly faster access times at the cost of memory.
|
||||
-- Since there (generally) won't be many instances of worlds this is a worthwhile tradeoff
|
||||
if (World.ENABLE_OPTIMIZATION) then
|
||||
Utils.shallowCopy(World, world)
|
||||
end
|
||||
|
||||
return world
|
||||
end
|
||||
|
||||
--- Adds an Entity to the World.
|
||||
-- @param e Entity to add
|
||||
-- @return self
|
||||
function World:addEntity(e)
|
||||
if not Type.isEntity(e) then
|
||||
error("bad argument #1 to 'World:addEntity' (Entity expected, got "..type(e)..")", 2)
|
||||
end
|
||||
|
||||
if e.__world then
|
||||
error("bad argument #1 to 'World:addEntity' (Entity was already added to a world)", 2)
|
||||
end
|
||||
|
||||
e.__world = self
|
||||
self.__added:__add(e)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes an Entity from the World.
|
||||
-- @param e Entity to remove
|
||||
-- @return self
|
||||
function World:removeEntity(e)
|
||||
if not Type.isEntity(e) then
|
||||
error("bad argument #1 to 'World:removeEntity' (Entity expected, got "..type(e)..")", 2)
|
||||
end
|
||||
|
||||
self.__removed:__add(e)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Internal: Marks an Entity as dirty.
|
||||
-- @param e Entity to mark as dirty
|
||||
function World:__dirtyEntity(e)
|
||||
if not self.__dirty:has(e) then
|
||||
self.__dirty:__add(e)
|
||||
end
|
||||
end
|
||||
|
||||
--- Internal: Flushes all changes to Entities.
|
||||
-- This processes all entities. Adding and removing entities, as well as reevaluating dirty entities.
|
||||
-- @return self
|
||||
function World:__flush()
|
||||
-- Early out
|
||||
if (self.__added.size == 0 and self.__removed.size == 0 and self.__dirty.size == 0) then
|
||||
return self
|
||||
end
|
||||
|
||||
-- Switch buffers
|
||||
self.__added, self.__backAdded = self.__backAdded, self.__added
|
||||
self.__removed, self.__backRemoved = self.__backRemoved, self.__removed
|
||||
self.__dirty, self.__backDirty = self.__backDirty, self.__dirty
|
||||
|
||||
local e
|
||||
|
||||
-- Process added entities
|
||||
for i = 1, self.__backAdded.size do
|
||||
e = self.__backAdded[i]
|
||||
|
||||
self.entities:__add(e)
|
||||
|
||||
for j = 1, self.systems.size do
|
||||
self.systems[j]:__evaluate(e)
|
||||
end
|
||||
|
||||
self:onEntityAdded(e)
|
||||
end
|
||||
self.__backAdded:__clear()
|
||||
|
||||
-- Process removed entities
|
||||
for i = 1, self.__backRemoved.size do
|
||||
e = self.__backRemoved[i]
|
||||
|
||||
e.__world = nil
|
||||
self.entities:__remove(e)
|
||||
|
||||
for j = 1, self.systems.size do
|
||||
self.systems[j]:__remove(e)
|
||||
end
|
||||
|
||||
self:onEntityRemoved(e)
|
||||
end
|
||||
self.__backRemoved:__clear()
|
||||
|
||||
-- Process dirty entities
|
||||
for i = 1, self.__backDirty.size do
|
||||
e = self.__backDirty[i]
|
||||
|
||||
for j = 1, self.systems.size do
|
||||
self.systems[j]:__evaluate(e)
|
||||
end
|
||||
end
|
||||
self.__backDirty:__clear()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- These functions won't be seen as callbacks that will be emitted to.
|
||||
local blacklistedSystemFunctions = {
|
||||
"init",
|
||||
"onEnabled",
|
||||
"onDisabled",
|
||||
}
|
||||
|
||||
--- Adds a System to the World.
|
||||
-- Callbacks are registered automatically
|
||||
-- Entities added before are added to the System retroactively
|
||||
-- @see World:emit
|
||||
-- @param systemClass SystemClass of System to add
|
||||
-- @return self
|
||||
function World:addSystem(systemClass)
|
||||
if (not Type.isSystemClass(systemClass)) then
|
||||
error("bad argument #1 to 'World:addSystems' (SystemClass expected, got "..type(systemClass)..")", 2)
|
||||
end
|
||||
|
||||
if (self.__systemLookup[systemClass]) then
|
||||
error("bad argument #1 to 'World:addSystems' (SystemClass was already added to World)", 2)
|
||||
end
|
||||
|
||||
-- Create instance of system
|
||||
local system = systemClass(self)
|
||||
|
||||
self.__systemLookup[systemClass] = system
|
||||
self.systems:__add(system)
|
||||
|
||||
for callbackName, callback in pairs(systemClass) do
|
||||
-- Skip callback if its blacklisted
|
||||
if (not blacklistedSystemFunctions[callbackName]) then
|
||||
-- Make container for all listeners of the callback if it does not exist yet
|
||||
if (not self.events[callbackName]) then
|
||||
self.events[callbackName] = {}
|
||||
end
|
||||
|
||||
-- Add callback to listeners
|
||||
local listeners = self.events[callbackName]
|
||||
listeners[#listeners + 1] = {
|
||||
system = system,
|
||||
callback = callback,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Evaluate all existing entities
|
||||
for j = 1, self.entities.size do
|
||||
system:__evaluate(self.entities[j])
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Adds multiple Systems to the World.
|
||||
-- Callbacks are registered automatically
|
||||
-- @see World:addSystem
|
||||
-- @see World:emit
|
||||
-- @param ... SystemClasses of Systems to add
|
||||
-- @return self
|
||||
function World:addSystems(...)
|
||||
for i = 1, select("#", ...) do
|
||||
local systemClass = select(i, ...)
|
||||
|
||||
self:addSystem(systemClass)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Returns if the World has a System.
|
||||
-- @param systemClass SystemClass of System to check for
|
||||
-- @return True if World has System, false otherwise
|
||||
function World:hasSystem(systemClass)
|
||||
if not Type.isSystemClass(systemClass) then
|
||||
error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2)
|
||||
end
|
||||
|
||||
return self.__systemLookup[systemClass] and true or false
|
||||
end
|
||||
|
||||
--- Gets a System from the World.
|
||||
-- @param systemClass SystemClass of System to get
|
||||
-- @return System to get
|
||||
function World:getSystem(systemClass)
|
||||
if not Type.isSystemClass(systemClass) then
|
||||
error("bad argument #1 to 'World:getSystem' (systemClass expected, got "..type(systemClass)..")", 2)
|
||||
end
|
||||
|
||||
return self.__systemLookup[systemClass]
|
||||
end
|
||||
|
||||
--- Emits a callback in the World.
|
||||
-- Calls all functions with the functionName of added Systems
|
||||
-- @param functionName Name of functions to call.
|
||||
-- @param ... Parameters passed to System's functions
|
||||
-- @return self
|
||||
function World:emit(functionName, ...)
|
||||
if not functionName or type(functionName) ~= "string" then
|
||||
error("bad argument #1 to 'World:emit' (String expected, got "..type(functionName)..")")
|
||||
end
|
||||
|
||||
local listeners = self.events[functionName]
|
||||
|
||||
if listeners then
|
||||
for i = 1, #listeners do
|
||||
local listener = listeners[i]
|
||||
|
||||
if (listener.system.__enabled) then
|
||||
self:__flush()
|
||||
|
||||
listener.callback(listener.system, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Removes all entities from the World
|
||||
-- @return self
|
||||
function World:clear()
|
||||
for i = 1, self.entities.size do
|
||||
self:removeEntity(self.entities[i])
|
||||
end
|
||||
|
||||
for i = 1, self.systems.size do
|
||||
self.systems[i]:clear()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Callback for when an Entity is added to the World.
|
||||
-- @param e The Entity that was added
|
||||
function World:onEntityAdded(e) -- luacheck: ignore
|
||||
end
|
||||
|
||||
--- Callback for when an Entity is removed from the World.
|
||||
-- @param e The Entity that was removed
|
||||
function World:onEntityRemoved(e) -- luacheck: ignore
|
||||
end
|
||||
|
||||
return setmetatable(World, {
|
||||
__call = function(_, ...)
|
||||
return World.new(...)
|
||||
end,
|
||||
})
|
33
concord/worlds.lua
Normal file
33
concord/worlds.lua
Normal file
|
@ -0,0 +1,33 @@
|
|||
--- Worlds
|
||||
-- Container for registered Worlds
|
||||
|
||||
local PATH = (...):gsub('%.[^%.]+$', '')
|
||||
|
||||
local Type = require(PATH..".type")
|
||||
|
||||
local Worlds = {}
|
||||
|
||||
--- Registers a World.
|
||||
-- @param name Name to register under
|
||||
-- @param world World to register
|
||||
function Worlds.register(name, world)
|
||||
if (type(name) ~= "string") then
|
||||
error("bad argument #1 to 'Worlds.register' (string expected, got "..type(name)..")", 3)
|
||||
end
|
||||
|
||||
if (not Type.isWorld(world)) then
|
||||
error("bad argument #2 to 'Worlds.register' (world expected, got "..type(world)..")", 3)
|
||||
end
|
||||
|
||||
if (rawget(Worlds, name)) then
|
||||
error("bad argument #2 to 'Worlds.register' (World with name '"..name.."' is already registerd)", 3)
|
||||
end
|
||||
|
||||
Worlds[name] = world
|
||||
end
|
||||
|
||||
return setmetatable(Worlds, {
|
||||
__index = function(_, name)
|
||||
error("Attempt to index world '"..tostring(name).."' that does not exist / was not registered", 2)
|
||||
end
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue