diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
index ca60d46..17994ca 100644
--- a/.github/workflows/doc.yml
+++ b/.github/workflows/doc.yml
@@ -29,7 +29,7 @@ jobs:
run: luarocks show ldoc
- name: Build docs
- run: ldoc .
+ run: lua scripts/build-docs.lua
- name: Deploy
uses: JamesIves/github-pages-deploy-action@4.1.5
diff --git a/.gitignore b/.gitignore
index 989d364..bd74f33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,4 +40,7 @@ luac.out
*.hex
# VSCode
-.vscode/
\ No newline at end of file
+.vscode/
+
+# LDoc preprocess output
+.ldoc-guides/
\ No newline at end of file
diff --git a/README.md b/README.md
index c9626cc..ac2dbd4 100644
--- a/README.md
+++ b/README.md
@@ -17,13 +17,13 @@ local Concord = require("path.to.concord")
## Documentation
-Start with **[Get Started](docs/Get%20Started.md)**. It covers Worlds, Components, Entities, Filters, Systems, and Events.
+Start with **[Getting Started](docs/index.html)**. It covers Worlds, Components, Entities, Filters, Systems, and Events.
### Guides
-- [Why ECS?](docs/Why%20ECS.md) — when Concord (and ECS in general) is a good fit
-- [Custom Pools](docs/Custom%20Pools.md) — using a different data structure than the default Pool list
-- [Pool Flushing and Timing](docs/Pool%20Flushing%20and%20Timing.md) — when Filters update, and how that interacts with Events
+- [Why ECS?](docs/guides/Why-ECS.md.html) — when Concord (and ECS in general) is a good fit
+- [Custom Pools](docs/guides/Custom-Pools.md.html) — using a different data structure than the default Pool list
+- [Pool Flushing and Timing](docs/guides/Pool-Flushing-and-Timing.md.html) — when Filters update, and how that interacts with Events
API reference is generated with LDoc and published on the [GitHub page](https://keyslam-group.github.io/Concord/).
diff --git a/concord/builtins/key.lua b/concord/builtins/key.lua
index 7b23275..3591178 100644
--- a/concord/builtins/key.lua
+++ b/concord/builtins/key.lua
@@ -1,3 +1,7 @@
+--- Built-in Component that assigns a unique key to an Entity in its World.
+-- The Entity must belong to a World. Calling the Component returns the key value.
+-- @builtin key
+
local PATH = (...):gsub('%.builtins%.[^%.]+$', '')
local Component = require(PATH..".component")
@@ -18,14 +22,21 @@ local Key = Component("key", function (self, key)
self.value = getKey(self, key)
end)
+--- Assigns a key from serialized data.
+-- @param data Previously serialized key value
function Key:deserialize (data)
self.value = getKey(self, data)
end
+--- Returns the key value.
+-- @function Key
+-- @treturn string|number The Entity's key
function Key.__mt:__call()
return self.value
end
+--- Callback: Clears the World key when the Component is removed (not replaced).
+-- @tparam boolean replaced True when the Component is being overwritten
function Key:removed (replaced)
if not replaced then
local entity = self.__entity
diff --git a/concord/builtins/serializable.lua b/concord/builtins/serializable.lua
index 125f76b..1107334 100644
--- a/concord/builtins/serializable.lua
+++ b/concord/builtins/serializable.lua
@@ -1,9 +1,16 @@
+--- Built-in marker Component. Entities with this Component are included in World:serialize.
+-- The Component itself is not serialized.
+-- @builtin serializable
+
local PATH = (...):gsub('%.builtins%.[^%.]+$', '')
local Component = require(PATH..".component")
local Serializable = Component("serializable")
+--- Callback: When the Component gets serialized as part of an Entity.
+-- Returns nil so this marker is not written to serialized data.
+-- @treturn nil
function Serializable:serialize ()
-- Don't serialize this Component
return nil
diff --git a/concord/component.lua b/concord/component.lua
index 981b999..d04583f 100644
--- a/concord/component.lua
+++ b/concord/component.lua
@@ -11,8 +11,9 @@ Component.__mt = {
__index = Component,
}
---- Creates a new ComponentClass.
--- @tparam function populate Function that populates a Component with values
+--- Creates a new ComponentClass and registers it by name.
+-- @string name Unique name of the ComponentClass
+-- @tparam[opt] function populate Function that populates a Component with values
-- @treturn Component A new ComponentClass
function Component.new(name, populate)
if (type(name) ~= "string") then
@@ -51,11 +52,13 @@ end
function Component:__populate() -- luacheck: ignore
end
--- Callback: When the Component gets removed or replaced in an Entity.
-function Component:removed() -- luacheck: ignore
+--- Callback: When the Component gets removed or replaced in an Entity.
+-- @tparam boolean replaced True when the Component is being overwritten, false when it is being removed
+function Component:removed(replaced) -- luacheck: ignore
end
--- Callback: When the Component gets serialized as part of an Entity.
+--- Callback: When the Component gets serialized as part of an Entity.
+-- @treturn table|nil Serialized data, or nil to omit this Component
function Component:serialize()
local data = Utils.shallowCopy(self, {})
@@ -68,7 +71,8 @@ function Component:serialize()
return data
end
--- Callback: When the Component gets deserialized from serialized data.
+--- Callback: When the Component gets deserialized from serialized data.
+-- @tparam table data Serialized data to copy onto this Component
function Component:deserialize(data)
Utils.shallowCopy(data, self)
end
@@ -95,7 +99,7 @@ end
function Component:__initialize(entity, ...)
local component = self:__new(entity)
- ---@diagnostic disable-next-line: redundant-parameter
+ -- LuaLS: redundant-parameter (varargs forwarded to populate)
self.__populate(component, ...)
return component
diff --git a/concord/components.lua b/concord/components.lua
index b2743e7..fd08fd9 100644
--- a/concord/components.lua
+++ b/concord/components.lua
@@ -6,7 +6,7 @@ local Components = {}
Components.__REJECT_PREFIX = "!"
Components.__REJECT_MATCH = "^(%"..Components.__REJECT_PREFIX.."?)(.+)"
---- Returns true if the containter has the ComponentClass with the specified name
+--- Returns true if the container has the ComponentClass with the specified name
-- @string name Name of the ComponentClass to check
-- @treturn boolean
function Components.has(name)
@@ -27,10 +27,10 @@ end
--- Returns true and the ComponentClass if one was registered with the specified name
-- or false and an error otherwise
-- @string name Name of the ComponentClass to check
--- @boolean acceptRejected Whether to accept names prefixed with the Reject Prefix.
--- @treturn boolean
--- @treturn Component or error string
--- @treturn true if acceptRejected was true and the name had the Reject Prefix, false otherwise.
+-- @tparam[opt] boolean acceptRejected Whether to accept names prefixed with the Reject Prefix.
+-- @treturn boolean ok
+-- @treturn Component|string ComponentClass on success, or an error string on failure
+-- @treturn string|boolean On success: the stripped Component name if the name had the Reject Prefix, otherwise false
function Components.try(name, acceptRejected)
if type(name) ~= "string" then
return false, "ComponentsClass name is expected to be a string, got "..type(name)..")"
@@ -54,7 +54,7 @@ end
--- Returns the ComponentClass with the specified name
-- @string name Name of the ComponentClass to get
--- @treturn Component
+-- @treturn Component The registered ComponentClass
function Components.get(name)
local ok, value = Components.try(name)
diff --git a/concord/entity.lua b/concord/entity.lua
index 29145fa..cf8c990 100644
--- a/concord/entity.lua
+++ b/concord/entity.lua
@@ -13,6 +13,7 @@ local Builtins = require(PATH..".builtins.init") --luacheck: ignore
-- Builtins is unused but the require already registers the Components
local Entity = {
+ --- If true, new Entities are given a "serializable" Component.
SERIALIZE_BY_DEFAULT = true,
}
@@ -20,7 +21,8 @@ Entity.__mt = {
__index = Entity,
}
---- Creates a new Entity. Optionally adds it to a World.
+--- Creates a new Entity. Optionally queues it to be added to a World.
+-- If Entity.SERIALIZE_BY_DEFAULT is true, the Entity is given a "serializable" Component.
-- @tparam[opt] World world World to add the entity to
-- @treturn Entity A new Entity
function Entity.new(world)
@@ -121,25 +123,25 @@ local function removeComponent(e, name)
end
--- Gives an Entity a Component.
--- If the Component already exists, it's overridden by this new Component
--- @tparam Component componentClass ComponentClass to add an instance of
--- @param ... additional arguments to pass to the Component's populate function
+-- If the Component already exists, it's overridden by this new Component.
+-- @tparam string|Component name Name of the ComponentClass, or a Component instance to copy onto the Entity
+-- @param ... additional arguments to pass to the Component's populate function (ignored when copying a Component)
-- @treturn Entity self
function Entity:give(name, ...)
return giveComponent(self, false, name, ...)
end
---- Ensures an Entity to have a Component.
--- If the Component already exists, no action is taken
--- @tparam Component componentClass ComponentClass to add an instance of
--- @param ... additional arguments to pass to the Component's populate function
+--- Ensures an Entity has a Component.
+-- If the Component already exists, no action is taken.
+-- @tparam string|Component name Name of the ComponentClass, or a Component instance to copy onto the Entity
+-- @param ... additional arguments to pass to the Component's populate function (ignored when copying a Component)
-- @treturn Entity self
function Entity:ensure(name, ...)
return giveComponent(self, true, name, ...)
end
--- Removes a Component from an Entity.
--- @tparam Component componentClass ComponentClass of the Component to remove
+-- @string name Name of the ComponentClass to remove
-- @treturn Entity self
function Entity:remove(name)
local ok, componentClass = Components.try(name)
@@ -168,8 +170,8 @@ function Entity:assemble(assemblage, ...)
end
--- Destroys the Entity.
--- Removes the Entity from its World if it's in one.
--- @return self
+-- Queues removal from its World if it's in one. The Entity stays in the World until the next flush.
+-- @treturn Entity self
function Entity:destroy()
if self.__world then
self.__world:removeEntity(self)
@@ -189,7 +191,7 @@ function Entity:__dirty()
end
--- Returns true if the Entity has a Component.
--- @tparam Component componentClass ComponentClass of the Component to check
+-- @string name Name of the ComponentClass to check
-- @treturn boolean
function Entity:has(name)
local ok, componentClass = Components.try(name)
@@ -202,8 +204,8 @@ function Entity:has(name)
end
--- Gets a Component from the Entity.
--- @tparam Component componentClass ComponentClass of the Component to get
--- @treturn table
+-- @string name Name of the ComponentClass to get
+-- @treturn Component|nil The Component, or nil if the Entity does not have it
function Entity:get(name)
local ok, componentClass = Components.try(name)
@@ -214,10 +216,11 @@ function Entity:get(name)
return self[name]
end
---- Returns a table of all Components the Entity has.
--- Warning: Do not modify this table.
--- Use Entity:give/ensure/remove instead
--- @treturn table Table of all Components the Entity has
+--- Returns a shallow copy of the Entity's fields, excluding __world and __isEntity.
+-- This is primarily the Entity's Components. Extra non-component fields are also copied.
+-- Mutating a copied Component still mutates the Entity's Component.
+-- @tparam[opt] table output Table to copy into. A new table is created if omitted.
+-- @treturn table
function Entity:getComponents(output)
output = output or {}
local components = Utils.shallowCopy(self, output)
@@ -227,7 +230,8 @@ function Entity:getComponents(output)
return components
end
---- Returns true if the Entity is in a World.
+--- Returns true if the Entity has been assigned a World.
+-- True as soon as addition is queued, and remains true until removal is flushed.
-- @treturn boolean
function Entity:inWorld()
return self.__world and true or false
@@ -239,6 +243,10 @@ function Entity:getWorld()
return self.__world
end
+--- Serializes the Entity's Components.
+-- Components that return nil from serialize are omitted.
+-- @tparam[opt] boolean ignoreKey If true, the key Component is omitted
+-- @treturn table Serialized Entity data
function Entity:serialize(ignoreKey)
local data = {}
@@ -262,6 +270,8 @@ function Entity:serialize(ignoreKey)
return data
end
+--- Deserializes Components onto the Entity.
+-- @tparam table data Serialized Entity data
function Entity:deserialize(data)
for i = 1, #data do
local componentData = data[i]
diff --git a/concord/filter.lua b/concord/filter.lua
index 0c4dabf..da78d12 100644
--- a/concord/filter.lua
+++ b/concord/filter.lua
@@ -16,9 +16,12 @@ Filter.__mt = {
}
--- Validates a Filter Definition to make sure every component is valid.
--- @string name Name for the Filter.
--- @tparam table definition Table containing the Filter Definition
--- @tparam onComponent Optional function, called when a component is valid.
+-- Also checks that def.constructor is callable when present, unless onComponent is provided.
+-- @int errorLevel Extra stack level added to error reports
+-- @tparam[opt] string name Name for the Filter. If omitted, errors refer to a World:query filter.
+-- @tparam table def Table containing the Filter Definition
+-- @tparam[opt] function onComponent Called with (component, reject) when a component is valid.
+-- reject is the stripped Component name when the entry had the Reject Prefix, otherwise false.
function Filter.validate (errorLevel, name, def, onComponent)
local filter = "World:query filter"
if name then
@@ -46,13 +49,12 @@ function Filter.validate (errorLevel, name, def, onComponent)
end
end
---- Parses the Filter Defintion into two tables
--- required: An array of all the required component names.
--- rejected: An array of all the components that will be rejected.
--- @string name Name for the Filter.
--- @tparam table definition Table containing the Filter Definition
--- @treturn table required
--- @treturn table rejected
+--- Parses a Filter Definition into a single interleaved table.
+-- Entries are stored as name, required pairs: {name, true, name, false, ...}
+-- where true means the Component is required and false means it is rejected.
+-- @tparam[opt] string name Name for the Filter.
+-- @tparam table def Table containing the Filter Definition
+-- @treturn table Parsed filter
function Filter.parse (name, def)
local filter = {}
@@ -69,6 +71,10 @@ function Filter.parse (name, def)
return filter
end
+--- Returns true if the Entity matches a parsed Filter.
+-- @tparam Entity e Entity to check
+-- @tparam table filter Parsed filter from Filter.parse
+-- @treturn boolean
function Filter.match (e, filter)
for i=#filter, 2, -2 do
local match = filter[i - 0]
@@ -83,6 +89,10 @@ end
local REQUIRED_METHODS = {"add", "remove", "has", "clear"}
local VALID_POOL_TYPES = {table=true, userdata=true, lightuserdata=true, cdata=true}
+--- Checks that a custom Pool implements add, remove, has, and clear as callables.
+-- The Pool may be a table, userdata, lightuserdata, or cdata.
+-- @string name Name of the Filter / Pool
+-- @param pool Pool returned by a custom constructor
function Filter.isValidPool (name, pool)
local poolType = type(pool)
--Check that pool is not nil
@@ -99,8 +109,9 @@ function Filter.isValidPool (name, pool)
end
--- Creates a new Filter
+-- If definition.constructor is set, it is called with the definition to create the Pool.
-- @string name Name for the Filter.
--- @tparam table definition Table containing the Filter Definition
+-- @tparam table def Table containing the Filter Definition
-- @treturn Filter The new Filter
-- @treturn Pool The associated Pool
function Filter.new (name, def)
@@ -134,6 +145,10 @@ function Filter:eligible(e)
return Filter.match(e, self.__filter)
end
+--- Evaluates an Entity against this Filter and updates the Pool.
+-- Adds the Entity if it became eligible, removes it if it no longer is.
+-- @tparam Entity e Entity to evaluate
+-- @treturn Filter self
function Filter:evaluate (e)
local has = self.pool:has(e)
local eligible = self:eligible(e)
@@ -148,9 +163,9 @@ function Filter:evaluate (e)
end
--- Adds an Entity to the Pool, if it passes the Filter.
--- @param e Entity to add
--- @param bypass Whether to bypass the Filter or not.
+--- Adds an Entity to the Pool, if it passes the Filter.
+-- @tparam Entity e Entity to add
+-- @tparam[opt] boolean bypass Whether to bypass the Filter or not.
-- @treturn Filter self
-- @treturn boolean Whether the entity was added or not.
function Filter:add (e, bypass)
@@ -163,24 +178,25 @@ function Filter:add (e, bypass)
return self, true
end
--- Remove an Entity from the Pool associated to this Filter.
--- @param e Entity to remove
+--- Removes an Entity from the Pool associated to this Filter.
+-- @tparam Entity e Entity to remove
-- @treturn Filter self
function Filter:remove (e)
self.pool:remove(e)
return self
end
--- Clear the Pool associated to this Filter.
--- @param e Entity to remove
+--- Clears the Pool associated to this Filter.
+-- Arguments are forwarded to the Pool's clear method.
+-- @param e Forwarded to pool:clear
-- @treturn Filter self
function Filter:clear (e)
self.pool:clear(e)
return self
end
--- Check if the Pool bound to this System contains the passed Entity
--- @param e Entity to check
+--- Checks if the Pool associated to this Filter contains the passed Entity.
+-- @tparam Entity e Entity to check
-- @treturn boolean Whether the Entity exists.
function Filter:has (e)
return self.pool:has(e)
diff --git a/concord/init.lua b/concord/init.lua
index cf0be80..2020a25 100644
--- a/concord/init.lua
+++ b/concord/init.lua
@@ -1,5 +1,11 @@
----
+--- A feature-complete ECS library.
-- @module Concord
+-- @field Entity Entity class
+-- @field Component Component class
+-- @field Components Registry of ComponentClasses
+-- @field System System class
+-- @field World World class
+-- @field utils Utility functions
local PATH = (...):gsub("%.init$", "")
diff --git a/concord/list.lua b/concord/list.lua
index 7f10c74..cb41f73 100644
--- a/concord/list.lua
+++ b/concord/list.lua
@@ -1,4 +1,4 @@
---- Data structure that allows for fast removal at the cost of containing order.
+--- Data structure that allows for fast removal at the cost of order.
-- @classmod List
local List = {}
@@ -32,7 +32,7 @@ end
--- Removes an object from the List.
-- @param obj Object to remove
--- @treturn List self
+-- @treturn List|nil self, or nil if the object was not in the List
function List:remove(obj)
local index = self[obj]
if not index then return end
@@ -57,6 +57,7 @@ function List:remove(obj)
end
--- Clears the List completely.
+-- Does not call onRemoved.
-- @treturn List self
function List:clear()
for i = 1, self.size do
@@ -86,6 +87,7 @@ function List:get(i)
end
--- Returns the index of an object in the List.
+-- Errors if the object is not in the List.
-- @param obj Object to get index of
-- @treturn number index of object in the List.
function List:indexOf(obj)
@@ -98,7 +100,7 @@ end
--- Sorts the List in place, using the order function.
-- The order function is passed to table.sort internally so documentation on table.sort can be used as reference.
--- @param order Function that takes two Entities (a and b) and returns true if a should go before than b.
+-- @param order Function that takes two items (a and b) and returns true if a should go before b.
-- @treturn List self
function List:sort(order)
table.sort(self, order)
@@ -115,7 +117,7 @@ end
function List:onAdded (obj) --luacheck: ignore
end
---- Callback for when an item is removed to the List.
+--- Callback for when an item is removed from the List.
-- @param obj Object that was removed
function List:onRemoved (obj) --luacheck: ignore
end
diff --git a/concord/system.lua b/concord/system.lua
index 070c3e4..21fc2e4 100644
--- a/concord/system.lua
+++ b/concord/system.lua
@@ -1,5 +1,5 @@
---- Iterates over Entities. From these Entities its get Components and modify them.
--- A System contains 1 or more Pools.
+--- Iterates over Entities. From these Entities it gets Components and modifies them.
+-- A System contains 0 or more Pools.
-- A System is contained by 1 World.
-- @classmod System
@@ -25,9 +25,9 @@ System.mt = {
__isSystemClass = false, -- Overwrite value from systemClass
}, systemClass)
- -- Optimization: We deep copy the System class into our instance of a system.
+ -- Optimization: We shallow copy the System class into our instance of a system.
-- 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
+ -- Since there (generally) won't be many instances of systems this is a worthwhile tradeoff
if (System.ENABLE_OPTIMIZATION) then
Utils.shallowCopy(systemClass, system)
end
@@ -45,8 +45,8 @@ System.mt = {
end,
}
--- Creates a new SystemClass.
--- @string name Name of the System
--- @tparam table definition A table containing filters (name = {components...})
+-- @tparam[opt] string name Name of the System. If omitted, or if a table is passed first, the first argument is the definition.
+-- @tparam[opt] table definition A table containing filters (name = {components...})
-- @treturn System A new SystemClass
function System.new(name, definition)
if type(name) == "table" then
@@ -74,9 +74,9 @@ function System.new(name, definition)
}, System.mt)
systemClass.__index = systemClass
- -- Optimization: We deep copy the World class into our instance of a world.
+ -- Optimization: We shallow copy the System class into the SystemClass.
-- 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
+ -- Since there (generally) won't be many instances of systems this is a worthwhile tradeoff
if (System.ENABLE_OPTIMIZATION) then
Utils.shallowCopy(System, systemClass)
end
@@ -118,7 +118,7 @@ function System:__clear()
return self
end
---- Sets if the System is enabled
+--- Sets whether the System is enabled.
-- @tparam boolean enable
-- @treturn System self
function System:setEnabled(enable)
@@ -133,7 +133,7 @@ function System:setEnabled(enable)
return self
end
---- Returns is the System is enabled
+--- Returns whether the System is enabled.
-- @treturn boolean
function System:isEnabled()
return self.__enabled
diff --git a/concord/type.lua b/concord/type.lua
index bf7f52a..a90787a 100644
--- a/concord/type.lua
+++ b/concord/type.lua
@@ -1,8 +1,11 @@
---- Type
--- Helper module to do easy type checking for Concord types
+--- Helper module to do easy type checking for Concord types
+-- @module Type
local Type = {}
+--- Returns true if the value is a function or has a __call metamethod.
+-- @param t Object to check
+-- @treturn boolean
function Type.isCallable(t)
if type(t) == "function" then return true end
diff --git a/concord/utils.lua b/concord/utils.lua
index 0802586..e4ddc63 100644
--- a/concord/utils.lua
+++ b/concord/utils.lua
@@ -1,15 +1,20 @@
---- Utils
--- Helper module for misc operations
+--- Helper module for misc operations
+-- @module Utils
local Utils = {}
+--- Raises a formatted error.
+-- @int level Error level passed to error(), plus one
+-- @string str Format string
+-- @param ... Format arguments
function Utils.error(level, str, ...)
error(string.format(str, ...), level + 1)
end
---- 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
+--- Copies keys from orig into target. Existing keys in target are overwritten.
+-- @tparam table orig Table to copy from
+-- @tparam table target Table to copy into
+-- @treturn table target
function Utils.shallowCopy(orig, target)
for key, value in pairs(orig) do
target[key] = value
@@ -18,12 +23,14 @@ function Utils.shallowCopy(orig, target)
return target
end
---- Requires files and puts them in a table.
--- Accepts a table of paths to Lua files: {"path/to/file_1", "path/to/another/file_2", "etc"}
--- Accepts a path to a directory with Lua files: "my_files/here"
--- @param pathOrFiles The table of paths or a path to a directory.
--- @param namespace A table that will hold the required files
--- @treturn table The namespace table
+--- Requires files and stores them in a namespace table.
+-- Accepts a table of require paths: {"path/to/file_1", "path/to/another/file_2", "etc"}
+-- Accepts a path to a directory of Lua files: "my_files/here"
+-- Directory loading uses love.filesystem and requires LÖVE.
+-- If namespace is omitted, files are still required and this function returns nil.
+-- @param pathOrFiles A directory path or a table of require paths
+-- @tparam[opt] table namespace Table that will hold the required files
+-- @treturn table|nil The namespace table
function Utils.loadNamespace(pathOrFiles, namespace)
if type(pathOrFiles) ~= "string" and type(pathOrFiles) ~= "table" then
Utils.error(2, "bad argument #1 to 'loadNamespace' (string/table of strings expected, got %s)", type(pathOrFiles))
diff --git a/concord/world.lua b/concord/world.lua
index c11c9a6..e074e48 100644
--- a/concord/world.lua
+++ b/concord/world.lua
@@ -1,7 +1,6 @@
--- 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.
+-- A World emits events so Systems can iterate their Pools.
+-- A World contains any amount of Systems and Entities, plus optional named resources.
-- @classmod World
local PATH = (...):gsub('%.[^%.]+$', '')
@@ -57,7 +56,7 @@ function World.new()
__flushing = false,
}, World.__mt)
- -- Optimization: We deep copy the World class into our instance of a world.
+ -- Optimization: We shallow 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
@@ -67,7 +66,9 @@ function World.new()
return world
end
---- Adds an Entity to the World.
+--- Queues an Entity to be added to the World.
+-- The Entity's world is set immediately. The Entity is not in getEntities()
+-- and is not evaluated by Systems until the next flush.
-- @tparam Entity e Entity to add
-- @treturn World self
function World:addEntity(e)
@@ -85,12 +86,20 @@ function World:addEntity(e)
return self
end
---- Creates a new Entity and adds it to the World.
+--- Creates a new Entity and queues it to be added to the World.
-- @treturn Entity e the new Entity
function World:newEntity()
return Entity(self)
end
+--- Queries flushed Entities against a Filter definition.
+-- Pending (not yet flushed) additions are not included. Component changes on
+-- already flushed Entities are visible immediately.
+-- If onMatch is a callable it is invoked for each match and nothing is returned.
+-- Otherwise matches are appended to onMatch when it is a table, or to a new table.
+-- @tparam table def Filter definition (component names, with ! prefix to reject)
+-- @tparam[opt] function|table onMatch Callback for each match, or a table to fill
+-- @treturn table|nil The list of matches, or nil if onMatch was a callback
function World:query(def, onMatch)
local filter = Filter.parse(nil, def)
@@ -112,7 +121,9 @@ function World:query(def, onMatch)
return list
end
---- Removes an Entity from the World.
+--- Queues an Entity to be removed from the World.
+-- If the Entity has a key Component, it is removed first.
+-- The Entity remains in the World until the next flush.
-- @tparam Entity e Entity to remove
-- @treturn World self
function World:removeEntity(e)
@@ -149,8 +160,8 @@ function World:canFlush()
return not self.__flushing
end
---- Flushes all changes to Entities.
--- This processes all entities. Adding and removing entities, as well as reevaluating dirty entities.
+--- Applies queued Entity additions, removals, and dirty reevaluations.
+-- Only the queued buffers are processed, not every Entity in the World.
-- @treturn World self
function World:flush()
if not self:canFlush() then
@@ -221,7 +232,8 @@ function World:flush()
return self
end
--- These functions won't be seen as callbacks that will be emitted to.
+-- Callback names that should not be registered as World events.
+-- This table is an array, so lookup by callback name never matches.
local blacklistedSystemFunctions = {
"init",
"onEnabled",
@@ -269,8 +281,8 @@ local tryAddSystem = function (world, systemClass)
end
--- Adds a System to the World.
--- Callbacks are registered automatically
--- Entities added before are added to the System retroactively
+-- Functions on the SystemClass are registered as event listeners.
+-- Entities already in the World are evaluated against the System's Filters.
-- @see World:emit
-- @tparam System systemClass SystemClass of System to add
-- @treturn World self
@@ -285,7 +297,7 @@ function World:addSystem(systemClass)
end
--- Adds multiple Systems to the World.
--- Callbacks are registered automatically
+-- Functions on each SystemClass are registered as event listeners.
-- @see World:addSystem
-- @see World:emit
-- @param ... SystemClasses of Systems to add
@@ -303,7 +315,7 @@ function World:addSystems(...)
return self
end
---- Returns if the World has a System.
+--- Returns whether the World has a System.
-- @tparam System systemClass SystemClass of System to check for
-- @treturn boolean
function World:hasSystem(systemClass)
@@ -326,9 +338,11 @@ function World:getSystem(systemClass)
end
--- Emits a callback in the World.
--- Calls all functions with the functionName of added Systems
+-- Calls all functions with the functionName of added Systems.
-- Automatically flushes before forwarding the event, unless this emit is nested
-- or a flush is already in progress.
+-- If beforeEmit exists and returns a truthy value, listeners are skipped.
+-- afterEmit is called after listeners when present.
-- @string functionName Name of functions to call.
-- @param ... Parameters passed to System's functions
-- @treturn World self
@@ -391,7 +405,9 @@ function World:emitNoFlush(functionName, ...)
return self
end
---- Removes all entities from the World
+--- Removes all Entities from the World and flushes.
+-- Entities that were queued to be added are detached immediately without
+-- onEntityRemoved. Already flushed Entities are removed through the normal flush.
-- @treturn World self
function World:clear()
for i = 1, self.__entities.size do
@@ -409,14 +425,22 @@ function World:clear()
return self
end
+--- Returns the List of flushed Entities in the World.
+-- @treturn List
function World:getEntities()
return self.__entities
end
+--- Returns the List of Systems in the World.
+-- @treturn List
function World:getSystems()
return self.__systems
end
+--- Serializes serializable Entities in the World.
+-- Flushes first. Only Entities with a serializable Component are included.
+-- @tparam[opt] boolean ignoreKeys If true, Entity keys are omitted
+-- @treturn table Serialized world data
function World:serialize(ignoreKeys)
self:flush()
@@ -434,6 +458,11 @@ function World:serialize(ignoreKeys)
return data
end
+--- Deserializes Entities into the World.
+-- @tparam table data Serialized world data
+-- @tparam[opt] boolean startClean If true, existing Entities are cleared first
+-- @tparam[opt] boolean ignoreGenerator If true, the World's key generator state is left unchanged
+-- @treturn World self
function World:deserialize(data, startClean, ignoreGenerator)
if startClean then
self:clear()
@@ -468,6 +497,10 @@ function World:deserialize(data, startClean, ignoreGenerator)
return self
end
+--- Sets the function used to generate Entity keys.
+-- @tparam function generator Callable that receives the current state and returns key, newState
+-- @param initialState Initial state passed to the generator
+-- @treturn World self
function World:setKeyGenerator(generator, initialState)
if not Type.isCallable(generator) then
Utils.error(2, "bad argument #1 to 'World:setKeyGenerator' (function expected, got %s)", type(generator))
@@ -479,6 +512,7 @@ function World:setKeyGenerator(generator, initialState)
return self
end
+-- Internal: Clears the key assigned to an Entity.
function World:__clearKey(e)
local key = self.__hash.keys[e]
@@ -490,6 +524,10 @@ function World:__clearKey(e)
return self
end
+-- Internal: Assigns a key to an Entity, generating one if needed.
+-- @param e Entity to assign a key to
+-- @param key Optional explicit key
+-- @return The assigned key
function World:__assignKey(e, key)
local hash = self.__hash
@@ -512,6 +550,9 @@ function World:__assignKey(e, key)
return key
end
+--- Gets an Entity by its key.
+-- @param key Key to look up
+-- @treturn Entity|nil
function World:getEntityByKey(key)
return self.__hash.entities[key]
end
@@ -526,25 +567,25 @@ end
function World:onEntityRemoved(e) -- luacheck: ignore
end
---- Sets a named resource in the world
+--- Sets a named resource in the World.
-- @string name Name of the resource
--- @tparam Any resource Resource to set
+-- @param resource Resource to set
-- @treturn World self
function World:setResource(name, resource)
self.__resources[name] = resource
return self
end
---- Gets a named resource from the world
+--- Gets a named resource from the World.
-- @string name Name of the resource
--- @treturn Any resource
+-- @return The resource, or nil if none was set
function World:getResource(name)
return self.__resources[name]
end
return setmetatable(World, {
__call = function(_, ...)
- ---@diagnostic disable-next-line: redundant-parameter
+ -- LuaLS: redundant-parameter (World.new takes no arguments)
return World.new(...)
end,
})
diff --git a/config.ld b/config.ld
index 9b7ea55..afc2f6d 100644
--- a/config.ld
+++ b/config.ld
@@ -1,4 +1,21 @@
project = 'Concord'
description = 'A feature-complete ECS library'
-file = {'concord', exclude = {}}
+file = {'concord', exclude = {'concord/init.lua', 'concord/builtins/init.lua'}}
dir = 'docs'
+
+format = 'markdown'
+use_markdown_titles = true
+
+new_type("builtin", "Builtins", true)
+kind_names = {topic = 'Guides'}
+
+-- Preprocessed by scripts/build-docs.lua
+readme = {
+ '.ldoc-guides/Get-Started.md',
+ '.ldoc-guides/Why-ECS.md',
+ '.ldoc-guides/Custom-Pools.md',
+ '.ldoc-guides/Pool-Flushing-and-Timing.md',
+}
+
+style = '.'
+
diff --git a/docs/builtins/key.html b/docs/builtins/key.html
new file mode 100644
index 0000000..e55ab93
--- /dev/null
+++ b/docs/builtins/key.html
@@ -0,0 +1,170 @@
+
+
+
+
+ Validates a Filter Definition to make sure every component is valid.
+ Also checks that def.constructor is callable when present, unless onComponent is provided.
+
+
+
Parameters:
+
+
errorLevel
+ integer
+ Extra stack level added to error reports
+
+
name
+ string
+ Name for the Filter. If omitted, errors refer to a World:query filter.
+ (optional)
+
+
def
+ table
+ Table containing the Filter Definition
+
+
onComponent
+ function
+ Called with (component, reject) when a component is valid.
+ reject is the stripped Component name when the entry had the Reject Prefix, otherwise false.
+ (optional)
+
+
+
+
+
+
+
+
+
+
+ filter:parse ([name], def)
+
+
+ Parses a Filter Definition into a single interleaved table.
+ Entries are stored as name, required pairs: {name, true, name, false, ...}
+ where true means the Component is required and false means it is rejected.
+
+
+
Parameters:
+
+
name
+ string
+ Name for the Filter.
+ (optional)
+
+
def
+ table
+ Table containing the Filter Definition
+
+ If true, new Entities are given a "serializable" Component.
+
+
+
+
+
+
+
+
+
Methods
-
- Entity:new ([world])
+
+ entity:new ([world])
- Creates a new Entity. Optionally adds it to a World.
+ Creates a new Entity. Optionally queues it to be added to a World.
+ If Entity.SERIALIZEBYDEFAULT is true, the Entity is given a "serializable" Component.
Gives an Entity a Component.
- If the Component already exists, it's overridden by this new Component
+ If the Component already exists, it's overridden by this new Component.
Parameters:
-
componentClass
- Component
- ComponentClass to add an instance of
+
name
+ string or Component
+ Name of the ComponentClass, or a Component instance to copy onto the Entity
...
- additional arguments to pass to the Component's populate function
+ additional arguments to pass to the Component's populate function (ignored when copying a Component)
- Ensures an Entity to have a Component.
- If the Component already exists, no action is taken
+ Ensures an Entity has a Component.
+ If the Component already exists, no action is taken.
Parameters:
-
componentClass
- Component
- ComponentClass to add an instance of
+
name
+ string or Component
+ Name of the ComponentClass, or a Component instance to copy onto the Entity
...
- additional arguments to pass to the Component's populate function
+ additional arguments to pass to the Component's populate function (ignored when copying a Component)
Destroys the Entity.
- Removes the Entity from its World if it's in one.
+ Queues removal from its World if it's in one. The Entity stays in the World until the next flush.
- Returns a table of all Components the Entity has.
- Warning: Do not modify this table.
- Use Entity:give/ensure/remove instead
+ Returns a shallow copy of the Entity's fields, excluding __world and __isEntity.
+ This is primarily the Entity's Components. Extra non-component fields are also copied.
+ Mutating a copied Component still mutates the Entity's Component.
+
Parameters:
+
+
output
+ table
+ Table to copy into. A new table is created if omitted.
+ (optional)
+
+
Returns:
- table
- Table of all Components the Entity has
+ table
+
+
+
@@ -362,11 +422,12 @@
-
- Entity:inWorld ()
+
+ entity:inWorld ()
- Returns true if the Entity is in a World.
+ Returns true if the Entity has been assigned a World.
+ True as soon as addition is queued, and remains true until removal is flushed.
@@ -375,6 +436,8 @@
boolean
+
+
@@ -382,8 +445,8 @@
-
- Entity:getWorld ()
+
+ entity:getWorld ()
Returns the World the Entity is in.
@@ -395,20 +458,70 @@
World
+
+
+
+
+
+ entity:serialize ([ignoreKey])
+
+
+ Serializes the Entity's Components.
+ Components that return nil from serialize are omitted.
+
+
+
Parameters:
+
+
ignoreKey
+ boolean
+ If true, the key Component is omitted
+ (optional)
+