Table of Contents

Queries Guide

Queries are how you find and iterate over entities with specific components. This guide covers query patterns and optimization.

Basic Queries

Query entities by their components:

// All entities with Position
foreach (var entity in world.Query<Position>())
{
    ref var pos = ref world.Get<Position>(entity);
    Console.WriteLine($"{entity}: ({pos.X}, {pos.Y})");
}

// Entities with Position AND Velocity
foreach (var entity in world.Query<Position, Velocity>())
{
    ref var pos = ref world.Get<Position>(entity);
    ref readonly var vel = ref world.Get<Velocity>(entity);

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

Multi-Component Queries

Query up to 4 component types directly:

// Two components
foreach (var entity in world.Query<Position, Velocity>())
{ }

// Three components
foreach (var entity in world.Query<Position, Velocity, Health>())
{ }

// Four components
foreach (var entity in world.Query<Position, Velocity, Health, Damage>())
{ }

Query Filters

With Filter

Include entities that also have specific components:

// Entities with Position AND Velocity AND the Player tag
foreach (var entity in world.Query<Position, Velocity>().With<Player>())
{
    // Only player entities
}

// Chain multiple With filters
foreach (var entity in world.Query<Position>().With<Enemy>().With<Visible>())
{
    // Visible enemies only
}

Without Filter

Exclude entities that have specific components:

// Entities with Position but NOT Disabled
foreach (var entity in world.Query<Position>().Without<Disabled>())
{
    // Skip disabled entities
}

// Chain multiple Without filters
foreach (var entity in world.Query<Position>().Without<Disabled>().Without<Dead>())
{
    // Active, living entities only
}

Combining Filters

// Complex filter: Enemies that are active (not disabled, not dead)
foreach (var entity in world.Query<Position, Velocity>()
    .With<Enemy>()
    .Without<Disabled>()
    .Without<Dead>())
{
    // Active enemy entities
}

Query Results

Queries return an enumerable of Entity:

var enemies = world.Query<Position>().With<Enemy>();

// Count matching entities
int count = enemies.Count();

// Convert to list
var list = enemies.ToList();

// Check if any match
bool hasEnemies = enemies.Any();

Component Access in Queries

Mutable Access

foreach (var entity in world.Query<Position, Velocity>())
{
    // Mutable - can modify
    ref var pos = ref world.Get<Position>(entity);
    pos.X += 1;
}

Read-Only Access

foreach (var entity in world.Query<Position, Velocity>())
{
    // Read-only - prevents accidental modification
    ref readonly var vel = ref world.Get<Velocity>(entity);
    float speed = MathF.Sqrt(vel.X * vel.X + vel.Y * vel.Y);
}

Query Patterns

Processing All Entities of a Type

public class MovementSystem : SystemBase
{
    public override void Update(float deltaTime)
    {
        foreach (var entity in World.Query<Position, Velocity>())
        {
            ref var pos = ref World.Get<Position>(entity);
            ref readonly var vel = ref World.Get<Velocity>(entity);

            pos.X += vel.X * deltaTime;
            pos.Y += vel.Y * deltaTime;
        }
    }
}

Filtering by Tag

// Players only
foreach (var entity in World.Query<Position>().With<Player>())
{
    // Handle player movement
}

// Enemies only
foreach (var entity in World.Query<Position>().With<Enemy>())
{
    // Handle enemy AI
}

Skipping Disabled Entities

foreach (var entity in World.Query<Position, Velocity>().Without<Disabled>())
{
    // Skip entities marked as disabled
}

Finding Single Entities

// Get first matching entity (or Entity.Null if none)
var player = World.Query<Position>().With<Player>().FirstOrDefault();

if (player.IsValid && World.IsAlive(player))
{
    ref var pos = ref World.Get<Position>(player);
}

Query Caching

KeenEyes automatically caches query results for performance:

  • First query execution computes matching archetypes
  • Subsequent queries with the same filters return cached results
  • Cache is automatically invalidated when archetypes change

You don't need to manually manage query caching - it happens automatically.

Performance Tips

Prefer Specific Queries

// ❌ Less efficient: Query all, then filter manually
foreach (var entity in world.Query<Position>())
{
    if (world.Has<Velocity>(entity) && world.Has<Enemy>(entity))
    {
        // ...
    }
}

// ✅ More efficient: Use query filters
foreach (var entity in world.Query<Position, Velocity>().With<Enemy>())
{
    // ...
}

Avoid Queries in Hot Loops

// ❌ Creates query object each frame
public override void Update(float deltaTime)
{
    foreach (var entity in World.Query<Position, Velocity>())
    {
        // Inner loop - avoid querying here
        foreach (var other in World.Query<Position>().With<Enemy>())
        {
            // O(n*m) queries!
        }
    }
}

// ✅ Better: Collect entities first, then process
public override void Update(float deltaTime)
{
    var enemies = World.Query<Position>().With<Enemy>().ToList();

    foreach (var entity in World.Query<Position, Velocity>())
    {
        foreach (var enemy in enemies)
        {
            // Reuse collected list
        }
    }
}

Use Tags for Fast Filtering

Tags are zero-size and don't store data, making them efficient for filtering:

// Efficient: Tag-based filtering
foreach (var entity in world.Query<Position>().With<Renderable>())
{
    // Render entity
}

Common Patterns

Player Input System

foreach (var entity in World.Query<Position, Velocity>().With<Player>())
{
    ref var vel = ref World.Get<Velocity>(entity);

    // Handle input - only affects players
    if (Input.IsKeyDown(Keys.Right)) vel.X = 5;
    if (Input.IsKeyDown(Keys.Left)) vel.X = -5;
}

Collision Detection

var collidables = World.Query<Position, Collider>().ToList();

for (int i = 0; i < collidables.Count; i++)
{
    for (int j = i + 1; j < collidables.Count; j++)
    {
        var a = collidables[i];
        var b = collidables[j];

        ref readonly var posA = ref World.Get<Position>(a);
        ref readonly var posB = ref World.Get<Position>(b);

        if (CheckCollision(posA, posB))
        {
            // Handle collision
        }
    }
}

Cleanup System

var buffer = new CommandBuffer();

foreach (var entity in World.Query<Health>())
{
    ref readonly var health = ref World.Get<Health>(entity);
    if (health.Current <= 0)
    {
        buffer.Despawn(entity);
    }
}

buffer.Flush(World);

Next Steps