Skip to content

Archetypes & Chunks

genar edited this page Apr 16, 2023 · 2 revisions

Arch is an archetype ECS. An archetype ECS groups entities with the same set of components in tightly packed arrays for the fastest possible iteration performance. This has no direct effect on its API usage or the way you develop your game. But understanding the internal structure can help you to improve your game performance even more. However, it has a big impact on the internal structures being used and is the secret to its incredible performance.

Archetype

An archetype manages all entities with a common set of components. Like the world is used to manage ALL entities, the archetype is only used to manage a specific set of entities... all entities with the same component structure. The world stores those archetypes and accesses them to iterate, create, update and remove entities.

// Creates one entity, one archetype where all entities with Position and Velocity will be stored
var archetype = new ComponentType[]{ typeof(Position), typeof(Velocity) };
var entity = world.Create(archetype);                            

// Creates another entity, another archetype where all entities with Position, Velocity AND Rotation will be stored
var secondArchetype = new ComponentType[]{ typeof(Position), typeof(Velocity), typeof(Rotation) } ; 
var secondEntity = world.Create(secondArchetype);

You may probably now ask: "Why does this create two separate archtypes? Both entities share position and velocity, so they could be stored together."... Shared subsets of components do NOT matter... all that matters is the exact structure of an entity. Why is that? Because this way we can utilize the cache during iterations, however explaining this would probably break the scope of this documentation.

Chunks

Chunks are where the entities and their components are stored. They utilize dense packed contiguous arrays ( SoA ) to store and access entities with the same group of components. Its internal arrays are always 16KB in total, this is intended since 16kb fits perfectly into the L1 CPU Cache which gives us insane iteration speeds.

The internal structure simplified looks like this...

Chunk
[
    [Entity, Entity, Entity],
    [Position, Position, Position],
    [Velocity, Velocity, Velocity]
    ...
]

This way they are fast to (de)allocate which also reduces memory usage to a minimum. Each archetype contains multiple chunks and will create and destroy chunks based on the world's needs.

Archetype and Chunk usage

Arch gives you access to the internal structures as well. You will mostly not need this feature, however, it can be useful to leverage the performance, write custom queries or add new features.

var archetypes = world.Archetypes;  // Returns direct access to all archetypes of the world
var chunks = archetypes[0].Chunks   // Returns direct access to all chunks of an archetype
 
world.GetArchetypes(in queryDescription, myArchetypeList);  // Fills all archetypes fitting the query description into the passed list
world.GetChunks(in queryDescription, myChunkList);          // Fills all chunks fitting the query description into the passed list

var query = world.Query(in queryDescription);               // Creates a query
query.GetArchetypeIterator();                               // Returns iterator, iterating over only valid archetypes fitting the query
query.GetChunkIterator();                                   // Returns iterator, iterating over only valid chunks fitting the query

By accessing or using those methods, you will have the power to extend the features of this lib and access its underlaying foundation. It can be used e.g.:

  • Writing custom queries
  • Implementing different iteration techniques
  • Serialisation
  • Multithreading
  • SIMD
  • Code generation

Let's take a look at how the world.Query methods are implemented using that raw access of archetypes and chunks.

// Inside the world
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Query(in QueryDescription queryDescription, ForEach forEntity)
{
    var query = Query(in queryDescription);
    foreach (ref var chunk in query)
    {
        ref var entityFirstElement = ref chunk.Entity(0);
        foreach(var index in chunk)
        {
            ref readonly var entity = ref Unsafe.Add(ref entityFirstElement, entityIndex);
            forEntity(entity);
        }
    }
}

With the power of accessing Archetype and Chunk directly from the world, you could easily write such high-performance queries yourself. Great! Isn't it? :)
Let's look at a small example of how you could utilize those features.

Let's say we want to iterate over every second entity with a Position component since we don't wanna update ALL of them every tick. Well, you could do this easily like this e.g.

var queryDesc = new QueryDescription().WithAll<Position>();
var query = world.Query(in queryDesc); // Get all chunks for entities with a position component

foreach(ref var chunk in query){

    // Acess the position component array and loop over each position
    var positions = chunk.GetArray<Position>();
    for (var entityIndex = 0; entityIndex < chunk.Size; entityIndex+=2){ // <- Always skip one entity
    
        ref var pos = ref positions[entityIndex];
        pos.x++;
        pos.y++;

        Console.WriteLine($"{pos.x}/{pos.y}");
    }
}

How to avoid pitfalls

Since Archetype and Chunk do give you raw access to the underlying memory and foundation, there are some actions that should be avoided.

  • Adding/Removing entities directly to Archetype and Chunk, use always the World for this
  • Trimming or moving the internal arrays and hashmaps
Clone this wiki locally