Concord/concord/world.lua
Pablo Ariel Mayobre cc0fd1614c Serializable component
You can remove the component to tell Concord an Entity shouldn't be serialized.

It's given automatically on Entity creation, but this can be disabled by changing Entity.SERIALIZE_BY_DEFAULT to false.
2023-02-14 22:20:34 -03:00

366 lines
9 KiB
Lua

--- 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.
-- @classmod World
local PATH = (...):gsub('%.[^%.]+$', '')
local Entity = require(PATH..".entity")
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.
-- @treturn World The new World
function World.new()
local world = setmetatable({
__entities = List(),
__systems = List(),
__events = {},
__emitSDepth = 0,
__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.
-- @tparam Entity e Entity to add
-- @treturn World self
function World:addEntity(e)
if not Type.isEntity(e) then
Utils.error(2, "bad argument #1 to 'World:addEntity' (Entity expected, got %s)", type(e))
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.
-- @tparam Entity e Entity to remove
-- @treturn World self
function World:removeEntity(e)
if not Type.isEntity(e) then
Utils.error(2, "bad argument #1 to 'World:removeEntity' (Entity expected, got %s)", type(e))
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.
-- @treturn World 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]
if e.__world == self then
self.__entities:add(e)
for j = 1, self.__systems.size do
self.__systems[j]:__evaluate(e)
end
self:onEntityAdded(e)
end
end
self.__backAdded:clear()
-- Process removed entities
for i = 1, self.__backRemoved.size do
e = self.__backRemoved[i]
if e.__world == self then
e.__world = nil
self.__entities:remove(e)
for j = 1, self.__systems.size do
self.__systems[j]:__remove(e)
end
self:onEntityRemoved(e)
end
end
self.__backRemoved:clear()
-- Process dirty entities
for i = 1, self.__backDirty.size do
e = self.__backDirty[i]
if e.__world == self then
for j = 1, self.__systems.size do
self.__systems[j]:__evaluate(e)
end
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",
}
local tryAddSystem = function (world, systemClass)
if (not Type.isSystemClass(systemClass)) then
return false, "SystemClass expected, got "..type(systemClass)
end
if (world.__systemLookup[systemClass]) then
return false, "SystemClass was already added to World"
end
-- Create instance of system
local system = systemClass(world)
world.__systemLookup[systemClass] = system
world.__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 world.__events[callbackName]) then
world.__events[callbackName] = {}
end
-- Add callback to listeners
local listeners = world.__events[callbackName]
listeners[#listeners + 1] = {
system = system,
callback = callback,
}
end
end
-- Evaluate all existing entities
for j = 1, world.__entities.size do
system:__evaluate(world.__entities[j])
end
return true
end
--- Adds a System to the World.
-- Callbacks are registered automatically
-- Entities added before are added to the System retroactively
-- @see World:emit
-- @tparam System systemClass SystemClass of System to add
-- @treturn World self
function World:addSystem(systemClass)
local ok, err = tryAddSystem(self, systemClass)
if not ok then
Utils.error(2, "bad argument #1 to 'World:addSystem' (%s)", err)
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
-- @treturn World self
function World:addSystems(...)
for i = 1, select("#", ...) do
local systemClass = select(i, ...)
local ok, err = tryAddSystem(self, systemClass)
if not ok then
Utils.error(2, "bad argument #%d to 'World:addSystems' (%s)", i, err)
end
end
return self
end
--- Returns if the World has a System.
-- @tparam System systemClass SystemClass of System to check for
-- @treturn boolean
function World:hasSystem(systemClass)
if not Type.isSystemClass(systemClass) then
Utils.error(2, "bad argument #1 to 'World:hasSystem' (SystemClass expected, got %s)", type(systemClass))
end
return self.__systemLookup[systemClass] and true or false
end
--- Gets a System from the World.
-- @tparam System systemClass SystemClass of System to get
-- @treturn System System to get
function World:getSystem(systemClass)
if not Type.isSystemClass(systemClass) then
Utils.error(2, "bad argument #1 to 'World:getSystem' (SystemClass expected, got %s)", type(systemClass))
end
return self.__systemLookup[systemClass]
end
--- Emits a callback in the World.
-- Calls all functions with the functionName of added Systems
-- @string functionName Name of functions to call.
-- @param ... Parameters passed to System's functions
-- @treturn World self
function World:emit(functionName, ...)
if not functionName or type(functionName) ~= "string" then
Utils.error(2, "bad argument #1 to 'World:emit' (String expected, got %s)", type(functionName))
end
local shouldFlush = self.__emitSDepth == 0
self.__emitSDepth = self.__emitSDepth + 1
local listeners = self.__events[functionName]
if listeners then
for i = 1, #listeners do
local listener = listeners[i]
if (listener.system.__enabled) then
if (shouldFlush) then
self:__flush()
end
listener.callback(listener.system, ...)
end
end
end
self.__emitSDepth = self.__emitSDepth - 1
return self
end
--- Removes all entities from the World
-- @treturn World self
function World:clear()
for i = 1, self.__entities.size do
self:removeEntity(self.__entities[i])
end
for i = 1, self.__added.size do
local e = self.__added[i]
e.__world = nil
end
self.__added:clear()
self:__flush()
return self
end
function World:getEntities()
return self.__entities
end
function World:getSystems()
return self.__systems
end
function World:serialize()
self:__flush()
local data = {}
for i = 1, self.__entities.size do
local entity = self.__entities[i]
if entity.serializable then
local entityData = entity:serialize()
table.insert(data, entityData)
end
end
return data
end
function World:deserialize(data, append)
if (not append) then
self:clear()
end
for i = 1, #data do
local entityData = data[i]
local entity = Entity()
entity:deserialize(entityData)
self:addEntity(entity)
end
self:__flush()
end
--- Callback for when an Entity is added to the World.
-- @tparam Entity e The Entity that was added
function World:onEntityAdded(e) -- luacheck: ignore
end
--- Callback for when an Entity is removed from the World.
-- @tparam Entity e The Entity that was removed
function World:onEntityRemoved(e) -- luacheck: ignore
end
return setmetatable(World, {
__call = function(_, ...)
return World.new(...)
end,
})