Module ecs
ECS (entity compenent system) library for Lua.
Entity Component System library, inspired by the excellent tiny-ecs 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).
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
Functions
world (...) | Create and returns a world system based on a list of systems. |
all (...) | Returns a filter that returns true if, for every argument, a field with the same name exists in the entity. |
any (...) | Returns a filter that returns true if one of the arguments if the name of a field in the entity. |
scene (name[, systems={}[, entities={}]]) | If uqt.scene is available, returns a new scene that will consist of a ECS world with the specified systems and entities. |
Entity objects
Components.
Entity.first | First element of the highest layer linked list of entities: { entity, next_element, element_in_lower_layer }. |
Entity.firstBase | First element of the base layer. |
Entity.previous | List of hash map (one per skip listlayer) of entities in the system and their previous linked list element. |
Entity.nLayers | Number of layers in the skip list. |
Entity.n | Number of elements in the skip list. |
System objects
Modifiable fields.
System.name | Name of the system. |
System.systems | List of subsystems. |
System.interval | If not false , the system will only update every interval seconds. |
System.active | The system and its susbsystems will only update if this is true . |
System.visible | The system and its subsystems will only draw if this is true . |
System.component | Name of the system component. |
System.default | Defaults value to put into the entities’s system component when they are added. |
Callbacks.
System:filter (e) [callback] | Called when checking if an entity should be added to this system. |
System:compare (e1, e2) [callback] | Called when adding an entity to this system determining its order. |
System:onAdd (e, c) [callback] | Called when adding an entity to the system. |
System:onRemove (e, c) [callback] | Called when removing an entity from the system. |
System:onInstance () [callback] | Called when the system is instancied, before any call to System:onAddToWorld (including other systems in the world). |
System:onAddToWorld (world) [callback] | Called when the system is added to a world. |
System:onRemoveFromWorld (world) [callback] | Called when the system is removed from a world (i.e., the world is destroyed). |
System:onDestroy () [callback] | Called when the world is destroyed, after every call to System:onRemoveFromWorld (including other systems in the world). |
System:onUpdate (dt) [callback] | Called when updating the system. |
System:onDraw () [callback] | Called when drawing the system. |
System:process (e, c, dt) [callback] | Called when updating the system, for every entity the system contains. |
System:render (e, c) [callback] | Called when drawing the system, for every entity the system contains. |
System:onUpdateEnd (dt) [callback] | Called after updating the system. |
System:onDrawEnd () [callback] | Called after drawing the system. |
Read-only fields.
System.world [read-only] | The world the system belongs to. |
System.w [read-only] | Shortcut to System.world. |
System.entityCount [read-only] | Number of entities in the system. |
System.s [read-only] | Map of all named systems in the world (not only subsystems). |
Methods.
System:add (e, ...) | Add entities to the system and its subsystems. |
System:remove (e, ...) | Remove entities from the system and its subsystems. |
System:refresh (e, ...) | Refresh an entity’s systems. |
System:reorder (e, ...) | Reorder an entity. |
System:has (e, ...) | Returns true if all these entities are in the system. |
System:iter () | Returns an iterator that iterate through the entties in this system, in order. |
System:get (i) | Get the i th entity in the system. |
System:clear () | Remove every entity from the system and its subsystems. |
System:update (dt) | Try to update the system and its subsystems. |
System:draw () | Try to draw the system and its subsystems. |
System:callback (name, e, ...) | Trigger a custom callback on a single entity. |
System:callbackFiltered (filter, name, e, ...) | Same as callback, but will check every system against a filter before calling the callback. |
System:emit (name, ...) | Emit an event on the system. |
System:emitFiltered (filter, name, ...) | Same as emit, but will check every system against a filter before calling the event. |
System:destroy () | Remove all the entities and subsystems in this system. |
Functions
- world (...)
-
Create and returns a world system based on a list of systems.
The systems will be instancied for this world.
Parameters:
- ... table,... list of (uninstancied) systems
Returns:
-
System
the world system
- all (...)
-
Returns a filter that returns
true
if, for every argument, a field with the same name exists in the entity.Parameters:
- ... string,... list of field names that must be in entity
Returns:
-
function(e)
that returns
true
if e has all the fields - any (...)
-
Returns a filter that returns
true
if one of the arguments if the name of a field in the entity.Parameters:
- ... string,... list of field names that may be in entity
Returns:
-
function(e)
that returns
true
if e has at leats one of the fields - scene (name[, systems={}[, entities={}]])
-
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
andonResume
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.Requires:
-
ubiquitousse.scene
Parameters:
- name string the name of the new scene
- systems table/function 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. (default {})
- entities table/function 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. (default {})
Returns:
-
scene
the new scene
Entity objects
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”.
Components.
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.
- Entity.first
-
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.Fields:
- head
- nil
- Entity.firstBase
- First element of the base layer.
- Entity.previous
-
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).Fields:
- {
- Entity.nLayers
- Number of layers in the skip list.
- Entity.n
- Number of elements in the skip list.
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 }
System objects
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.
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)
Modifiable fields.
- System.name
-
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 inworld.s
.Do not change after system instanciation.
Type:
string
nil
if no name
- System.systems
-
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.
Type:
table
nil
if no subsystem
- System.interval
-
If not
false
, the system will only update every interval seconds.false
by default.Type:
number
interval of time between each updatefalse
to disable
- System.active
-
The system and its susbsystems will only update if this is
true
.true
by default.Type:
boolean
- System.visible
-
The system and its subsystems will only draw if this is
true
.true
by default.Type:
boolean
- System.component
-
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.Type:
string
nil
if no name
- System.default
-
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.
Type:
any
nil
if no default
Callbacks.
- System:filter (e) [callback]
-
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; iffalse
, 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.
Parameters:
- e table entity table to check
Returns:
-
boolean
true
if entity should be added - System:compare (e1, e2) [callback]
-
Called when adding an entity to this system determining its order.
Returns
true
ife1 <=
e2 (i.e., ife1
should be processed beforee2
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.
Parameters:
Returns:
-
boolean
true
if e1 <= e2 - System:onAdd (e, c) [callback]
-
Called when adding an entity to the system.
Parameters:
- System:onRemove (e, c) [callback]
-
Called when removing an entity from the system.
Parameters:
- System:onInstance () [callback]
- Called when the system is instancied, before any call to System:onAddToWorld (including other systems in the world).
- System:onAddToWorld (world) [callback]
-
Called when the system is added to a world.
Parameters:
- world System world system
- System:onRemoveFromWorld (world) [callback]
-
Called when the system is removed from a world (i.e., the world is destroyed).
Parameters:
- world System world system
- System:onDestroy () [callback]
- Called when the world is destroyed, after every call to System:onRemoveFromWorld (including other systems in the world).
- System:onUpdate (dt) [callback]
-
Called when updating the system.
Called before any call to System:process or call to subsystems.
Parameters:
- dt number delta-time since last update
- System:onDraw () [callback]
- Called when drawing the system. Called before any call to System:draw or call to subsystems.
- System:process (e, c, dt) [callback]
-
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.
Parameters:
- System:render (e, c) [callback]
-
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.
Parameters:
- System:onUpdateEnd (dt) [callback]
-
Called after updating the system.
Called after System:onDraw, System:process and calls to subsystems.
Parameters:
- dt number delta-time since last update
- System:onDrawEnd () [callback]
- Called after drawing the system. Called after System:onUpdate, System:render and calls to subsystems.
Read-only fields.
- System.world [read-only]
-
The world the system belongs to.
Type:
System
world - System.w [read-only]
-
Shortcut to System.world.
Type:
System
world - System.entityCount [read-only]
-
Number of entities in the system.
Type:
integer
- System.s [read-only]
-
Map of all named systems in the world (not only subsystems). Same for every system from the same world.
Type:
table
{[system.name]=instanciedSystem, ...}
Methods.
- System:add (e, ...)
-
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.
Parameters:
- e Entity entity to add
- ... Entity... other entities to add
Returns:
-
Entity,...
e,…
the function arguments - System:remove (e, ...)
-
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.
Parameters:
- e Entity entity to remove
- ... Entity... other entities to remove
Returns:
-
Entity,...
e,…
the function arguments - System:refresh (e, ...)
-
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.
Parameters:
- e Entity entity to refresh
- ... Entity... other entities to refresh
Returns:
-
Entity,...
e,…
the function arguments - System:reorder (e, ...)
-
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.
Parameters:
- e Entity entity to reorder
- ... Entity... other entities to reorder
Returns:
-
Entity,...
e,…
the function arguments - System:has (e, ...)
-
Returns
true
if all these entities are in the system.Complexity: O(1).
Parameters:
- e Entity entity that may be in the system
- ... Entity... other entities that may be in the system
Returns:
-
boolean
true
if every entity is in the system - System:iter ()
-
Returns an iterator that iterate through the entties in this system, in order.
Complexity: O(1) per iteration; O(entityCount) for the full iteration
Returns:
-
iterator
iterator over the entities in this system
- System:get (i)
-
Get the
i
th entity in the system.Complexity: O(i)
Parameters:
- i number the index of the entity
Returns:
-
Entity
the entity;
nil
if there is no such entity in the system - System:clear ()
-
Remove every entity from the system and its subsystems.
Complexity: O(entityCount) per system
- System:update (dt)
-
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.
Parameters:
- dt number delta-time since last update
- System:draw ()
-
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.
- System:callback (name, e, ...)
-
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 associated with the current system, ande
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
Parameters:
- System:callbackFiltered (filter, name, e, ...)
-
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
Parameters:
- System:emit (name, ...)
-
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 (likeactive = 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
Parameters:
- name string name of the callback
- ... other arguments to pass to the callback
- if a callback returns
- System:emitFiltered (filter, name, ...)
-
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
Parameters:
- filter function filter function
- name string name of the callback
- ... other arguments to pass to the callback
- System:destroy ()
- Remove all the entities and subsystems in this system. Complexity: O(entityCount) per system