Queries
A query describes a shape of entity: which components it has, and which it doesn’t. world.Query() starts the chain.
Matching components with With
Section titled “Matching components with With”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
Section titled “WithMut”.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.
Running work
Section titled “Running work”ForEach
Section titled “ForEach”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.
Reading the matched entity
Section titled “Reading the matched entity”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.
ParallelForEach
Section titled “ParallelForEach”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));TrySingle
Section titled “TrySingle”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.
foreach
Section titled “foreach”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.
Conditional filters
Section titled “Conditional filters”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 |
Without
Section titled “Without”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.
Applying them conditionally
Section titled “Applying them conditionally”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>();Filtering by relation
Section titled “Filtering by relation”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.