Skip to content

Query techniques

genar edited this page Feb 5, 2024 · 11 revisions

Arch has a number of different ways to search and iterate over entities.

While this may seem a bit complex at first, each of these ways has advantages and disadvantages and is perfect for specific situations. In this article we will take a closer look at them.

Overview

There are currently 4 different query Apis that you can use as you wish.

  • World.Query
  • World.InlineQuery
  • Custom Enumeration
  • Query generation using Arch.Extended

We will leave out the parallel methods for the time being, since they do not represent a new type of query, but only supplement it with multithreading. The same applies to the entity variants, which simply pass the entity.

World.Query

Advantages:

  • Less code
  • Can pass methods as it uses delegate
  • Great for prototypes

Disadvantages:

  • Uses delegates, which is slow that the code can not be inlined.
  • All components can only be passed with ref, no security.
  • Rather slow compared to other techniques, still fast though.
  • Enclosure copies probably.
World.Query(in desc, (ref Position pos, ref Velocity vel) => {
   ...
});

or

World.Query(in desc, (in Entity en, ref Position pos, ref Velocity vel) => {
   ...
});

World.InlineQuery (InlineQuery Wiki)

Advantages:

  • Damn fast
  • Uses struct and interface, can be inlined
  • Allows an order and modularization of the code

Disadvantages:

  • Requires more boilerplate code
  • All components can only be passed with ref, no security.
public struct VelocityUpdate : IForEach<Position, Velocity> {

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Update(ref Position pos, ref Velocity vel) { 
        pos.x += vel.x;
        pos.y += vel.y;
    }
}

world.InlineQuery<VelocityUpdate, Position, Velocity>(in queryDescription);  // You can also pass the struct instance with ref

or

world.InlineEntityQuery<VelocityUpdate, Position, Velocity>(in queryDescription); // When you use IForEachWithEntity

Custom enumeration

Advantages:

  • The fastest query variant
  • Is also inlined
  • Allows custom query logic
  • SIMD
  • Own multithreading
  • Great for hotpaths

Disadvantages:

  • Requires the most boilerplate code
  • Bad for prototypes or fast coding
var query = world.Query(in desc);
foreach(ref var chunk in query.GetChunkIterator())
{
   var references = chunk.GetFirst<Position, Velocity>;  // chunk.GetArray, chunk.GetSpan...
   foreach(var entity in chunk)                          // Iterate over each row/entity inside chunk
   {
       ref var pos = ref Unsafe.Add(ref references.t0, entity);   // Get fitting component for each entity/row
       ref var vel = ref Unsafe.Add(ref references.t1, entity);

       pos.X += vel.Dx;
       pos.Y += vel.Dy;
   }
}

Note

The syntax differs slightly for .Net Standard, where it is .t0.Value.

Query generation using Arch.Extended

Advantages:

  • Same as the custom query
  • Less code
  • Declarative syntax
  • Allows passing on data into the query with ease
  • You can use in, ref and out for components

Disadvantages:

  • Requires Arch.Extended
  • Code generator, probably not available for all platforms

The recommended way that combines the best of all previous variants is Arch.Extended. Through the source generator, you write less boilerplate code and it automatically generates a query with the best performance.

[Query] 
// [All<...>, Any<...>, None<...>] to filter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MoveEntity([Data] ref float time, ref Position pos, ref Velocity vel)  
{
    pos.X += time * vel.X;
    pos.Y += time * vel.Y;
}