4 — Querying & views
Spawning and reading by id only gets you so far. Real work means asking questions of your data — "every Imperial on this planet", "everything within 15 metres of here", "what changed since last tick?". That's this chapter. It's the most feature-dense in the guide, because querying is where you'll spend a lot of your time.
Two shapes of question:
- One-shot queries — ask now, get an answer (
Execute/Count/Any/ iterate). - Live views — ask once, keep a result set that stays current as data changes, and tells you the delta each time.
Everything starts from tx.Query<TArchetype>().
1. Building a query
Query<Character>() returns a builder you refine with chainable filters. Nothing runs until a terminal call (§2).
Which entities — by component shape
tx.Query<PlayerArch>()
.With<Membership>() // only players who have a Membership component
.Without<Wallet>() // …and no Wallet at all
.Enabled<Session>() // …whose Session component is currently enabled (i.e. online)
With/Without filter on component presence; Enabled/Disabled filter on the per-entity enable flag — a cheap per-entity toggle that costs no storage and no index. It's how the sample shard models states rather than deleting data: an offline player is one whose Session is disabled, a depleted deposit is one whose Deposit is disabled (the data stays readable), a broken harvester is one whose MaintenanceState is disabled.
Exclude<TArch>() drops a whole sub-archetype — useful with the archetype hierarchy from ch.2 §4:
tx.Query<StructureArch>() // polymorphic: every Harvester AND every Factory
.Exclude<FactoryArch>() // …minus the factories
💡 Polymorphic by default.
Query<StructureArch>()matches the archetype and its whole subtree. When you want exactly one shape and none of its descendants, usetx.QueryExact<HarvesterArch>(). This is the payoff of declaring a base archetype: one query for the family, one for a leaf.
Which entities — by field value
This is the distinction that matters most for performance:
// Indexed-field predicate → the engine drives the scan from the index (fast, selective)
tx.Query<Character>().WhereField<Faction>(f => f.Value == Factions.Imperial)
// Free predicate → evaluated per candidate entity (broad scan)
tx.Query<Character>().Where<Ham>(h => h.Health < h.MaxHealth)
💡
WherevsWhereField— pick deliberately.Where<T>(lambda)takes any C# predicate and runs it against every entity the rest of the query admits — total freedom, linear cost.WhereField<T>(expression)is restricted to an indexed field and a comparable expression, which lets the engine narrow the candidates through the index instead of scanning, and is the form that backs an incremental live view (§3) — a freeWherecan still back a pull view that recomputes on refresh. Rule of thumb: filter on an indexed field withWhereField; useWherefor computed or non-indexed conditions (likeHealth < MaxHealth, which compares two fields and can't be a simple index lookup). You can chain both —WhereFieldto narrow,Whereto refine.
Which entities — by geometry
If a component has [SpatialIndex] (our Bounds, from ch.2), query it spatially — these run off the spatial index (Execute / Count / Any all apply the predicate):
tx.Query<Character>().WhereNearby<Bounds>(x, y, 0f, 15f).Execute() // x, y, z, radius
tx.Query<Character>().WhereInAABB<Bounds>(minX, minY, 0f, maxX, maxY, 0f).Execute() // inside a box
tx.Query<Character>().WhereRay<Bounds>(ox, oy, 0f, dx, dy, 0f, 50f).Execute() // origin, dir, maxDist
A spatial predicate composes with the field/Where filters above — WhereNearby<Bounds>(x, y, 0f, 15f).WhereField<Faction>(f => f.Value == Factions.Imperial) returns the intersection (in range and Imperial).
⚠️ The spatial index is maintained at the tick fence (ch.5); from a bare transaction, run
dbe.WriteTickFence(n)once after spawning so the index reflects current positions before you query.
Ordering & paging
tx.Query<PlayerArch>()
.WhereField<Player>(p => p.Level >= 10)
.OrderByFieldDescending<Player, int>(p => p.Level) // or OrderByField for ascending
.Skip(10).Take(20)
.ExecuteOrdered(); // ordered results need their own terminal
⚠️ Three constraints that will bite you otherwise.
OrderByFieldrequires (1) aWhereFieldfirst — that's what tells the engine which component table to order within; (2) an indexed field to order by — ordering is served by walking the index in key order, so an unindexed field throws; and (3) theExecuteOrdered()terminal, which returns an orderedList<EntityId>. PlainExecute()returns an unordered set and throws if you attached anOrderBy— deliberately, rather than silently discarding your ordering.Skip/Takelikewise require anOrderBy(paging an unordered set is meaningless).
2. Running a query
A query does nothing until a terminal call. Pick the one that matches what you need:
HashSet<EntityId> ids = q.Execute(); // materialise all matches (unordered)
List<EntityId> ordered = q.ExecuteOrdered();// ordered matches (requires OrderByField)
int n = q.Count(); // just how many
bool any = q.Any(); // does at least one match?
foreach (EntityId id in q) // iterate matches without a HashSet
{
var ham = tx.Open(id).Read(Character.Ham);
// …react to each match…
}
Execute is the workhorse; Count/Any short-circuit when you don't need the entities; the foreach form iterates its own pre-collected match list instead of building a HashSet — cheaper than Execute, but not fully allocation-free streaming.
💡 Know which scan you triggered.
Executepicks one of three paths automatically: a targeted scan when you usedWhereField(index-driven), a spatial scan when you used a spatial predicate (spatial-index-driven), or a broad scan otherwise (walk the archetype, apply anyWherepredicate per entity). The takeaway for cost: aWhereField/spatial query stays cheap as the archetype grows; a pure-Wherequery is linear in archetype size. Both are correct — choose with your data sizes in mind.
The running example's headline query — Imperials who are hurt — combines the tools:
var woundedImperials = tx.Query<Character>()
.WhereField<Faction>(f => f.Value == Factions.Imperial) // index-narrowed
.Where<Ham>(h => h.Health < h.MaxHealth) // refined per entity
.Execute();
3. Live views — results that stay current
A one-shot query is a snapshot answer. A view is a result set that you keep and refresh, and that reports what changed each time — exactly what a reactive system or a UI needs.
using var hurt = tx.Query<Character>()
.Where<Ham>(h => h.Health < h.MaxHealth / 2) // two fields → a pull view
.ToView();
// later, each tick:
hurt.Refresh(tx); // bring the view up to date
foreach (long pk in hurt.GetDelta().Added)
RaiseAlarm(pk); // characters that just dropped below half health
foreach (long pk in hurt.GetDelta().Removed)
ClearAlarm(pk); // …or healed back above / left the set
hurt.ClearDelta(); // reset the delta for the next cycle
A view gives you:
- Membership & iteration —
view.Contains(id), andforeachover its current entities. Refresh(tx)— re-evaluate against the latest data.- A delta —
view.GetDelta()returnsAdded/Removed/Modified(entity keys), andClearDelta()resets it for the next round.
💡 Three flavours of view. A view built on an indexed
WhereFieldpredicate (field vs. a constant, e.g.WhereField<Faction>(f => f.Value == Factions.Imperial)) updates incrementally: the engine watches the index and moves only the entities that actually crossed the boundary — never re-running the whole query. A bare archetype view with no predicate at all (tx.Query<Unit>().ToView()) is a membership view: it subscribes to the spawn/destroy channel and costs O(1) on a quiet tick, O(changes) when entities enter or leave — it never rescans either. A view built on a freeWhere(like our "below half health", which compares two fields and so can't be an index lookup) is a pull view: recomputed in full onRefresh. All three report the sameAdded/Removed/Modifieddelta — the difference is cost, not capability. Either way, a reactive UI or a streaming server is built on views, not on pollingExecute.
📌 A view is also how you feed a system. Every
QuerySystemin ch.5 takes a long-lived view as its input set — you build it once at startup and the runtime hands your system its entities every tick. Views aren't only for UI; they're the standing subscription the tick loop runs on.
4. Subscriptions — pushing state to clients
When the consumer is remote (a connected game client, a browser, another process), subscriptions are what carry engine state there and keep it current, so a client can mirror "the characters near my player" without re-querying.
You declare what each archetype exposes and which profile a session follows — the whole world, a radius around an entity, or a footprint the client sends — and after each write you want seen, your system calls ctx.Subscriptions.Replicate(…). The engine compares and encodes each changed entity once, gives every session the changes around it, and drains typed commands from clients into the next tick, where ordinary systems validate them. Clients decode against a catalog rather than C# type layouts, so browsers and native clients share one wire.
Subscriptions don't read views: they're built from declared projections and answered from geometry. They have a chapter of their own — ch.7, serving remote clients. Everything in §3 above is unaffected.
5. Reading in parallel
Everything above runs on one transaction (one thread). When a query system needs to fan a read-only pass across many worker threads at a single consistent snapshot, that's the PointInTimeAccessor — one frozen TSN, one accessor per worker, zero per-entity locking. It's the read engine behind parallel systems, so it's covered with the runtime in ch.5. For now: know that "query a million entities across all cores at one snapshot" is a first-class, supported pattern.
💡 The name is about parallelism, not time travel.
PointInTimeAccessorfreezes one current snapshot so every worker reads the same consistent moment — it does not read historical/past versions (Typhon has no user-facing as-of-past-version read API). The only "as-of" you get is snapshot isolation: a consistent view fixed at your transaction's start. See the cheat sheet's naming traps.
A note on planning: the engine keeps lightweight statistics about component data and uses them when choosing how to run targeted scans. Like indexes, it's bookkeeping you benefit from but never maintain.
🧭 What's next
You can now find data (one-shot) and observe it (live views). The last big piece is running logic over it continuously:
- Chapter 5 — Systems & the tick loop: systems, the scheduler, parallel reads with
PointInTimeAccessor, and how one UoW per tick drives the whole thing. - Chapter 6 — Operating & going deeper: observability, resource budgets, error handling, and the map into the in-depth reference.
🧩 Key concepts & types
Concepts: Query · View · Subscription · Snapshot isolation · PointInTimeAccessor.
Exact calls: tx.Query<TArch>() / tx.QueryExact<TArch>() → EcsQuery · With / Without / Exclude / Enabled / Disabled · Where (broad) vs WhereField (indexed) · WhereNearby / WhereInAABB / WhereRay · OrderByField / OrderByFieldDescending / Skip / Take · Execute / ExecuteOrdered / Count / Any / foreach · ToView → EcsView (Contains / Refresh / GetDelta / ClearDelta).