diff --git a/docs/index.html b/docs/index.html index 5360958..c20553c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -108,7 +108,7 @@
generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
diff --git a/docs/modules/asset.html b/docs/modules/asset.html index bd62940..ebc5242 100644 --- a/docs/modules/asset.html +++ b/docs/modules/asset.html @@ -334,7 +334,7 @@
generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
diff --git a/docs/modules/ecs.html b/docs/modules/ecs.html index 93081ae..e93bbe2 100644 --- a/docs/modules/ecs.html +++ b/docs/modules/ecs.html @@ -1597,7 +1597,7 @@ its sibling systems (i.e. completely stop the propagation of the event).
generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
diff --git a/docs/modules/ldtk.html b/docs/modules/ldtk.html index 78164a0..15a6ac7 100644 --- a/docs/modules/ldtk.html +++ b/docs/modules/ldtk.html @@ -1754,7 +1754,7 @@ end Level background.

If there is a background image, background.image contains a table {image=image, x=number, y=number, sx=number, sy=number} - where image is the LÖVE image (or image filepath if LÖVE not available) x and y are the top-left position, + where image is the LÖVE image (or image filepath if LÖVE not available) x and y are the top-left position, and sx and sy the horizontal and vertical scale factors. @@ -1829,7 +1829,7 @@ end

  • Enum are converted into a Lua string giving the currently selected enum value.
  • Filepath are converted into a Lua string giving the file path.
  • Arrays are converted into a Lua table with the elements in it as a list.
  • -
  • Points are converted into a Lua table with the fields x and y: { x=number, y=number }.
  • +
  • Points are converted into a Lua table with the fields x and y: { x=number, y=number }.
  • Colors are converted into a Lua table with the red, green and blue components in [0-1] as a list: {r,g,b}.
  • @@ -1855,7 +1855,7 @@ end
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/modules/scene.html b/docs/modules/scene.html index 079971f..043130d 100644 --- a/docs/modules/scene.html +++ b/docs/modules/scene.html @@ -702,7 +702,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/modules/signal.html b/docs/modules/signal.html index 46a3bc6..8caf569 100644 --- a/docs/modules/signal.html +++ b/docs/modules/signal.html @@ -414,7 +414,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/modules/timer.html b/docs/modules/timer.html index d170bd9..48ec8f3 100644 --- a/docs/modules/timer.html +++ b/docs/modules/timer.html @@ -1153,7 +1153,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/modules/ubiquitousse.html b/docs/modules/ubiquitousse.html index a653206..96b312a 100644 --- a/docs/modules/ubiquitousse.html +++ b/docs/modules/ubiquitousse.html @@ -228,6 +228,7 @@
    Input management, if available. + TODO: not currently generated with LDoc. @@ -361,7 +362,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/modules/util.html b/docs/modules/util.html index 84ca62e..ab95eb3 100644 --- a/docs/modules/util.html +++ b/docs/modules/util.html @@ -784,7 +784,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/topics/LICENSE.html b/docs/topics/LICENSE.html index c9df972..79ece54 100644 --- a/docs/topics/LICENSE.html +++ b/docs/topics/LICENSE.html @@ -64,7 +64,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/docs/topics/README.md.html b/docs/topics/README.md.html index 312b394..6dd1a70 100644 --- a/docs/topics/README.md.html +++ b/docs/topics/README.md.html @@ -71,7 +71,7 @@
    generated by LDoc 1.4.6 -Last updated 2021-12-26 18:13:01 +Last updated 2021-12-26 18:43:30
    diff --git a/ecs/callback.can b/ecs/callback.can deleted file mode 100644 index dcd62f0..0000000 --- a/ecs/callback.can +++ /dev/null @@ -1,35 +0,0 @@ ---- Callback system --- Allow to call callbacks defined in other systems in entity methods. --- Example: --- entity.callback:onMove(...) -- call onAdd(system, entitySystemTable, ...) on every system the entity belong to (which has this callback) --- Can be also used for onAdd, etc. - ---- Recursively get a list of systems with a certain method. -let recGetSystemsWithMethod = (method, systems, l={}) - for _, s in ipairs(systems) do - if s[method] then - table.insert(l, s) - end - recGetSystemsWithMethod(method, s.systems, l) - end - return l -end - -return { - name = "callback", - filter = true, - methods = { - __index = :(c, k) - let s = recGetSystemsWithMethod(k, {@world}) - @methods[k] = :(c, ...) - let e = c.entity - for _, sys in ipairs(s) do - if sys._previous[e] then - sys[k](sys, e[sys.name], ...) - end - end - end - return @_methods_mt[k] - end - } -} diff --git a/ecs/children.can b/ecs/children.can index b7fa482..76b37dd 100644 --- a/ecs/children.can +++ b/ecs/children.can @@ -7,43 +7,34 @@ return { filter = true, default = { parent = nil, -- reference to parent entity, if any - -- ... list of children + -- ... list of children to add when the entity is added to the world + children = {}, -- [children]=true,children... map+list of children currently in the entity children (don't set this yourself) }, - methods = { - --- Add a new entity to the world, using this entity as a parent. - add = :(c, o) - if not o.children then - o.children = {} - end - o.children.parent = c.entity - table.insert(c, o) - @world:add(o) - end, - --- Remove an entity from the world and from this entity's children. - remove = :(c, o) - @world:remove(o) - for i=#c, 1, -1 do - if c[i] == o then - table.remove(c, i) - break - end - end - o.children.parent = nil + onAdd = :(c, e) + if c.parent then -- add to parent + let parentcc = c.parent.children.children + table.insert(parentcc, e) + parentcc[e] = true end - }, - onAdd = :(c) - for _, o in ipairs(c) do - o.children.parent = c.entity + for _, o in ipairs(c) do -- add children + if not o.children then o.children = {} end + o.children.parent = e @world:add(o) end end, - onRemove = :(c) - for _, o in ipairs(c) do - @world:remove(o) - o.children.parent = nil + onRemove = :(c, e) + for i=#c.children, 1, -1 do -- remove children + @world:remove(c.children[i]) end - if c.parent then - c.parent.children:remove(c.entity) + if c.parent then -- remove from parent + let parentcc = c.parent.children.children + for i=#parentcc, 1, -1 do + if parentcc[i] == e then + table.remove(parentcc, i) + break + end + end + parentcc[e] = nil end end } diff --git a/ecs/ecs.can b/ecs/ecs.can index 3a9cbd4..fcae56e 100644 --- a/ecs/ecs.can +++ b/ecs/ecs.can @@ -1,42 +1,101 @@ ---- ECS (entity compenent system) library --- --- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include: --- --- * ability to nest systems; --- * instanciation of systems for each world (no shared state); --- * adding and removing entities is done instantaneously --- * ability to add and remove components from entities after they were added to the world. --- --- No mandatory dependency. --- Optional dependency: `ubiquitousse.scene`, to allow quick creation of ECS-based scenes. --- --- The module returns a table that contains several functions, `world` or `scene` are starting points --- to create your world. --- --- @module ecs --- @usage TODO -local loaded, scene = pcall(require, (...):match("^(.-)ecs").."scene") +--[[-- ECS ([entity compenent system](https://en.wikipedia.org/wiki/Entity_component_system)) library for Lua. + +Entity Component System library, inspired by the excellent [tiny-ecs](https://github.com/bakpakin/tiny-ecs/tree/master) by bakpakin. +Main differences include: + +* ability to nest systems (more organisation potential); +* instanciation of systems for each world (no shared state) (several worlds can coexist at the same time easily); +* adding and removing entities is done instantaneously (no going isane over tiny-ecs cache issues); +* ability to add and remove components from entities after they were added to the world (more dynamic entities). + +And a fair amount of other quality-of-life features. + +The goals of this library are similar in spirit to tiny-ecs: simple to use, flexible, and useful. +The more advanced features it provides relative to tiny-ecs are made so that you can completely ignore them +if you don't use them. + +The module returns a table that contains several functions, `world` or `scene` are starting points +to create your world. + +No mandatory dependency. +Optional dependency: `ubiquitousse.scene`, to allow quick creation of ECS-based scenes (`ecs.scene`). + +@module ecs +@usage +-- Same example as tiny-ecs', for comparaison purposes + +local ecs = require("ubiquitousse.ecs") + +local talkingSystem = { + filter = { "name", "mass", "phrase" }, + process = function(self, c, e, dt) + e.mass = e.mass + dt * 3 + print(("%s who weighs %d pounds, says %q."):format(e.name, e.mass, e.phrase)) + end +} + +local joe = { + name = "Joe", + phrase = "I'm a plumber.", + mass = 150, + hairColor = "brown" +} + +local world = ecs.world(talkingSystem) +world:add(joe) + +for i = 1, 20 do + world:update(1) +end +--]] +local loaded, scene +if ... then loaded, scene = pcall(require, (...):match("^(.-)ecs").."scene") end if not loaded then scene = nil end let ecs -- TODO: Implement a skip list for faster search. -- better control over system order: process, draw, methods? (for lag reasons and dependencies) --- more generic events? --- TODO: :reorder (like refresh but update order) +--- Entities are regular tables that get processed by `System`s. +-- +-- The idea is that entities _should_ only contain data and no code; it's the systems that are responsible for the actual processing +-- (but it's your game, do as you want). +-- +-- This data is referred to, and organized in, "components". +-- @type Entity --- TODO: clarify documentation on entity tables and instanciated system table +--[[-- Components. ---- Entity table. --- TODO --- @section Entity +Entities are Lua tables, and thus contain key-values pairs: each one of these pairs is called a "component". The data can be +whatever you want, and ideally each component _should_ store the data for one singular aspect of the entity, for example its position, name, etc. ---- Entity system table. --- @doc entity +This library does not do any kind of special processing by itself on the entity tables and take them as is (no metatable, no methamethods, etc.), +so you are free to handle them as you want in your systems or elsewhere. ---- @table entity --- @field .. d +Since it's relatively common for systems to only operate on a single component, as a shortcut the library often consider what it calls the "system component": +that is, the component in the entity that has the same name as the system (if it exists). + +@doc Component +@usage +-- example entity +local entity = { + position = { x = 0, y = 52 }, -- a "position" component + sprite = newSprite("awesomeguy.png") -- a "sprite" component +} + +-- example "sprite" system +local sprite = { + name = "sprite", + filter = "sprite", -- process entities that have a "sprite" component + -- systems callbacks that are called per-entity often give you the system component as an argument + -- the system component is the component with the same name as the system, thus here the sprite component + render = function(self, component, entity) + -- component == entity.sprite + component:draw() + end +} +]]-- --- Recursively remove subsystems from a system. let recDestroySystems = (system) @@ -84,7 +143,7 @@ let copy = (a, b, cache={}) copy(v, b[k], cache) setmetatable(b[k], getmetatable(v)) end - elseif b[k] == "table" then + elseif type(b[k]) == "table" then copy(v, b[k], cache) end elseif b[k] == nil then @@ -93,23 +152,77 @@ let copy = (a, b, cache={}) end end ---- System fields and methods. --- --- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system"). --- Instancied systems can be retrieved in `system.s` or `system.systems`. --- --- The "world" is just the top-level system, behaving in exactly the same way as other systems. --- --- Every field defined below is optional and can be accessed or redefined at any time, unless written otherwise. Though you would typically set them --- when creating your system. --- @type System +--[[-- Systems and Worlds. +Systems are what do the processing on your entities. A system contains a list of entities; the entities in this list are selected +using a `filter`, and the system will only operate on those filtered entities. + +A system can also be created that do not accept any entity (`filter = false`, this is the default): such a system can still be +used to do processing that don't need to be done per-entity but still behave like other systems (e.g. to do some static calculation each update). + +The system also contains `callbacks`, these define the actual processing done on the system and its entities and you will want to redefine +at least one of them to make your system actually do something. + +Then you can call `System:update`, `System:draw`, `System:emit` or `System:callback` at appropriate times and the system will call the +associated callbacks on itself and its entities, and then pass it to its subsystems. In practise you would likely only call these on +the world system, so the callbacks are correctly propagated to every single system in the world. + +Systems are defined as regular tables with all the fields and methods you need in it. However, when a system is added to +a world, the table you defined is not used directly, but we use what we call an "instancied system": think of it of an instance of your system +like if it were a class. +The instancied system will have a metatable set that gives it some methods and fields defined by the library on top of what you defined. +Modifying the instancied system will only modify this instance and not the original system you defined, so several instances of your system +can exist in different worlds (note that the original system is not copied on instancing; if you reference a table in the original system it will use the +original table directly). + +Systems can have subsystems; that is a system that behave as an extension of their parent system. They only operates on the entities already +present in their parent subsystem, only update when their parent system updates, etc. You can thus organize your systems in a hierarchy to +avoid repeating your filters or allow controlling several system from a single parent system. + +The top-level system is called the "world"; it behaves in exactly the same way as other systems, and accept every entity by default. + +@type System +@usage +local sprite = { + filter = { "sprite", "position" }, -- only operate on entities with "sprite" and "position" components + systems = { animated }, -- subsystems: they only operate on entities already filtered by this system (on top of their own filtering) + + -- Called when an entity is added to this system. + onAdd = function(self, component, entity) + print("Added an entity, entity count in the system:", self.entityCount) -- self refer to the instancied system + end, + + -- Called when the system is updated, for every entity the system + process = function(self, component, entity, dt) + -- processing... + end +} + +local world = ecs.world(system) -- instanciate a world with the sprite system (and all its subsystems) + +-- Add an entity: doesn't pass the filtering, so nothing happens +world:add { + name = "John" +} + +-- Added to the sprite system! Call sprite:onAdd, and also try to add it to its subsystems +world:add { + sprite = newSprite("example.png"), + position = { x=5, y=0 } +} + +-- Trigger sprite:onUpdate and sprite:process callbacks +world:update(dt) +--]] let system_mt = { - --- Modifiable fields + --- Modifiable fields. + -- + -- Every field defined below is optional and can be accessed or redefined at any time, unless written otherwise. Though you would typically set them + -- before instanciating your systems. -- @doc modifiable --- Name of the system. - -- Used to create a field with the system's name in `world.s` and into each entity (the "entity's system table") that's in this system. - -- If not set, the entity will not have a system table. + -- Used to create a field with the system's name in `world.s` and determine the associated system component. + -- If not set, the system will not appear in `world.s` and gives `nil` instead of the system component in callbacks. -- -- Do not change after system instanciation. -- @ftype string @@ -124,37 +237,30 @@ let system_mt = { -- @ftype nil if no subsystem systems = nil, - --- If not false, the system will only update every interval seconds. + --- If not `false`, the system will only update every interval seconds. -- `false` by default. -- @ftype number interval of time between each update -- @ftype false to disable interval = false, - --- The system and its susbsystems will only update if this is true. + --- The system and its susbsystems will only update if this is `true`. -- `true` by default. -- @ftype boolean active = true, - --- The system and its subsystems will only draw if this is true. + --- The system and its subsystems will only draw if this is `true`. -- `true` by default. -- @ftype boolean visible = true, - --- Defaults value to put into the entities's system table when they are added. Will recursively fill missing values. + --- Defaults value to put into the entities's system component when they are added. + -- + -- If this is table, will recursively fill missing values. -- Metatables will be preserved during the copy but not copied themselves. -- - -- When an entity is added to a system, a `.entity` field is always set in the system table, referring to the full entity table. - -- -- Changing this will not affect entities already in the system. - -- @ftype table + -- Doesn't have any effect if the system doesn't have a name. + -- @ftype any -- @ftype nil if no default default = nil, - --- Defaults methods to assign to the entities's system table when they are added. - -- - -- When calling the methods with `entity.systemName:method(...)`, the method will actually receive the - -- arguments method(system, `system table, ...)`. Methamethods are accepted. New methods can be - -- created anytime. - -- @ftype table - -- @ftype nil if no methods - methods = nil, --- Callbacks. -- @@ -163,40 +269,42 @@ let system_mt = { -- @doc callbacks --- Called when checking if an entity should be added to this system. - -- Returns true if the entity should be added to this system (and therefore its subsystems). + -- Returns `true` if the entity should be added to this system (and therefore its subsystems). -- -- If this is a string or a table, it will be converted to a filter function on instanciation using ecs.any. -- - -- If this true, will accept every entity; if false, reject every entity. + -- If this `true`, will accept every entity; if `false`, reject every entity. -- -- Will only test entities when they are added; changing this after system creation will not affect entities already in the system. -- -- By default, rejects everything. -- @callback -- @tparam table e entity table to check - -- @treturn boolean true if entity should be added + -- @treturn boolean `true` if entity should be added filter = :(e) return false end, --- Called when adding an entity to this system determining its order. - -- Returns true if e1 <= e2. e1 and e2 are two entities. + -- Returns `true` if `e1 <=` e2 (i.e., if `e1` should be processed before `e2` in this system). e1 and e2 are two entities. -- -- Used to place the entity in the sorted entity list when it is added; changing this after system creation -- will not change the order of entities already in the system. -- - -- By default, entities are in the same order they were inserted. + -- By default, new entities are added at the start of the list. -- @callback - -- @tparam table e1 entity table to check for inferiority - -- @tparam table e2 entity table to check for superiority - -- @treturn boolean true if e1 <= e2 + -- @tparam Entity e1 entity table to check for inferiority + -- @tparam Entity e2 entity table to check for superiority + -- @treturn boolean `true` if e1 <= e2 compare = :(e1, e2) return true end, --- Called when adding an entity to the system. -- @callback - -- @tparam table s the entity's system table - onAdd = :(s) end, + -- @tparam Component c the entity's system component + -- @tparam Entity e the entity table + onAdd = :(c, e) end, --- Called when removing an entity from the system. -- @callback - -- @tparam table s the entity's system table - onRemove = :(s) end, + -- @tparam Component c the entity's system component + -- @tparam Entity e the entity table + onRemove = :(c, e) end, --- Called when the system is instancied, before any call to `System:onAddToWorld` (including other systems in the world). -- @callback onInstance = :() end, @@ -220,21 +328,29 @@ let system_mt = { onDraw = :() end, --- Called when updating the system, for every entity the system contains. Called after `System:onUpdate` was called on the system. -- @callback - -- @tparam table s the entity's system table + -- @tparam Component c the entity's system component + -- @tparam Entity e the entity table -- @number dt delta-time since last update - process = :(s, dt) end, + process = :(c, e, dt) end, --- Called when drawing the system, for every entity the system contains. Called after `System:onDraw` was called on the system. -- @callback - -- @tparam table s the entity's system table - render = :(s) end, + -- @tparam Component c the entity's system component + -- @tparam Entity e the entity table + render = :(c, e) end, - --- Read-only fields + --- Read-only fields. + -- + -- Fields available on instancied systems. Don't modify them unless you like broken things. -- @doc ro --- The world the system belongs to. -- @ftype System world -- @ro world = nil, + --- Shortcut to `System.world`. + -- @ftype System world + -- @ro + w = nil, --- Number of entities in the system. -- @ftype integer -- @ro @@ -249,22 +365,17 @@ let system_mt = { --- First element of the linked list of entities: { entity, next_element }. -- @local _first = nil, - --- Associative map of entities in the system and their previous linked list element (or true if first element). + --- Associative map of entities in the system and their previous linked list element (or `true` if first element). -- This make the list effectively a doubly linked list, but with easy access to the previous element using this map (and therefore O(1) deletion). -- @local _previous = nil, --- Amount of time waited since last update (if interval is set). -- @local _waited = 0, - --- Metatable of entities' system table. - -- Contains the methods defined in the methods field, wrapped to be called with the correct arguments, as well as a - -- __index field (if not redefined in methods). - -- @local - _methods_mt = nil, --- Methods. -- - -- Methods available on instancied system table. + -- Methods available on instancied systems. -- @doc smethods --- Add entities to the system and its subsystems. @@ -274,22 +385,18 @@ let system_mt = { -- Entities are added to subsystems after they were succesfully added to their parent system. -- -- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters. - -- - -- Since `System:remove` will not search for entities in systems where they should have been filtered out, the added entities will not be removed - -- when calling :remove on a parent system or the world. The entity can be removed by calling `System:remove` on the system `System:add` was called on. + -- If you do that, since `System:remove` will not search for entities in systems where they should have been filtered out, the added entities will not be removed + -- when calling `System:remove` on a parent system or the world. The entity can be removed by calling `System:remove` on the system `System:add` was called on. -- -- Complexity: O(1) per unordered system, O(entityCount) per ordered system. - -- @tparam table e entity to add - -- @tparam table... ... other entities to add - -- @treturn e,... the function arguments + -- @tparam Entity e entity to add + -- @tparam Entity... ... other entities to add + -- @treturn Entity,... `e,...` the function arguments add = :(e, ...) if e ~= nil and not @_previous[e] and @filter(e) then - -- setup entity - if @name then - if not e[@name] e[@name] = {} - if @default copy(@default, e[@name]) - if @methods setmetatable(e[@name], @_methods_mt) - e[@name].entity = e + -- copy default system component + if @name and @default then + copy({ [@name] = @default }, e) end -- add to linked list if @_first == nil then @@ -319,10 +426,12 @@ let system_mt = { end -- notify addition @entityCount += 1 - @onAdd(e[@name]) - -- add to subsystems - for _, s in ipairs(@systems) do - s:add(e) + @onAdd(e[@name], e) + -- add to subsystems (if it wasn't immediately removed in onAdd) + if @_previous[e] then + for _, s in ipairs(@systems) do + s:add(e) + end end end if ... then @@ -331,36 +440,7 @@ let system_mt = { return e end end, - --- Refresh an entity's systems. - -- - -- Behave similarly to `System:add`, but if the entity is already in the system, instead of skipping it, it - -- will check for new and removed components and add and remove from (sub)systems accordingly. - -- - -- Complexity: O(1) per system + add/remove complexity. - -- @tparam table e entity to refresh - -- @tparam table... ... other entities to refresh - -- @treturn e,... the function arguments - refresh = :(e, ...) - if e ~= nil then - if not @_previous[e] then - @add(e) - elseif @_previous[e] then - if not @filter(e) then - @remove(e) - else - for _, s in ipairs(@systems) do - s:refresh(e) - end - end - end - end - if ... then - return e, @refresh(...) - else - return e - end - end, - --- Remove entities to the system and its subsystems. + --- Remove entities from the system and its subsystems. -- -- Will skip entities that are not in the system. -- @@ -368,12 +448,10 @@ let system_mt = { -- -- If you intend to call this on a subsystem instead of the world, please read the warning in `System:add`. -- - -- Returns all removed entities. - -- -- Complexity: O(1) per system. - -- @tparam table e entity to remove - -- @tparam table... ... other entities to remove - -- @treturn e,... the function arguments + -- @tparam Entity e entity to remove + -- @tparam Entity... ... other entities to remove + -- @treturn Entity,... `e,...` the function arguments remove = :(e, ...) if e ~= nil then if @_previous[e] then @@ -399,7 +477,7 @@ let system_mt = { -- notify removal @_previous[e] = nil @entityCount -= 1 - @onRemove(e[@name]) + @onRemove(e[@name], e) end end if ... then @@ -408,12 +486,96 @@ let system_mt = { return e end end, - --- Returns true if every entity is in the system. + --- Refresh an entity's systems. + -- + -- Behave similarly to `System:add`, but if the entity is already in the system, instead of skipping it, it + -- will check for new and removed components and add and remove from (sub)systems accordingly. + -- + -- Complexity: O(1) per system + add/remove complexity. + -- @tparam Entity e entity to refresh + -- @tparam Entity... ... other entities to refresh + -- @treturn Entity,... `e,...` the function arguments + refresh = :(e, ...) + if e ~= nil then + if not @_previous[e] then + @add(e) + elseif @_previous[e] then + if not @filter(e) then + @remove(e) + else + for _, s in ipairs(@systems) do + s:refresh(e) + end + end + end + end + if ... then + return e, @refresh(...) + else + return e + end + end, + --- Reorder an entity. + -- + -- Will recalculate the entity position in the entity list for this system and its subsystems. + -- Will skip entities that are not in the system. + -- + -- Complexity: O(entityCount) per system. + -- @tparam Entity e entity to reorder + -- @tparam Entity... ... other entities to reorder + -- @treturn Entity,... `e,...` the function arguments + reorder = :(e, ...) + if e ~= nil then + if @_previous[e] then + let prev = @_previous[e] -- { prev, { e, next } } + let next = prev == true and @_first[2] or prev[2][2] + -- remove e from linked list + if prev == true then + @_first = @_first[2] + else + prev[2] = next + end + if next then + @_previous[next[1]] = prev + end + -- find position so that prev < e <= next + while prev ~= true and @compare(e, prev[1]) do -- ensure prev < e + next = prev + prev = @_previous[prev[1]] + end + while next ~= nil and not @compare(e, next[1]) do -- ensure e <= next + prev = next + next = next[2] + end + -- reinsert e in linked list + let new = { e, next } + @_previous[e] = prev + if next then + @_previous[next[1]] = new + end + if prev == true then + @_first = new + else + prev[2] = new + end + -- Reorder in subsystems + for _, s in ipairs(@systems) do + s:reorder(e) + end + end + end + if ... then + return e, @reorder(...) + else + return e + end + end, + --- Returns `true` if all these entities are in the system. -- -- Complexity: O(1). - -- @tparam table e entity that may be in the system - -- @tparam table... ... other entities that may be in the system - -- @treturn boolean true if every entity is in the system + -- @tparam Entity e entity that may be in the system + -- @tparam Entity... ... other entities that may be in the system + -- @treturn boolean `true` if every entity is in the system has = :(e, ...) let has = e == nil or not not @_previous[e] if ... then @@ -422,8 +584,8 @@ let system_mt = { return has end end, - --- Returns an iterator that iterate through the entties in this system. - -- @treturn iterator iterator over the entities in this system, in order + --- Returns an iterator that iterate through the entties in this system, in order. + -- @treturn iterator iterator over the entities in this system iter = :() return nextEntity, { @_first } end, @@ -451,7 +613,7 @@ let system_mt = { @onUpdate(dt) if @process ~= system_mt.process then for e in @iter() do - @process(e[@name], dt) + @process(e[@name], e, dt) end end for _, s in ipairs(@systems) do @@ -470,7 +632,7 @@ let system_mt = { @onDraw() if @render ~= system_mt.render then for e in @iter() do - @render(e[@name]) + @render(e[@name], e) end end for _, s in ipairs(@systems) do @@ -478,16 +640,74 @@ let system_mt = { end end end, + --- Trigger a custom callback on a single entity. + -- + -- This will call the `System:name(c, e, ...)` method in this system and its subsystems, + -- if the method exists and the entity is in the system. `c` is the system [component](#Entity.Component) + -- associated with the current system, and `e` is the `Entity`. + -- + -- Think of it as a way to perform custom callbacks issued from an entity event, similar to `System:onAdd`. + -- @tparam string name name of the callback + -- @tparam Entity e the entity to perform the callback on + -- @param ... other arguments to pass to the callback + callback = :(name, e, ...) + -- call callback + if @_previous[e] and @[name] then + @[name](@, e[@name], e, ...) + end + -- callback on subsystems (if it wasn't removed during the callback) + if @_previous[e] then + for _, ss in ipairs(@systems) do + ss:callback(name, e, ...) + end + end + end, + --- Emit an event on the system. + -- + -- This will call the `System:name(...)` method in this system and its subsystems, + -- if the method exists. + -- + -- Think of it as a way to perform custom callbacks issued from a general event, similar to `System:onUpdate`. + -- + -- The called methods may return a string value to affect the event propagation behaviour: + -- + -- * if a callback returns `"stop"`, the event will not be propagated to the subsystems. + -- * if a callback returns `"capture"`, the event will not be propagated to the subsystems _and_ + -- its sibling systems (i.e. completely stop the propagation of the event). + -- + -- `"stop"` would be for example used to disable some behaviour in the system and its subsystems (like `active = false` can + -- disable `System:onUpdate` behaviour on the system and its subsystems). + -- + -- `"capture"` would be for example used to prevent other systems from handling the event (for example to make sure an + -- input event is handled only once by a single system). + -- + -- @tparam string name name of the callback + -- @param ... other arguments to pass to the callback + emit = :(name, ...) + -- call event + let status + if @[name] then + status = @[name](@, ...) + end + -- call event on subsystems (if it wasn't stopped or captured) + if status ~= "stop" and status ~= "capture" then + for _, s in ipairs(@systems) do + status = s:emit(name, ...) + if status == "capture" then break end + end + end + return status + end, --- Remove all the entities and subsystems in this system. destroy = :() recCallOnRemoveFromWorld(@world, { @ }) recDestroySystems({ systems = { @ } }) - end + end, } --- Self descriptive let alwaysTrue = () return true end -let alwaysFalse = () return true end +let alwaysFalse = () return false end --- Recursively instanciate a list of systems for a world: -- * create their self table with instance fields set @@ -496,31 +716,13 @@ let recInstanciateSystems = (world, systems) let t = {} for _, s in ipairs(systems) do let system - -- setup method table - let methods_mt = {} - if s.methods then - methods_mt.__index = methods_mt - for k, v in pairs(s.methods) do - methods_mt[k] = :(...) - return v(system, @, ...) - end - end - setmetatable(s.methods, { - __newindex = :(k, v) - rawset(@, k, v) - methods_mt[k] = :(...) - return v(system, @, ...) - end - end - }) - end -- instanciate system system = setmetatable({ systems = recInstanciateSystems(world, s.systems or {}), world = world, + w = world, s = world.s, _previous = {}, - _methods_mt = methods_mt }, { __index = :(k) if s[k] ~= nil then @@ -563,7 +765,7 @@ end ecs = { --- Create and returns a world system based on a list of systems. -- The systems will be instancied for this world. - -- @tparam table,... ... list of (uninstancied) system tables + -- @tparam table,... ... list of (uninstancied) systems -- @treturn System the world system world = (...) let world = setmetatable({ @@ -572,14 +774,15 @@ ecs = { _previous = {} }, { __index = system_mt }) world.world = world + world.w = world world.systems = recInstanciateSystems(world, {...}) recCallOnAddToWorld(world, world.systems) return world end, - --- Returns a filter that returns true if, for every argument, a field with the same name exists in the entity. + --- Returns a filter that returns `true` if, for every argument, a field with the same name exists in the entity. -- @tparam string,... ... list of field names that must be in entity - -- @treturn function(e) that returns true if e has all the fields + -- @treturn function(e) that returns `true` if e has all the fields all = (...) if ... then let l = {...} @@ -596,9 +799,9 @@ ecs = { end end, - --- Returns a filter that returns true if one of the arguments if the name of a field in the entity. + --- Returns a filter that returns `true` if one of the arguments if the name of a field in the entity. -- @tparam string,... ... list of field names that may be in entity - -- @treturn function(e) that returns true if e has at leats one of the fields + -- @treturn function(e) that returns `true` if e has at leats one of the fields any = (...) if ... then let l = {...} diff --git a/init.lua b/init.lua index 6c91ec6..84f986a 100644 --- a/init.lua +++ b/init.lua @@ -58,6 +58,7 @@ ubiquitousse = { -- @see ecs ecs = nil, --- Input management, if available. + -- TODO: not currently generated with LDoc. -- @see input input = nil, --- LDtk level import, if available.