rename folder src to concord

This commit is contained in:
Tjakka5 2020-01-04 10:50:13 +01:00
parent 451b88cdea
commit f502f1b9f6
14 changed files with 0 additions and 0 deletions

37
concord/assemblage.lua Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
})