Skip to content

Queries

A query describes a shape of entity: which components it has, and which it doesn’t. world.Query() starts the chain.

world.Query().With<Position, Velocity>()

matches every entity that has both a Position and a Velocity. .With<A, B, C>() collapses what would otherwise be three chained .With<A>().With<B>().With<C>() calls into one.

.WithMut<TComponent>() is With’s mutable counterpart, for TrySingle and foreach below, not for ForEach. A .ForEach() terminal always reads write-vs-read from its own lambda’s ref/in parameter modifiers, so WithMut and With make no difference there:

world.Query().WithMut<Ship, Transform, Velocity>()

matches the same entities as With<Ship, Transform, Velocity>() would, but TrySingle’s and foreach’s row fields come back mutable instead of read-only. Same arity cap and multi-type collapsing as With.

world.Query().With<Position, Velocity>()
.ForEach((ref position, in velocity) =>
{
position.X += velocity.X;
position.Y += velocity.Y;
});

Whether a component is read-only or mutable comes from ref/in on the callback’s parameters themselves, in the same order as the With calls that requested them.

Add a leading EntityView entity parameter to get the matched entity alongside its components:

world.Query().With<Position>()
.ForEach((EntityView entity, ref Position position) =>
{
if (position.Y < 0f) entity.DestroyEntity();
});

EntityView carries the same mutation methods you’d use inside a system’s Update, AddComponent, RemoveComponent, AddTag, DestroyEntity, queued through the command buffer. Works on ParallelForEach too.

For CPU-heavy per-entity work, .ParallelForEach runs the same shape across the thread pool instead of inline.

var total = 0;
world.Query().With<Position>()
.ParallelForEach(0, (in int _, ref Position position) => Interlocked.Increment(ref total));

For the common “there’s exactly one entity like this” case, a singleton ship, a scoreboard, .TrySingle(out row) skips the per-entity lambda entirely:

if (!world.Query().WithMut<Ship, Transform, Velocity>().TrySingle(out var row)) return;
row.Transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, row.Ship.Heading.Radians);

row exposes each matched component by its type name (row.Transform, row.Ship, …), the matched row.Entity, and the same mutation methods as EntityView. TrySingle returns false for zero matches, true with row populated for exactly one, and throws InvalidOperationException for more than one, a duplicate singleton is a real bug, not a state worth silently picking one.

Query<TShape> is directly foreach-able, for a body that needs to return early, accumulate into local state first, or otherwise doesn’t fit a single ForEach lambda:

foreach (var row in world.Query().With<Transform>().Has<Bullet>())
{
bulletEntities.Add(row.Entity);
bulletPositions.Add(row.Transform.Position);
}

Same row shape as TrySingle: named component fields, .Entity, and command-buffer-queued mutation methods.

Without, Has, and Any only narrow the query’s runtime filter, they never change its shape the way With does. That means each of them can be applied conditionally, and the result still compiles and matches the way you’d expect.

Unlike With, none of the three bind data to a ForEach parameter, so all three accept IComponent and ITag equally:

Filter IComponent ITag
With yes, binds its data no, nothing to bind
Without yes yes
Has yes yes
Any yes yes

Excludes entities with the given components or tags:

struct Frozen : ITag { }
world.Query().With<Position, Velocity>().Without<Frozen>()

matches every entity with a Position and a Velocity, and no Frozen.

Requires a component or tag without binding it to a ForEach parameter, unlike With. Reach for Has when you only need to know something’s there, not read its data:

world.Query().With<Position>().Has<Frozen>()
.ForEach((ref Position position) =>
{
// every match has Frozen, there's nothing to read from a tag anyway
});

Frozen is a tag, so With<Frozen>() wouldn’t even compile here, With only binds IComponent types, and a tag has no data to bind. Has works on either, that’s exactly why it exists: Has<Position>() is legal too, for when you need presence without the data.

Matches if at least one of the given components or tags is present, common with tags for “any status effect” style checks:

struct Burning : ITag { }
struct Chilled : ITag { }
world.Query().With<Position>().Any<Burning, Chilled>()

matches every entity with a Position that has Burning, Chilled, or both.

Because none of the three change the query’s shape, they can be assigned back conditionally:

struct HardcoreOnly : ITag { }
var query = world.Query().With<Position>();
if (hardcoreMode) query = query.Has<HardcoreOnly>();

WithRelation/WithoutRelation filter on relation edges, the structural links AddRelation creates between entities. WithRelation<T>() matches any entity with at least one edge of that relation, target unspecified. WithoutRelation<T>() excludes them. A relation is neither an IComponent nor an ITag, it’s its own IRelation type, a third category the table above doesn’t cover.

struct Targeting : IRelation { public float ThreatLevel; }
world.Query().WithRelation<Targeting>()
.ForEach(0, (in int _, in RelationLinks<Targeting> link) => { /* one match per entity with a Targeting edge */ });
world.Query().With<Position>().WithoutRelation<Targeting>()

A query you build once and run every tick is a system. See Systems.