--[[-- 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); * much better performance for ordered systems (entities are stored in a skip list internally). 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. This library was designed to be reasonably fast; on my machine using LuaJIT, in the duration of a frame (1/60 seconds) about 40000 entities can be added to an unordered system or 8000 to an ordered system. Complexities are documented for each function. 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, e, c, 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: better control over system order: process, draw, methods? (for lag reasons and dependencies) --- 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 --[[-- Components. 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. 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. 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 is named like `System.component` (or `System.name` if it is not set). Though there's no problem if there's no system component or if it doesn't exist in the entity. @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, entity, component) -- component == entity.sprite draw(component) end } ]]-- --- Recursively remove subsystems from a system. let recDestroySystems = (system) for i=#system.systems, 1, -1 do let s = system.systems[i] recDestroySystems(s) s:onDestroy() system.systems[i] = nil if s.name then system.world.s[s.name] = nil end end end --- Recursively call :clear and :onRemoveFromWorld to a list of systems in a world. let recCallOnRemoveFromWorld = (world, systems) for _, s in ipairs(systems) do s:clear() recCallOnRemoveFromWorld(world, s.systems) s:onRemoveFromWorld(world) end end --- Recursively copy content of a into b if it isn't already present. -- Don't copy keys, will preserve metatable but not copy them. let copy = (a, b, cache={}) for k, v in pairs(a) do if type(v) == "table" then if b[k] == nil then if cache[v] then b[k] = cache[v] else cache[v] = {} b[k] = cache[v] copy(v, b[k], cache) setmetatable(b[k], getmetatable(v)) end elseif type(b[k]) == "table" then copy(v, b[k], cache) end elseif b[k] == nil then b[k] = v end end end --- Self descriptive let alwaysTrue = () return true end let alwaysFalse = () return false end --- Skip list implementation --- -- Well technically it's a conbination of a skip list (for ordering) and a hash map (for that sweet O(1) search). Takes more memory but oh so efficient. --- Special value used as the first element of each linked list. let head = {} --- Create a new linked list. let skipNew = () let s = { --- First element of the highest layer linked list of entities: { entity, next_element, element_in_lower_layer }. -- The default entity `head` is always added as a first element to simplify algorithms; remember to skip it. first = { head, nil }, --- First element of the base layer. firstBase = nil, --- List of hash map (one per skip listlayer) of entities in the system and their previous linked list element. -- Does not contain a key for the `head` entity. -- This make each linked list layer effectively a doubly linked list, but with fast access to the previous element using this map (and therefore O(1) deletion). previous = { {} }, --- Number of layers in the skip list. nLayers = 1, --- Number of elements in the skip list. n = 0 } s.firstBase = s.first return s end --- Iterate through the next entity in a linked list, based on state s: { previousLinkedListItem } let nextEntity = (s) if s[1] then let var = s[1][1] s[1] = s[1][2] return var else return nil end end --- Returns an iterator over all the elements of the skip list. -- Complexity: O(n) like expected let skipIter = :() return nextEntity, { @firstBase[2] } end --- Add new layers (if needed) in order to target O(log2(n)) complexity for skip list operations. -- i.e. add layers until we have log2(n) layers -- Complexity: O(log2(n)) if you never called it before, but you should call it with every insert for O(1) let skipAddLayers = :() while @n > 2^@nLayers do @first = { head, nil, @first } table.insert(@previous, {}) @nLayers += 1 end end --- 1/2 chance of being true, 1/2 of being false! How exciting! let coinFlip = () return math.random(0,1) == 1 end --- Insert an element into the skip list using system:compare for ordering. -- Behavior undefined if e is already in the skip list. -- Complexity: if luck is on your side, O(log2(n)); O(n) if the universe hates you; O(1) if compare always returns true and nLayers = 1 let skipInsert = :(system, e) -- find previous entity in each layer let prevLayer = {} let prev = @first -- no need to process first entity as it is the special `head` entity for i=@nLayers, 1, -1 do while true do -- next is end of layer or greater entity: select this prev entity if prev[2] == nil or system:compare(e, prev[2][1]) then prevLayer[i] = prev -- not on base layer: go down a layer, for loop will continue if prev[3] then prev = prev[3] -- same entity on lower layer break end break -- next entity on current layer else prev = prev[2] end end end -- add to each layer let inLowerLayer for i=1, @nLayers do prev = prevLayer[i] if i == 1 or coinFlip() then -- always present in base layer, otherwise 1/2 chance let nxt = prev[2] prev[2] = { e, nxt, inLowerLayer } @previous[i][e] = prev if nxt then @previous[i][nxt[1]] = prev[2] end inLowerLayer = prev[2] else break end end @n += 1 end --- Remove an element from the skip list. -- Behavior undefined if e is not in the skip list. -- Complexity: O(nLayers) at most, which should be O(log2(n)) if you called skipAddLayers often enough let skipDelete = :(e) -- remove from each layer for i=1, @nLayers do let previous = @previous[i] if previous[e] then let prev = previous[e] prev[2] = prev[2][2] previous[e] = nil if prev[2] then previous[prev[2][1]] = prev end else break -- won't appear on higher layers either end end @n -= 1 end --- Reorder an element into the skip list. -- Behavior undefined if e is not in the skip list. -- Complexity: if luck is on your side, O(log2(n)); O(n) if the universe hates you; O(1) if compare always returns true and nLayers = 1 let skipReorder = :(system, e) skipDelete(@, e) skipInsert(@, system, e) end --- Returns the ith element of the skip list. -- Complexity: O(n) let skipIndex = :(i) local n = 1 for e in skipIter(@) do if n == i then return e end n += 1 end return nil end --[[-- 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, entity, component) 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, entity, component, 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. -- -- 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 determine the associated system component if `System.component` is not set. -- If not set, the system will not appear in `world.s`. -- -- Do not change after system instanciation. -- @ftype string -- @ftype nil if no name name = nil, --- List of subsystems. -- On a instancied system, this is a list of the same subsystems, but instancied for this world. -- -- Do not change after system instanciation. -- @ftype table -- @ftype nil if no subsystem systems = nil, --- 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`. -- `true` by default. -- @ftype boolean active = true, --- The system and its subsystems will only draw if this is `true`. -- `true` by default. -- @ftype boolean visible = true, --- Name of the system component. -- Used to determine the associated system component. -- If not set, this will fall back to `System.name`. If this is also not set, then we will give `nil` instead of the system component in callbacks. -- @ftype string -- @ftype nil if no name component = nil, --- 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. -- -- Changing this will not affect entities already in the system. -- Doesn't have any effect if the system doesn't have a component name. -- @ftype any -- @ftype nil if no default default = nil, --- Callbacks. -- -- Functions that are called when something happens in the system. -- Redefine them to change system behaviour. -- @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). -- -- If this is a string or a table, it will be converted to a filter function on instanciation using `ecs.all`. -- -- 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 filter = :(e) return false end, --- Called when adding an entity to this system determining its order. -- 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, new entities are added at the start of the list. -- @callback -- @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 Entity e the entity table -- @tparam Component c the entity's system component, if any onAdd = :(e, c) end, --- Called when removing an entity from the system. -- @callback -- @tparam Entity e the entity table -- @tparam Component c the entity's system component, if any onRemove = :(e, c) end, --- Called when the system is instancied, before any call to `System:onAddToWorld` (including other systems in the world). -- @callback onInstance = :() end, --- Called when the system is added to a world. -- @callback -- @tparam System world world system onAddToWorld = :(world) end, --- Called when the system is removed from a world (i.e., the world is destroyed). -- @callback -- @tparam System world world system onRemoveFromWorld = :(world) end, --- Called when the world is destroyed, after every call to `System:onRemoveFromWorld` (including other systems in the world). -- @callback onDestroy = :() end, --- Called when updating the system. -- Called before any call to `System:process` or call to subsystems. -- @callback -- @number dt delta-time since last update onUpdate = :(dt) end, --- Called when drawing the system. -- Called before any call to `System:draw` or call to subsystems. -- @callback onDraw = :() end, --- Called when updating the system, for every entity the system contains. -- Called after `System:onUpdate` was called on the system, and before any call to subsystems. -- @callback -- @tparam Entity e the entity table -- @tparam Component c the entity's system component, if any -- @number dt delta-time since last update process = :(e, c, dt) end, --- Called when drawing the system, for every entity the system contains. -- Called after `System:onDraw` was called on the system, and before any call to subsystems. -- @callback -- @tparam Entity e the entity table -- @tparam Component c the entity's system component, if any render = :(e, c) end, --- Called after updating the system. -- Called after `System:onDraw`, `System:process` and calls to subsystems. -- @callback -- @number dt delta-time since last update onUpdateEnd = :(dt) end, --- Called after drawing the system. -- Called after `System:onUpdate`, `System:render` and calls to subsystems. -- @callback onDrawEnd = :() end, --- 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 entityCount = 0, --- Map of all named systems in the world (not only subsystems). Same for every system from the same world. -- @ftype table {[system.name]=instanciedSystem, ...} -- @ro s = nil, --- Private fields --- --- Hash map of the entities currently in the system. -- Used to quickly check if an entity is present of not in this system. -- This is actually the _skiplist.previous[1] table. -- @local _has = nil, --- Amount of time waited since last update (if interval is set). -- @local _waited = 0, --- Methods. -- -- Methods available on instancied systems. -- @doc smethods --- Add entities to the system and its subsystems. -- -- Will skip entities that are already in the system. -- -- 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. -- 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(log2(entityCount)) per ordered system. -- @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 @_has[e] and @filter(e) then -- copy default system component if @component and @default then copy({ [@component] = @default }, e) end -- ordered system: add new layer if needed if @compare ~= system_mt.compare then skipAddLayers(@_skiplist) end -- add to skip list skipInsert(@_skiplist, @, e) -- notify addition @entityCount += 1 @onAdd(e, e[@component]) -- add to subsystems (if it wasn't immediately removed in onAdd) if @_has[e] then for _, s in ipairs(@systems) do s:add(e) end end end if ... then return e, @add(...) else return e end end, --- Remove entities from the system and its subsystems. -- -- Will skip entities that are not in the system. -- -- Entities are removed from subsystems before they are removed from their parent system. -- -- If you intend to call this on a subsystem instead of the world, please read the warning in `System:add`. -- -- Complexity: O(1) per unordered system, O(log2(entityCount)) per ordered system. -- @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 @_has[e] then -- remove from subsystems for _, s in ipairs(@systems) do s:remove(e) end end if @_has[e] then -- recheck in case it was removed already from a subsystem onRemove callback skipDelete(@_skiplist, e) -- notify removal @entityCount -= 1 @onRemove(e, e[@component]) end end if ... then return e, @remove(...) else 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 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 @_has[e] then @add(e) else 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(1) per unordered system, O(log2(entityCount)) per ordered system. -- @tparam Entity e entity to reorder -- @tparam Entity... ... other entities to reorder -- @treturn Entity,... `e,...` the function arguments reorder = :(e, ...) if e ~= nil and @_has[e] then skipReorder(@_skiplist, @, e) -- Reorder in subsystems for _, s in ipairs(@systems) do s:reorder(e) 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 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 @_has[e] if ... then return has and @has(...) else return has end end, --- Returns an iterator that iterate through the entties in this system, in order. -- -- Complexity: O(1) per iteration; O(entityCount) for the full iteration -- @treturn iterator iterator over the entities in this system iter = :() return skipIter(@_skiplist) end, --- Get the `i`th entity in the system. -- -- Complexity: O(i) -- @tparam number i the index of the entity -- @treturn Entity the entity; `nil` if there is no such entity in the system get = :(i) return skipIndex(@_skiplist, i) end, --- Remove every entity from the system and its subsystems. -- -- Complexity: O(entityCount) per system clear = :() for e in skipIter(@_skiplist) do @remove(e) end for _, s in ipairs(@systems) do s:clear() end end, --- Try to update the system and its subsystems. Should be called on every game update. -- -- Subsystems are updated after their parent system. -- -- Complexity: O(entityCount) per system if system:process is defined; O(1) per system otherwise. -- @number dt delta-time since last update update = :(dt) if @active then if @interval then @_waited += dt if @_waited < @interval then return end end @onUpdate(dt) if @process ~= system_mt.process then for e in skipIter(@_skiplist) do @process(e, e[@component], dt) end end for _, s in ipairs(@systems) do s:update(dt) end @onUpdateEnd(dt) if @interval then @_waited -= @interval end end end, --- Try to draw the system and its subsystems. Should be called on every game draw. -- -- Subsystems are drawn after their parent system. -- -- -- Complexity: O(entityCount) per system if system:render is defined; O(1) per system otherwise. draw = :() if @visible then @onDraw() if @render ~= system_mt.render then for e in skipIter(@_skiplist) do @render(e, e[@component]) end end for _, s in ipairs(@systems) do s:draw() end @onDrawEnd() end end, --- Trigger a custom callback on a single entity. -- -- This will call the `System:name(e, c, ...)` 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`. -- -- Complexity: O(1) per system -- @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, ...) return @callbackFiltered(alwaysTrue, name, e, ...) end, --- Same as `callback`, but will check every system against a filter before calling the callback. -- -- `filter` is a function that receive the arguments `filter(system, name, e, ...)`, and returns a boolean. -- It will be called on each system before emitting the callback on it; if the filter returns false, the callback will not -- be called on this system and its subsystems. -- -- Complexity: O(1) per system -- @tparam function filter filter function -- @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 callbackFiltered = :(filter, name, e, ...) if filter(@, name, e, ...) then -- call callback if @_has[e] and @[name] then @[name](@, e, e[@component], ...) end -- callback on subsystems (if it wasn't removed during the callback) if @_has[e] then for _, ss in ipairs(@systems) do ss:callbackFiltered(filter, name, e, ...) end 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). -- -- Complexity: O(1) per system -- @tparam string name name of the callback -- @param ... other arguments to pass to the callback emit = :(name, ...) return @emitFiltered(alwaysTrue, name, ...) end, --- Same as `emit`, but will check every system against a filter before calling the event. -- -- `filter` is a function that receive the arguments `filter(system, name, ...)`, and returns a boolean. -- It will be called on each system before emitting the event on it; if the filter returns false, the event will not -- be emitted to this system and its subsystems. -- -- Complexity: O(1) per system -- @tparam function filter filter function -- @tparam string name name of the callback -- @param ... other arguments to pass to the callback emitFiltered = :(filter, name, ...) if filter(@, name, ...) then -- 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:emitFiltered(filter, name, ...) if status == "capture" then break end end end return status end end, --- Remove all the entities and subsystems in this system. -- Complexity: O(entityCount) per system destroy = :() recCallOnRemoveFromWorld(@world, { @ }) recDestroySystems({ systems = { @ } }) end, } --- Recursively instanciate a list of systems for a world: -- * create their self table with instance fields set -- * create a field with their name in world.s (if name defined) let recInstanciateSystems = (world, systems) let t = {} for _, s in ipairs(systems) do let system -- instanciate system system = setmetatable({ systems = recInstanciateSystems(world, s.systems or {}), world = world, w = world, s = world.s, _skiplist = skipNew() }, { __index = :(k) if s[k] ~= nil then return s[k] else return system_mt[k] end end }) system._has = system._skiplist.previous[1] -- create filter if type(s.filter) == "string" then system.filter = (_, e) return e[s.filter] ~= nil end elseif type(s.filter) == "table" then system.filter = ecs.all(unpack(s.filter)) elseif type(s.filter) == "boolean" then if s.filter then system.filter = alwaysTrue else system.filter = alwaysFalse end end -- system component fallback on system name if not s.component and s.name then s.component = s.name end -- add system table.insert(t, system) if s.name then world.s[s.name] = system end system:onInstance() end return t end --- Recursively call :onAddToWorld to a list of systems in a world. let recCallOnAddToWorld = (world, systems) for _, s in ipairs(systems) do recCallOnAddToWorld(world, s.systems) s:onAddToWorld(world) end end --- ECS module. -- @section 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) systems -- @treturn System the world system world = (...) let world = setmetatable({ filter = ecs.all(), s = {}, _skiplist = skipNew(), }, { __index = system_mt }) world._has = world._skiplist.previous[1] 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. -- @tparam string,... ... list of field names that must be in entity -- @treturn function(e) that returns `true` if e has all the fields all = (...) if ... then let l = {...} return function(s, e) for _, k in ipairs(l) do if e[k] == nil then return false end end return true end else return alwaysTrue end end, --- 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 any = (...) if ... then let l = {...} return function(s, e) for _, k in ipairs(l) do if e[k] ~= nil then return true end end return false end else return alwaysFalse end end, --- If `uqt.scene` is available, returns a new scene that will consist of a ECS world with the specified systems and entities. -- -- When suspending and resuming the scene, the `onSuspend` and `onResume` events will be emitted on the ECS world. -- -- Note that since `uqt.scene` use `require` to load the scenes, the scenes files will be cached - including variables defined in them. -- So if you store the entity list directly in a variable in the scene file, it will be reused every time the scene is entered, and will thus -- be shared among the different execution of the scene. This may be problematic if an entity is modified by one scene instance, as it will affect others -- (this should not be a problem with systems due to system instanciation). To avoid this issue, instead you would typically define entities through a -- function that will recreate the entities on every scene load. -- @require ubiquitousse.scene -- @string name the name of the new scene -- @tparam[opt={}] table/function systems list of systems to add to the world. If it is a function, it will be executed every time we enter the scene and returns the list of systems. -- @tparam[opt={}] table/function entities list of entities to add to the world. If it is a function, it will be executed every time we enter the scene and returns the list of entities. -- @treturn scene the new scene scene = (name, systems={}, entities={}) assert(scene, "ubiquitousse.scene unavailable") let s = scene.new(name) let w function s:enter() local sys, ent = systems, entities if type(systems) == "function" then sys = { systems() } end if type(entities) == "function" then ent = { entities() } end w = ecs.world(unpack(sys)) w:add(unpack(ent)) end function s:exit() w:destroy() end function s:suspend() w:emit("onSuspend") end function s:resume() w:emit("onResume") end function s:update(dt) w:update(dt) end function s:draw() w:draw() end return s end } return ecs