-
-
Notifications
You must be signed in to change notification settings - Fork 91
Archetypes & Chunks
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.
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 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.
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}");
}
}
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
andChunk
, use always theWorld
for this - Trimming or moving the internal arrays and hashmaps