Show HN: Evolved.lua – An Evolved Entity Component System for Lua

3 days ago 1

Evolved ECS (Entity-Component-System) for Lua

lua5.1 lua5.4 luajit 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.

You can install evolved.lua using luarocks with the following command:

luarocks install evolved.lua

Or just clone the repository and copy the evolved.lua file to your project.

To start using evolved.lua, read the Overview section first. It will give you a basic understanding of how the library works and how to use it. After that, check the full-featured 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.

Enjoy! :suspect:

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

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! :godmode:

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

Entities, Fragments, and Components

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))

evolved.DESTRUCTION_POLICY

evolved.DESTRUCTION_POLICY_DESTROY_ENTITY

evolved.DESTRUCTION_POLICY_REMOVE_FRAGMENT

---@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

evolved.chunk_mt:entities

---@return evolved.entity[] entity_list ---@return integer entity_count ---@nodiscard function evolved.chunk_mt:entities() end

evolved.chunk_mt:fragments

---@return evolved.fragment[] fragment_list ---@return integer fragment_count ---@nodiscard function evolved.chunk_mt:fragments() end

evolved.chunk_mt:components

---@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

evolved.builder_mt:has_all

---@param ... evolved.fragment fragments ---@return boolean ---@nodiscard function evolved.builder_mt:has_all(...) end

evolved.builder_mt:has_any

---@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

evolved.builder_mt:remove

---@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

evolved.builder_mt:unique

---@return evolved.builder builder function evolved.builder_mt:unique() end

evolved.builder_mt:explicit

---@return evolved.builder builder function evolved.builder_mt:explicit() end

evolved.builder_mt:default

---@param default evolved.component ---@return evolved.builder builder function evolved.builder_mt:default(default) end

evolved.builder_mt:duplicate

---@param duplicate evolved.duplicate ---@return evolved.builder builder function evolved.builder_mt:duplicate(duplicate) end

evolved.builder_mt:prefab

---@return evolved.builder builder function evolved.builder_mt:prefab() end

evolved.builder_mt:disabled

---@return evolved.builder builder function evolved.builder_mt:disabled() end

evolved.builder_mt:include

---@param ... evolved.fragment fragments ---@return evolved.builder builder function evolved.builder_mt:include(...) end

evolved.builder_mt:exclude

---@param ... evolved.fragment fragments ---@return evolved.builder builder function evolved.builder_mt:exclude(...) end

evolved.builder_mt:on_set

---@param on_set evolved.set_hook ---@return evolved.builder builder function evolved.builder_mt:on_set(on_set) end

evolved.builder_mt:on_assign

---@param on_assign evolved.assign_hook ---@return evolved.builder builder function evolved.builder_mt:on_assign(on_assign) end

evolved.builder_mt:on_insert

---@param on_insert evolved.insert_hook ---@return evolved.builder builder function evolved.builder_mt:on_insert(on_insert) end

evolved.builder_mt:on_remove

---@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

evolved.builder_mt:execute

---@param execute evolved.execute ---@return evolved.builder builder function evolved.builder_mt:execute(execute) end

evolved.builder_mt:prologue

---@param prologue evolved.prologue ---@return evolved.builder builder function evolved.builder_mt:prologue(prologue) end

evolved.builder_mt:epilogue

---@param epilogue evolved.epilogue ---@return evolved.builder builder function evolved.builder_mt:epilogue(epilogue) end

evolved.builder_mt:destruction_policy

---@param destruction_policy evolved.id ---@return evolved.builder builder function evolved.builder_mt:destruction_policy(destruction_policy) end
Read Entire Article