Skip to content

Query techniques

genar edited this page Jan 16, 2023 · 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 Enumretion
  • 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.

Example:

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

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

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.X;
       pos.Y += vel.Y;
   }
}

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.

// BaseSystem provides several usefull methods for interacting and structuring systems
public partial class MovementSystem : BaseSystem<World, float>{

    public MovementSystem(World world) : base(world) {}
    
    // Generates a query and calls this annotated method for all entities with position and velocity components.
    [Query]  // Marks method inside BaseSystem for source generation.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void MoveEntity([Data] ref float time, ref Position pos, ref Velocity vel)  // Entity requires atleast those components. "in Entity" can also be passed. 
    {
        pos.X += time * vel.X;
        pos.Y += time * vel.Y;
    }
    
    /// Generates a query and calls this method for all entities with velocity, player, mob, particle, either moving or idle and no dead component.
    /// All, Any, None are seperate attributes and do not require each other.
    [Query]
    [All<Player, Mob, Particle>, Any<Moving, Idle>, None<Alive>]  // Adds filters to the source generation to adress certain entities. 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void StopDeadEntities(ref Velocity vel)
    {
        vel.X = 0;
        vel.Y = 0;
    }
}
Clone this wiki locally