Files
evolved.lua/MANUAL.md
BlackMATov 36a5e6ac51 manual wip
2025-05-16 21:13:10 +07:00

11 KiB

Manual

Identifiers

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.

Traits

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.

Singletons

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)

Chunks

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 the same fragments, they will be stored in the <health, stamina> chunk. If you have another entity with the fragments health, stamina, and mana, 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.

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

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

---@param self evolved.chunk
---@param ... evolved.fragment fragments
---@return evolved.component[] ... component_lists
function chunk_mt:components(...) end

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

Structural Changes

Every time we add 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)