Queries & Iteration¶
Find and iterate over entities by tag, component, or set membership.
Two entry points:
[ForEachEntity]— a method that becomes the body of an auto-generated loop. See Systems — ForEachEntity.World.Query()— a fluent builder for manualforeachloops.
ForEachEntity¶
// By tag
[ForEachEntity(typeof(GameTags.Player))]
void Execute(ref Position position, in Velocity velocity) { ... }
// By multiple tags
[ForEachEntity(typeof(BallTags.Ball), typeof(BallTags.Active))]
void Execute(in ActiveBall ball) { ... }
// By components (any template that declares these components, regardless of tags)
[ForEachEntity(MatchByComponents = true)]
void Execute(ref Position position, in Velocity velocity) { ... }
// By set membership
[ForEachEntity(typeof(GameTags.Particle), Set = typeof(SampleSets.Highlighted))]
void Execute(in ParticleView particle) { ... }
The named Tag = typeof(...) / Tags = new[] { typeof(...) } properties also work. The positional form above is the canonical style.
QueryBuilder¶
WorldAccessor.Query() returns a fluent QueryBuilder. Chain filters, then call a terminator.
Filters¶
// Tags
World.Query().WithTags<GameTags.Player>()
World.Query().WithTags<GameTags.Player, GameTags.Active>()
World.Query().WithoutTags<GameTags.Dead>()
// Components
World.Query().WithComponents<Position, Velocity>()
World.Query().WithoutComponents<Frozen>()
WithTags / WithoutTags and WithComponents / WithoutComponents come in 1–4-arg generic overloads. Combine them when a tag is shared across templates with different component layouts:
// Renderable entities that also have a Velocity component (skips static obstacles).
foreach (EntityHandle entity in World.Query()
.WithTags<CommonTags.Renderable>()
.WithComponents<Velocity>()
.Handles())
{
// ...
}
A query must have at least one filter — terminators assert otherwise.
Terminators¶
// Iterate entity handles — primary path
foreach (EntityHandle entity in World.Query().WithTags<GameTags.Player>().Handles())
{
ref Position pos = ref entity.Component<Position>(World).Write;
pos.Value.y += 1f;
}
// Count
int total = World.Query().WithTags<GameTags.Enemy>().Count();
// Single entity (asserts exactly one match) — returns an EntityHandle
EntityHandle player = World.Query().WithTags<GameTags.Player>().SingleHandle();
ref Health hp = ref player.Component<Health>(World).Write;
// Single, no-throw form
if (World.Query().WithTags<GameTags.Player>().TrySingleHandle(out var p))
{
ref readonly Position pos = ref p.Component<Position>(World).Read;
}
Other counting helpers on WorldAccessor: CountAllEntities(), CountEntitiesWithTags<T>(), CountEntitiesInGroup(GroupIndex).
Sets¶
InSet<T>() filters to members of the given set. Only one set filter per query — test additional sets inside the loop body.
foreach (var entity in World.Query()
.WithTags<GameTags.Particle>()
.InSet<HighlightedParticles>()
.Handles())
{
// ...
}
Aspect queries¶
Every aspect gets a generated Query() method that bundles component access into the iteration variable. Read and write through the aspect's properties instead of going through entity.Component<T>(World):
partial struct PlayerView : IAspect, IRead<Position>, IWrite<Health> { }
foreach (var player in PlayerView.Query(World).WithTags<GameTags.Player>())
{
float3 pos = player.Position;
player.Health -= 1f;
}
Aspect queries do not auto-filter by the aspect's declared components — always scope with WithTags<…>(), MatchByComponents(), or InSet<…>().
// Match by the aspect's component shape, regardless of tags.
foreach (var boid in Boid.Query(World).MatchByComponents()) { ... }
PlayerView.Query(World).WithTags<GameTags.Player>().Single() works as well.
GlobalIndex¶
When iteration spans multiple groups, [GlobalIndex] int gives each entity a unique packed index from 0 to total − 1. Useful for filling a contiguous output array shared across groups:
[ForEachEntity(typeof(GameTags.Boid))]
void Execute(in Position pos, [GlobalIndex] int globalIndex)
{
_outputs[globalIndex] = pos.Value;
}
GroupSlices¶
For performance-critical loops needing direct buffer access, iterate per group via GroupSlices() rather than per entity. See GroupSlices.
Where queries are allowed¶
The accessor's role determines which groups a query can resolve — Fixed-role accessors can't iterate [VariableUpdateOnly]-only groups, for example. The query asserts at the terminator if it would otherwise return groups the accessor isn't permitted to read.