Evolved ECS (Entity-Component-System) for Lua
- Introduction
- Installation
- Quick Start
- Overview
- Cheat Sheet
- License
evolved.lua
is a fast and flexible ECS (Entity-Component-System) library for Lua. It is designed to be simple and easy to use, while providing all the features needed to create complex systems with blazing performance. Before we start exploring the library, let's take a look at the main advantages of using evolved.lua
:
This library is designed to be fast. Many techniques are employed to achieve this. It uses an archetype-based approach to store entities and their components. Components are stored in contiguous arrays in a SoA (Structure of Arrays) manner, which allows for fast iteration and processing. Chunks are used to group entities with the same set of components together, enabling efficient filtering through queries. Additionally, all operations are designed to minimize GC (Garbage Collector) pressure and avoid unnecessary allocations. I have tried to take into account all the performance pitfalls of vanilla Lua and LuaJIT.
Not all the optimizations I want to implement are done yet, but I will be working on them. However, I can already say that the library is fast enough for most use cases.
I have tried to keep the API as simple and intuitive as possible. I also keep the number of functions under control. All the functions are self-explanatory and easy to use. After reading the Overview section, you should be able to use the library without any problems.
And yes, the library has some unusual concepts at its core, but once you get the hang of it, you will find it's very easy to use.
evolved.lua
is not just about keeping components in entities. It's a full-fledged ECS library that allows you to create complex systems and processes. You can create queries with filters, use deferred operations, and batch operations. You can also create systems that process entities in a specific order. The library is designed to be flexible and extensible, so you can easily add your own features and functionality. Features like fragment hooks allow you to manage your components in a more flexible way, synchronizing them with external systems or libraries. The library also provides syntactic sugar like the entity builder for creating entities, fragments, and systems to make your life easier.
On the other hand, evolved.lua
tries to be minimalistic and does not provide features that can be implemented outside the library. I'm trying to find a balance between minimalism and the number of possibilities, which forces me to make flexible decisions in the library's design. I hope you will find this balance acceptable.
evolved.lua
is a single-file pure Lua library and does not require any external dependencies. It is designed to work with Lua 5.1 and later, LuaJIT, and Luau (Roblox).
All you need to start using the library is the evolved.lua source file. You can download it from the releases page or clone the repository and copy the file to your project.
If you are using LuaRocks, you can install the library using the following command:
luarocks install evolved.lua
To get started with evolved.lua
, read the Overview section to understand the basic concepts and how to use the library. After that, check the Example, which demonstrates complex usage of the library. Finally, refer to the Cheat Sheet for a quick reference of all the functions and classes provided by the library.
The library is designed to be simple and highly performant. It uses an archetype-based approach to store entities and their components. This allows you to filter and process your entities very efficiently, especially when you have many of them.
If you are familiar with the ECS (Entity-Component-System) pattern, you will feel right at home. If not, I highly recommend reading about it first. Here is a good starting point: Entity Component System FAQ.
Let's get started!
An identifier is a packed 40-bit integer. The first 20 bits represent the index, and the last 20 bits represent the version. To create a new identifier, use the evolved.id
function.
---@param count? integer
---@return evolved.id ... ids
function evolved.id(count) end
The count
parameter is optional and defaults to 1
. The function returns one or more identifiers depending on the count
parameter. The maximum number of alive identifiers is 2^20-1
(1048575). After that, the function will throw an error: | evolved.lua | id index overflow
.
Identifiers can be recycled. When an identifier is no longer needed, use the evolved.destroy
function to destroy it. This will free the identifier for reuse.
---@param ... evolved.id ids
function evolved.destroy(...) end
The evolved.destroy
function takes one or more identifiers as arguments. Destroyed identifiers will be added to the recycler free list. It is safe to call evolved.destroy
on identifiers that are not alive; the function will simply ignore them.
After destroying an identifier, it can be reused by calling the evolved.id
function again. The new identifier will have the same index as the destroyed one, but a different version. The version is incremented each time an identifier is destroyed. This mechanism allows us to reuse indices and to know whether an identifier is alive or not.
The set of evolved.alive
functions can be used to check whether identifiers are alive.
---@param id evolved.id
---@return boolean
function evolved.alive(id) end
---@param ... evolved.id ids
---@return boolean
function evolved.alive_all(...) end
---@param ... evolved.id ids
---@return boolean
function evolved.alive_any(...) end
Sometimes (for debugging purposes, for example), it is necessary to extract the index and version from an identifier or to pack them back into an identifier. The evolved.pack
and evolved.unpack
functions can be used for this purpose.
---@param index integer
---@param version integer
---@return evolved.id id
function evolved.pack(index, version) end
---@param id evolved.id
---@return integer index
---@return integer version
function evolved.unpack(id) end
Here is a short example of how to use identifiers:
local evolved = require 'evolved'
local id = evolved.id() -- create a new identifier
assert(evolved.alive(id)) -- check that the identifier is alive
local index, version = evolved.unpack(id) -- unpack the identifier
assert(evolved.pack(index, version) == id) -- pack it back
evolved.destroy(id) -- destroy the identifier
assert(not evolved.alive(id)) -- check that the identifier is not alive now
First, you need to understand that entities and fragments are just identifiers. The difference between them is purely semantic. Entities are used to represent objects in the world, while fragments are used to represent types of components that can be attached to entities. Components, on the other hand, are any data that is attached to entities through fragments.
---@alias evolved.entity evolved.id
---@alias evolved.fragment evolved.id
---@alias evolved.component any
Here is a simple example of how to attach a component to an entity:
local evolved = require 'evolved'
local entity, fragment = evolved.id(2)
evolved.set(entity, fragment, 100)
assert(evolved.get(entity, fragment) == 100)
I know it's not very clear yet, but don't worry, we'll get there. In the next example, I'm going to name the entity and fragment, so it will be easier to understand what's going on here.
local evolved = require 'evolved'
local player = evolved.id()
local health = evolved.id()
local stamina = evolved.id()
evolved.set(player, health, 100)
evolved.set(player, stamina, 50)
assert(evolved.get(player, health) == 100)
assert(evolved.get(player, stamina) == 50)
We created an entity called player
and two fragments called health
and stamina
. We attached the components 100
and 50
to the entity through these fragments. After that, we can retrieve the components using the evolved.get
function.
We'll cover the evolved.set
and evolved.get
functions in more detail later in the section about modifying operations. For now, let's just say that they are used to set and get components from entities through fragments.
The main thing to understand here is that you can attach any data to any identifier by using other identifiers.
Since fragments are just identifiers, you can use them as entities too! Fragments of fragments are usually called traits
. This is very useful, for example, for marking fragments with some metadata.
local evolved = require 'evolved'
local serializable = evolved.id()
local position = evolved.id()
evolved.set(position, serializable, true)
local velocity = evolved.id()
evolved.set(velocity, serializable, true)
local player = evolved.id()
evolved.set(player, position, {x = 0, y = 0})
evolved.set(player, velocity, {x = 0, y = 0})
In this example, we create a trait called serializable
and mark the fragments position
and velocity
as serializable. After that, you can write a function that will serialize entities, and this function will serialize only fragments that are marked as serializable. This is a very powerful feature of the library, and it allows you to create very flexible systems.
Fragments can even be attached to themselves; this is called a singleton. Use this when you want to store some data without having a separate entity. For example, you can use it to store global data, like the game state or the current level.
local evolved = require 'evolved'
local gravity = evolved.id()
evolved.set(gravity, gravity, 10)
assert(evolved.get(gravity, gravity) == 10)
The next thing we need to understand is that all non-empty entities are stored in chunks. Chunks are just tables that store entities and their components together. Each unique combination of fragments is stored in a separate chunk. This means that if you have two entities with health
and stamina
fragments, they will be stored in the <health, stamina>
chunk. If you have another entity with health
, stamina
, and mana
fragments, it will be stored in the <health, stamina, mana>
chunk. This is very useful for performance reasons, as it allows us to store entities with the same fragments together, making it easier to iterate, filter, and process them.
local evolved = require 'evolved'
local health, stamina, mana = evolved.id(3)
local entity1 = evolved.id()
evolved.set(entity1, health, 100)
evolved.set(entity1, stamina, 50)
local entity2 = evolved.id()
evolved.set(entity2, health, 75)
evolved.set(entity2, stamina, 40)
local entity3 = evolved.id()
evolved.set(entity3, health, 50)
evolved.set(entity3, stamina, 30)
evolved.set(entity3, mana, 20)
Here is what the chunks will look like after the code above has executed:
chunk | health | stamina |
---|---|---|
entity1 | 100 | 50 |
entity2 | 75 | 40 |
chunk | health | stamina | mana |
---|---|---|---|
entity3 | 50 | 30 | 20 |
Usually, you don't need to operate on chunks directly, but you can use the evolved.chunk
function to get the specific chunk.
---@param fragment evolved.fragment
---@param ... evolved.fragment fragments
---@return evolved.chunk chunk
function evolved.chunk(fragment, ...) end
The evolved.chunk
function takes one or more fragments as arguments and returns the chunk for this combination. After that, you can use the chunk's methods to retrieve their entities, fragments, and components.
---@return evolved.entity[] entity_list
---@return integer entity_count
function chunk_mt:entities() end
---@return evolved.fragment[] fragment_list
---@return integer fragment_count
function chunk_mt:fragments() end
---@param ... evolved.fragment fragments
---@return evolved.storage ... storages
function chunk_mt:components(...)
Full example:
local evolved = require 'evolved'
local health, stamina, mana = evolved.id(3)
local entity1 = evolved.id()
evolved.set(entity1, health, 100)
evolved.set(entity1, stamina, 50)
local entity2 = evolved.id()
evolved.set(entity2, health, 75)
evolved.set(entity2, stamina, 40)
local entity3 = evolved.id()
evolved.set(entity3, health, 50)
evolved.set(entity3, stamina, 30)
evolved.set(entity3, mana, 20)
-- get (or create if it doesn't exist) the chunk <health, stamina>
local chunk = evolved.chunk(health, stamina)
-- get the list of entities in the chunk and the number of them
local entity_list, entity_count = chunk:entities()
-- get the columns of components in the chunk
local health_components = chunk:components(health)
local stamina_components = chunk:components(stamina)
for i = 1, entity_count do
local entity = entity_list[i]
local entity_health = health_components[i]
local entity_stamina = stamina_components[i]
-- do something with the entity and its components
print(string.format(
'Entity: %d, Health: %d, Stamina: %d',
entity, entity_health, entity_stamina))
end
-- Expected output:
-- Entity: 1048602, Health: 100, Stamina: 50
-- Entity: 1048603, Health: 75, Stamina: 40
Every time we insert or remove a fragment from an entity, the entity will be migrated to a new chunk. This is done automatically by the library, of course. However, you should be aware of this because it can affect performance, especially if you have many fragments on the entity. This is called a structural change
.
You should try to avoid structural changes, especially in performance-critical code. For example, you can spawn entities with all the fragments they will ever need and avoid changing them during the entity's lifetime. Overriding existing components is not a structural change, so you can do it freely.
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.spawn(components) end
---@param prefab evolved.entity
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.clone(prefab, components) end
The evolved.spawn
function allows you to spawn an entity with all the necessary fragments. It takes a table of components as an argument, where the keys are fragments and the values are components. By the way, you don't need to create this components
table every time; consider using a predefined table for maximum performance.
You can also use the evolved.clone
function to clone an existing entity. This is useful for creating entities with the same fragments as an existing entity but with different components.
local evolved = require 'evolved'
local health, stamina = evolved.id(2)
-- spawn an entity with all the necessary fragments
local enemy1 = evolved.spawn {
[health] = 100,
[stamina] = 50,
}
-- spawn another entity with the same fragments,
-- but with a different component for some of them
local enemy2 = evolved.clone(enemy1, {
[health] = 50,
})
-- there are no structural changes here,
-- we just override existing components
evolved.set(enemy1, health, 75)
evolved.set(enemy1, stamina, 42)
Another way to avoid structural changes when spawning entities is to use the evolved.builder
fluid interface. The evolved.builder
function returns a builder object that allows you to spawn entities with a specific set of fragments and components without the necessity of setting them one by one with structural changes for each change.
local evolved = require 'evolved'
local health, stamina = evolved.id(2)
local enemy = evolved.builder()
:set(health, 100)
:set(stamina, 50)
:spawn()
Builders can be reused, so you can create a builder with a specific set of fragments and components and then use it to spawn multiple entities with the same fragments and components.
The library provides all the necessary functions to access entities and their components. I'm not going to cover all the accessor functions here, because they are pretty straightforward and self-explanatory. You can check the API Reference for all of them. Here are some of the most important ones:
---@param entity evolved.entity
---@return boolean
function evolved.alive(entity) end
---@param entity evolved.entity
---@return boolean
function evolved.empty(entity) end
---@param entity evolved.entity
---@param fragment evolved.fragment
function evolved.has(entity, fragment) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
---@return evolved.component ... components
function evolved.get(entity, ...) end
The evolved.alive
function checks whether an entity is alive. The evolved.empty
function checks whether an entity is empty (has no fragments). The evolved.has
function checks whether an entity has a specific fragment. The evolved.get
function retrieves the components of an entity for the specified fragments. If the entity doesn't have some of the fragments or if the fragments are marked with the evolved.TAG
, the function will return nil
for them.
All of these functions can be safely called on non-alive entities and non-alive fragments. Also, they do not cause any structural changes, because they do not modify anything.
The library provides a classic set of functions for modifying entities. These functions are used to insert, override, and remove fragments from entities.
---@param entity evolved.entity
---@param fragment evolved.fragment
---@param component evolved.component
function evolved.set(entity, fragment, component) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
function evolved.remove(entity, ...)
---@param ... evolved.entity entities
function evolved.clear(...)
---@param ... evolved.entity entities
function evolved.destroy(...)
The evolved.set
function is used to set a component on an entity. If the entity doesn't have this fragment, it will be inserted, with causing a structural change, of course. If the entity already has the fragment, the component will be overridden. The function should not be called on non-alive entities, because it is not possible to set any component on a destroyed entity, ignoring this can lead to errors. The Debug Mode can be used to check this kind of error.
Use the evolved.remove
function to remove fragments from an entity. If the entity doesn't have some of the fragments, they will be ignored. When one or more fragments are removed from an entity, the entity will be migrated to a new chunk, which is a structural change. When you want to remove more than one fragment, pass all of them as arguments. Do not remove fragments one by one, as this will cause a structural change for each fragment. The evolved.remove
function will ignore non-alive entities, because post-conditions are satisfied (destroyed entities do not have any fragments, including those that we want to remove).
To remove all fragments from an entity, use the evolved.clear
function. This function will remove all fragments at once, causing only one structural change. The evolved.clear
function does not destroy the entity, it just removes all fragments from it. The entity after this operation will be empty, but it will still be alive. You can use this function to clear more than one entity at once, passing them as arguments. The function will ignore empty and non-alive entities.
To destroy an entity, use the evolved.destroy
function. This function will remove all fragments from the entity and free the identifier of the entity for reuse. The evolved.destroy
function will ignore non-alive entities. To destroy more than one entity, pass them as arguments.
The library has a debug mode that can be enabled by the evolved.debug_mode
function. When the debug mode is enabled, the library will check for incorrect usages of the API and throw errors when they are detected. This is very useful for debugging and development, but it can slow down performance a bit.
---@param yesno boolean
function evolved.debug_mode(yesno) end
The debug mode is disabled by default, so you need to enable it manually. I strongly recommend doing this in the development environment. You can even leave it enabled in production, but only if you are sure the performance is acceptable for your case.
local evolved = require 'evolved'
evolved.debug_mode(true)
local entity = evolved.id()
local fragment = evolved.id()
evolved.destroy(fragment)
-- try to use the destroyed fragment
evolved.set(entity, fragment, 42)
-- [error] | evolved.lua | the fragment ($1048599#23:1) is not alive and cannot be used
One of the most important features of any ECS library is the ability to process entities by filters or queries. evolved.lua
provides a simple and efficient way to do this.
First, you need to create a query that describes which entities you want to process. You can specify fragments you want to include, and fragments you want to exclude. Queries are just identifiers with a special predefined fragments: evolved.INCLUDES
and evolved.EXCLUDES
. These fragments expect a list of fragments as their components.
local evolved = require 'evolved'
local health, poisoned, resistant = evolved.id(3)
local query = evolved.id()
evolved.set(query, evolved.INCLUDES, { health, poisoned })
evolved.set(query, evolved.EXCLUDES, { resistant })
The builder interface can be used to create queries too. It is more convenient to use, because the builder has special methods for including and excluding fragments. Here is a simple example of this:
local query = evolved.builder()
:include(health, poisoned)
:exclude(resistant)
:spawn()
We don't have to set both evolved.INCLUDES
and evolved.EXCLUDES
fragments, we can even do it without filters at all, then the query will match all chunks in the world.
After the query is created, we are ready to process our filtered by this query entities. You can do this by using the evolved.execute
function. This function takes a query as an argument and returns an iterator that can be used to iterate over all matching with the query chunks.
---@param query evolved.query
---@return evolved.execute_iterator iterator
---@return evolved.execute_state? iterator_state
function evolved.execute(query) end
for chunk, entity_list, entity_count in evolved.execute(query) do
---@type number[]
local health_components = chunk:components(health)
for i = 1, entity_count do
health_components[i] = math.max(
health_components[i] - 1,
0)
end
end
As you can see, evolved.execute_iterator
returns a chunk, a list of entities in the chunk, and the number of entities in this chunk. We already know how to use chunks, so we can use the chunk's methods to retrieve the components of the entities in the chunk, change them, and so on.
Note
But I haven't mentioned one important thing yet: structural changes are not allowed during any iteration over chunks. This means that you cannot insert or remove fragments from entities while iterating. Also, you cannot destroy or spawn entities because this will cause structural changes too. This is done to avoid inconsistencies in the iteration process. If we allowed structural changes here, we might skip some entities during iteration, or process the same entity multiple times. The debug mode can catch this kind of error.
Now we know that structural changes are not allowed during iteration, but what if we want to make some structural changes after the iteration is finished? For example, we might want to remove some fragments from entities after we have processed them, or we might want to spawn new entities while processing existing ones. To do all of this, we can use deferred operations.
---@return boolean started
function evolved.defer() end
---@return boolean committed
function evolved.commit() end
The evolved.defer
function starts a deferred scope. This means that all changes made inside the scope will be queued and applied after leaving the scope. The evolved.commit
function closes the last deferred scope and applies all queued changes. These functions can be nested, so you can start a new deferred scope inside an existing one. The evolved.commit
function will apply all queued changes only when the last deferred scope is closed.
local evolved = require 'evolved'
local health, poisoned = evolved.id(2)
local player = evolved.builder()
:set(health, 100)
:set(poisoned, true)
:spawn()
-- start a deferred scope
evolved.defer()
-- this removal will be queued, not applied immediately
evolved.remove(player, poisoned)
-- the player still has the poisoned fragment inside the deferred scope
assert(evolved.has(player, poisoned))
-- commit the deferred operations
evolved.commit()
-- now the poisoned fragment is removed
assert(not evolved.has(player, poisoned))
The library provides a set of functions for batch operations. These functions are used to perform modifying operations on multiple chunks at once. This is very useful for performance reasons.
---@param query evolved.query
---@param fragment evolved.fragment
---@param component evolved.component
function evolved.batch_set(query, fragment, component) end
---@param query evolved.query
---@param ... evolved.fragment fragments
function evolved.batch_remove(query, ...) end
---@param ... evolved.query queries
function evolved.batch_clear(...) end
---@param ... evolved.query queries
function evolved.batch_destroy(...) end
These functions are similar to the common modifying operations, but they take a query as an argument instead of an entity. Here is a classic example that provides a huge performance boost when applied.
local evolved = require 'evolved'
local destroying_mark = evolved.id()
local destroying_mark_query = evolved.builder()
:include(destroying_mark)
:spawn()
-- destroy all entities with the destroying_mark fragment
evolved.batch_destroy(destroying_mark_query)
Tip
You should always prefer batch operations over common modifying operations when you need to perform simple operations like destroying or removing fragments from multiple entities at once. Instead of applying the operation to each entity one by one, batch operations will apply the operation chunk by chunk.
In all other respects, batch operations behave the same way as the common modifying operations that we have already covered. Of course, they can also be used with deferred operations.
Usually, we want to organize our processing of entities into systems that will be executed in a specific order. The library has a way to do this using special evolved.QUERY
and evolved.EXECUTE
fragments that are used to specify the system's queries and execution callbacks. And yes, systems are just entities with special fragments.
local evolved = require 'evolved'
local health, max_health = evolved.id(2)
local query = evolved.builder()
:include(health, max_health)
:spawn()
local system = evolved.builder()
:query(query)
:execute(function(chunk, entity_list, entity_count)
local health_components = chunk:components(health)
local max_health_components = chunk:components(max_health)
for i = 1, entity_count do
health_components[i] = math.min(
health_components[i] + 1,
max_health_components[i])
end
end):spawn()
The evolved.process
function is used to process systems. It takes systems as arguments and executes them in the order they were passed.
---@param ... evolved.system systems
function evolved.process(...) end
To group systems together, you can use the evolved.GROUP
fragment. Systems with a specified group will be processed when you call the evolved.process
function with this group. For example, you can group all physics systems together and process them in one evolved.process
call.
local evolved = require 'evolved'
local gravity_x = 0
local gravity_y = -9.81
local position_x, position_y = evolved.id(2)
local velocity_x, velocity_y = evolved.id(2)
local physical_body_query = evolved.builder()
:include(position_x, position_y)
:include(velocity_x, velocity_y)
:spawn()
local physics_group = evolved.id()
evolved.builder()
:group(physics_group)
:query(physical_body_query)
:execute(function(chunk, entity_list, entity_count)
local vx = chunk:components(velocity_x)
local vy = chunk:components(velocity_y)
for i = 1, entity_count do
vx[i] = vx[i] + gravity_x
vy[i] = vy[i] + gravity_y
end
end):spawn()
evolved.builder()
:group(physics_group)
:query(physical_body_query)
:execute(function(chunk, entity_list, entity_count)
local px = chunk:components(position_x)
local py = chunk:components(position_y)
local vx = chunk:components(velocity_x)
local vy = chunk:components(velocity_y)
for i = 1, entity_count do
px[i] = px[i] + vx[i]
py[i] = py[i] + vy[i]
end
end):spawn()
evolved.process(physics_group)
Systems and groups also can have the evolved.PROLOGUE
and evolved.EPILOGUE
fragments. These fragments are used to specify callbacks that will be executed before and after the system or group is processed. This is useful for setting up and tearing down systems or groups, or for performing some additional processing before or after the main processing.
local evolved = require 'evolved'
local system = evolved.builder()
:prologue(function()
print('Prologue')
end)
:epilogue(function()
print('Epilogue')
end)
:spawn()
evolved.process(system)
The prologue and epilogue fragments do not require an explicit query. They will be executed before and after the system is processed, regardless of the query.
Note
And one more thing about systems. Execution callbacks are called in the deferred scope, which means that all modifying operations inside the callback will be queued and applied after the system has processed all chunks. But prologue and epilogue callbacks are not called in the deferred scope, so all modifying operations inside them will be applied immediately. This is done to avoid confusion and to make it clear that prologue and epilogue callbacks are not part of the chunk processing.
Sometimes you want to have a fragment without a component. For example, you might want to have some marks that will be used to mark entities for processing. Fragments without components are called tags
. Such fragments take up less memory, because they do not require any components to be stored. Migration of entities with tags is faster, because the library does not need to migrate components, only the tags themselves. To create a tag, mark the fragment with the evolved.TAG
fragment.
local evolved = require 'evolved'
local player_tag = evolved.id()
evolved.set(player_tag, evolved.TAG)
local player = evolved.id()
evolved.set(player, player_tag)
-- player has the player_tag fragment
assert(evolved.has(player, player_tag))
-- player_tag is a tag, so it doesn't have a component
assert(evolved.get(player, player_tag) == nil)
The library provides a way to execute callbacks when fragments are set, assigned, inserted, or removed from entities. This is done using special fragments: evolved.ON_SET
, evolved.ON_ASSIGN
, evolved.ON_INSERT
, and evolved.ON_REMOVE
. These fragments are used to specify the callbacks that will be executed when the corresponding operation is performed on the fragment.
local evolved = require 'evolved'
local health = evolved.builder()
:on_set(function(entity, fragment, component)
print('health set to ' .. component)
end):spawn()
local player = evolved.id()
evolved.set(player, health, 100) -- prints "health set to 100"
evolved.set(player, health, 200) -- prints "health set to 200"
Use evolved.ON_SET
for callbacks on fragment insert or override, evolved.ON_ASSIGN
for overrides, and evolved.ON_INSERT
/evolved.ON_REMOVE
for insertions or removals.
Some fragments should not be cloned when cloning entities. For example, evolved.lua
has a special fragment called evolved.PREFAB
, which marks entities used as sources for cloning. This fragment should not be present on the cloned entities. To prevent a fragment from being cloned, mark it as unique using the evolved.UNIQUE
fragment trait. This ensures the fragment will not be copied when cloning entities.
local evolved = require 'evolved'
local health, stamina = evolved.id(2)
local enemy_prefab = evolved.builder()
:prefab()
:set(health, 100)
:set(stamina, 50)
:spawn()
local enemy_clone = evolved.clone(enemy_prefab)
-- the enemy_prefab has the evolved.PREFAB fragment
assert(evolved.has(enemy_prefab, evolved.PREFAB))
-- but the enemy_clone doesn't have it, because it is marked as unique
assert(not evolved.has(enemy_clone, evolved.PREFAB))
In some cases, you might want to hide chunks with certain fragments from queries by default. For example, the library has a special fragment called evolved.DISABLED
that behaves this way. This fragment is marked with the evolved.EXPLICIT
fragment trait, which means it will be hidden from queries unless you explicitly include it. This is useful for fragments that are used for internal or editor purposes and should not be exposed to queries by default.
Additionally, the evolved.PREFAB
fragment is also marked with the evolved.EXPLICIT
fragment trait. This prevents prefabs from being processed in queries at runtime. Prefabs are used only for cloning entities, so they should not be processed by default.
local evolved = require 'evolved'
local enemy_tag = evolved.builder()
:tag()
:spawn()
local only_enabled_enemies = evolved.builder()
:include(enemy_tag)
:spawn()
local all_enemies_including_disabled = evolved.builder()
:include(enemy_tag)
:include(evolved.DISABLED)
:spawn()
Often, we want to store components as tables, and by default, these tables will be shared between entities. This means that if you modify the table in one entity, it will be modified in all entities that share this table. Sometimes this is what we want. For example, when we want to share a configuration or some resource between entities. But in other cases, we want each entity to have its own copy of the table. For example, if we want to store the position of an entity as a table, we don't want to share this table with other entities. Yes, we can copy the table manually, but the library provides a little bit of syntactic sugar for this.
local evolved = require 'evolved'
local initial_position = { x = 0, y = 0 }
local position = evolved.id()
local enemy1 = evolved.builder()
:set(position, initial_position)
:spawn()
local enemy2 = evolved.builder()
:set(position, initial_position)
:spawn()
-- the enemy1 and enemy2 share the same table,
-- and that's definitely not what we want in this case
assert(evolved.get(enemy1, position) == evolved.get(enemy2, position))
To avoid this, evolved.lua
provides a fragment trait called evolved.DUPLICATE
. This trait expects a function that will be called when the component of this fragment is set. The function should return a new table to be used as the component for the entity. This way, each entity will have its own copy of the table, and modifying one entity will not affect the others.
To make this example clearer, we will also use the evolved.DEFAULT
fragment trait. This trait is used to specify a default value for the component. The default value will be used when the component is not set explicitly.
local evolved = require 'evolved'
local function vector2(x, y)
return { x = x, y = y }
end
local function vector2_duplicate(v)
return { x = v.x, y = v.y }
end
local position = evolved.builder()
:default(vector2(0, 0))
:duplicate(vector2_duplicate)
:spawn()
local enemy1 = evolved.builder()
:set(position)
:spawn()
local enemy2 = evolved.builder()
:set(position)
:spawn()
-- the enemy1 and enemy2 have different tables now
assert(evolved.get(enemy1, position) ~= evolved.get(enemy2, position))
Typically, fragments remain alive for the entire lifetime of the program. However, in some cases, you might want to destroy fragments when they are no longer needed. For example, you can use some runtime entities as fragments for other entities. In this case, you might want to destroy such fragments even while they are still attached to other entities. Since entities cannot have destroyed fragments, a destruction policy must be applied to resolve this. By default, the library will remove the destroyed fragment from all entities that have it.
local evolved = require 'evolved'
local world = evolved.builder()
:tag()
:spawn()
local entity = evolved.builder()
:set(world)
:spawn()
-- destroy the world fragment that is attached to the entity
evolved.destroy(world)
-- the entity is still alive, but it no longer has the world fragment
assert(evolved.alive(entity) and not evolved.has(entity, world))
The default behavior works well in most cases, but you can change it by using the evolved.DESTRUCTION_POLICY
fragment. This fragment expects one of the following predefined identifiers:
-
evolved.DESTRUCTION_POLICY_DESTROY_ENTITY
will destroy any entity that has the destroyed fragment. This is useful for cases like the one above, where you want to destroy all entities when their world is destroyed. -
evolved.DESTRUCTION_POLICY_REMOVE_FRAGMENT
will remove the destroyed fragment from all entities that have it. This is the default behavior, so you don't have to set it explicitly, but you can if you want.
local evolved = require 'evolved'
local world = evolved.builder()
:tag()
:destruction_policy(evolved.DESTRUCTION_POLICY_DESTROY_ENTITY)
:spawn()
local entity = evolved.builder()
:set(world)
:spawn()
-- destroy the world fragment that is attached to the entity
evolved.destroy(world)
-- the entity is destroyed together with the world fragment now
assert(not evolved.alive(entity))
id :: implementation-specific
entity :: id
fragment :: id
query :: id
system :: id
component :: any
storage :: component[]
default :: component
duplicate :: {component -> component}
execute :: {chunk, entity[], integer}
prologue :: {}
epilogue :: {}
set_hook :: {entity, fragment, component, component?}
assign_hook :: {entity, fragment, component, component}
insert_hook :: {entity, fragment, component}
remove_hook :: {entity, fragment, component}
each_state :: implementation-specific
execute_state :: implementation-specific
each_iterator :: {each_state? -> fragment?, component?}
execute_iterator :: {execute_state? -> chunk?, entity[]?, integer?}
TAG :: fragment
NAME :: fragment
UNIQUE :: fragment
EXPLICIT :: fragment
DEFAULT :: fragment
DUPLICATE :: fragment
PREFAB :: fragment
DISABLED :: fragment
INCLUDES :: fragment
EXCLUDES :: fragment
ON_SET :: fragment
ON_ASSIGN :: fragment
ON_INSERT :: fragment
ON_REMOVE :: fragment
GROUP :: fragment
QUERY :: fragment
EXECUTE :: fragment
PROLOGUE :: fragment
EPILOGUE :: fragment
DESTRUCTION_POLICY :: fragment
DESTRUCTION_POLICY_DESTROY_ENTITY :: id
DESTRUCTION_POLICY_REMOVE_FRAGMENT :: id
id :: integer? -> id...
pack :: integer, integer -> id
unpack :: id -> integer, integer
defer :: boolean
commit :: boolean
spawn :: <fragment, component>? -> entity
clone :: entity -> <fragment, component>? -> entity
alive :: entity -> boolean
alive_all :: entity... -> boolean
alive_any :: entity... -> boolean
empty :: entity -> boolean
empty_all :: entity... -> boolean
empty_any :: entity... -> boolean
has :: entity, fragment -> boolean
has_all :: entity, fragment... -> boolean
has_any :: entity, fragment... -> boolean
get :: entity, fragment... -> component...
set :: entity, fragment, component -> ()
remove :: entity, fragment... -> ()
clear :: entity... -> ()
destroy :: entity... -> ()
batch_set :: query, fragment, component -> ()
batch_remove :: query, fragment... -> ()
batch_clear :: query... -> ()
batch_destroy :: query... -> ()
each :: entity -> {each_state? -> fragment?, component?}, each_state?
execute :: query -> {execute_state? -> chunk?, entity[]?, integer?}, execute_state?
process :: system... -> ()
debug_mode :: boolean -> ()
collect_garbage :: ()
chunk :: fragment, fragment... -> chunk, entity[], integer
chunk_mt:alive :: boolean
chunk_mt:empty :: boolean
chunk_mt:has :: fragment -> boolean
chunk_mt:has_all :: fragment... -> boolean
chunk_mt:has_any :: fragment... -> boolean
chunk_mt:entities :: entity[], integer
chunk_mt:fragments :: fragment[], integer
chunk_mt:components :: fragment... -> storage...
builder :: builder
builder_mt:spawn :: entity
builder_mt:clone :: entity -> entity
builder_mt:has :: fragment -> boolean
builder_mt:has_all :: fragment... -> boolean
builder_mt:has_any :: fragment... -> boolean
builder_mt:get :: fragment... -> component...
builder_mt:set :: fragment, component -> builder
builder_mt:remove :: fragment... -> builder
builder_mt:clear :: builder
builder_mt:tag :: builder
builder_mt:name :: string -> builder
builder_mt:unique :: builder
builder_mt:explicit :: builder
builder_mt:default :: component -> builder
builder_mt:duplicate :: {component -> component} -> builder
builder_mt:prefab :: builder
builder_mt:disabled :: builder
builder_mt:include :: fragment... -> builder
builder_mt:exclude :: fragment... -> builder
builder_mt:on_set :: {entity, fragment, component, component?} -> builder
builder_mt:on_assign :: {entity, fragment, component, component} -> builder
builder_mt:on_insert :: {entity, fragment, component} -> builder
builder_mt:on_remove :: {entity, fragment} -> builder
builder_mt:group :: system -> builder
builder_mt:query :: query -> builder
builder_mt:execute :: {chunk, entity[], integer} -> builder
builder_mt:prologue :: {} -> builder
builder_mt:epilogue :: {} -> builder
builder_mt:destruction_policy :: id -> builder
evolved.lua
is licensed under the MIT License. For more details, see the LICENSE.md file in the repository.
---@param count? integer
---@return evolved.id ... ids
---@nodiscard
function evolved.id(count) end
---@param index integer
---@param version integer
---@return evolved.id id
---@nodiscard
function evolved.pack(index, version) end
---@param id evolved.id
---@return integer index
---@return integer version
---@nodiscard
function evolved.unpack(id) end
---@return boolean started
function evolved.defer() end
---@return boolean committed
function evolved.commit() end
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.spawn(components) end
---@param prefab evolved.entity
---@param components? table<evolved.fragment, evolved.component>
---@return evolved.entity
function evolved.clone(prefab, components) end
---@param entity evolved.entity
---@return boolean
---@nodiscard
function evolved.alive(entity) end
---@param ... evolved.entity entities
---@return boolean
---@nodiscard
function evolved.alive_all(...) end
---@param ... evolved.entity entities
---@return boolean
---@nodiscard
function evolved.alive_any(...) end
---@param entity evolved.entity
---@return boolean
---@nodiscard
function evolved.empty(entity) end
---@param ... evolved.entity entities
---@return boolean
---@nodiscard
function evolved.empty_all(...) end
---@param ... evolved.entity entities
---@return boolean
---@nodiscard
function evolved.empty_any(...) end
---@param entity evolved.entity
---@param fragment evolved.fragment
---@return boolean
---@nodiscard
function evolved.has(entity, fragment) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.has_all(entity, ...) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.has_any(entity, ...) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
---@return evolved.component ... components
---@nodiscard
function evolved.get(entity, ...) end
---@param entity evolved.entity
---@param fragment evolved.fragment
---@param component evolved.component
function evolved.set(entity, fragment, component) end
---@param entity evolved.entity
---@param ... evolved.fragment fragments
function evolved.remove(entity, ...) end
---@param ... evolved.entity entities
function evolved.clear(...) end
---@param ... evolved.entity entities
function evolved.destroy(...) end
---@param query evolved.query
---@param fragment evolved.fragment
---@param component evolved.component
function evolved.batch_set(query, fragment, component) end
---@param query evolved.query
---@param ... evolved.fragment fragments
function evolved.batch_remove(query, ...) end
---@param ... evolved.query queries
function evolved.batch_clear(...) end
---@param ... evolved.query queries
function evolved.batch_destroy(...) end
---@param entity evolved.entity
---@return evolved.each_iterator iterator
---@return evolved.each_state? iterator_state
---@nodiscard
function evolved.each(entity) end
---@param query evolved.query
---@return evolved.execute_iterator iterator
---@return evolved.execute_state? iterator_state
---@nodiscard
function evolved.execute(query) end
---@param ... evolved.system systems
function evolved.process(...) end
---@param yesno boolean
function evolved.debug_mode(yesno) end
function evolved.collect_garbage() end
---@param fragment evolved.fragment
---@param ... evolved.fragment fragments
---@return evolved.chunk chunk
---@return evolved.entity[] entity_list
---@return integer entity_count
---@nodiscard
function evolved.chunk(fragment, ...) end
---@return boolean
---@nodiscard
function evolved.chunk_mt:alive() end
---@return boolean
---@nodiscard
function evolved.chunk_mt:empty() end
---@param fragment evolved.fragment
---@return boolean
---@nodiscard
function evolved.chunk_mt:has(fragment) end
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.chunk_mt:has_all(...) end
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.chunk_mt:has_any(...) end
---@return evolved.entity[] entity_list
---@return integer entity_count
---@nodiscard
function evolved.chunk_mt:entities() end
---@return evolved.fragment[] fragment_list
---@return integer fragment_count
---@nodiscard
function evolved.chunk_mt:fragments() end
---@param ... evolved.fragment fragments
---@return evolved.storage ... storages
---@nodiscard
function evolved.chunk_mt:components(...) end
---@return evolved.builder builder
---@nodiscard
function evolved.builder() end
---@return evolved.entity
function evolved.builder_mt:spawn() end
---@param prefab evolved.entity
---@return evolved.entity
function evolved.builder_mt:clone(prefab) end
---@param fragment evolved.fragment
---@return boolean
---@nodiscard
function evolved.builder_mt:has(fragment) end
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.builder_mt:has_all(...) end
---@param ... evolved.fragment fragments
---@return boolean
---@nodiscard
function evolved.builder_mt:has_any(...) end
---@param ... evolved.fragment fragments
---@return evolved.component ... components
---@nodiscard
function evolved.builder_mt:get(...) end
---@param fragment evolved.fragment
---@param component evolved.component
---@return evolved.builder builder
function evolved.builder_mt:set(fragment, component) end
---@param ... evolved.fragment fragments
---@return evolved.builder builder
function evolved.builder_mt:remove(...) end
---@return evolved.builder builder
function evolved.builder_mt:clear() end
---@return evolved.builder builder
function evolved.builder_mt:tag() end
---@param name string
---@return evolved.builder builder
function evolved.builder_mt:name(name) end
---@return evolved.builder builder
function evolved.builder_mt:unique() end
---@return evolved.builder builder
function evolved.builder_mt:explicit() end
---@param default evolved.component
---@return evolved.builder builder
function evolved.builder_mt:default(default) end
---@param duplicate evolved.duplicate
---@return evolved.builder builder
function evolved.builder_mt:duplicate(duplicate) end
---@return evolved.builder builder
function evolved.builder_mt:prefab() end
---@return evolved.builder builder
function evolved.builder_mt:disabled() end
---@param ... evolved.fragment fragments
---@return evolved.builder builder
function evolved.builder_mt:include(...) end
---@param ... evolved.fragment fragments
---@return evolved.builder builder
function evolved.builder_mt:exclude(...) end
---@param on_set evolved.set_hook
---@return evolved.builder builder
function evolved.builder_mt:on_set(on_set) end
---@param on_assign evolved.assign_hook
---@return evolved.builder builder
function evolved.builder_mt:on_assign(on_assign) end
---@param on_insert evolved.insert_hook
---@return evolved.builder builder
function evolved.builder_mt:on_insert(on_insert) end
---@param on_remove evolved.remove_hook
---@return evolved.builder builder
function evolved.builder_mt:on_remove(on_remove) end
---@param group evolved.system
---@return evolved.builder builder
function evolved.builder_mt:group(group) end
---@param query evolved.query
---@return evolved.builder builder
function evolved.builder_mt:query(query) end
---@param execute evolved.execute
---@return evolved.builder builder
function evolved.builder_mt:execute(execute) end
---@param prologue evolved.prologue
---@return evolved.builder builder
function evolved.builder_mt:prologue(prologue) end
---@param epilogue evolved.epilogue
---@return evolved.builder builder
function evolved.builder_mt:epilogue(epilogue) end
---@param destruction_policy evolved.id
---@return evolved.builder builder
function evolved.builder_mt:destruction_policy(destruction_policy) end