Skip to content

Query Matching ​

Neofox: hug_duck_heart THE HEART OF ECS!

Match Expressions define which Entities your Query contains – by the Components they must have, may have, or must not have.

Quick Reference ​

ExpressionDescription
Has<C>(Match = default)Include only Entities that have component C
Not<C>(Match = default)Exclude Entities that have component C
Any<C>(Match = default)Match Entities with at least one of the Any components
Neofox: science Matching (Intermediate, see Keys)

Concrete Match Expressions ​

ExpressionDescription
Match.Plaindefault - Match only plain components (no relation)
Entity eRelation Key matching specific Entity
Link.With()Relation Key matching specific Link

Wildcard Match Expressions ​

ExpressionDescription
Match.AnyMatch any component, with or without target
Match.TargetMatch any actual target (Object or Entity)
Match.ObjectMatch only Object Links
Match.EntityMatch only Entity Relations

Two Intertwined Concepts ​

Neofox: book MATCH TYPE

Matching groups/selects Entities by the Components they must have, may have, or must not have.

Queries let us find and access any set of Entities & Components extremely quickly.

Neofox: heart MATCH TARGET

Targets further group entities by an optional secondary key, like an Object or another Entity.

Match Expressions with targets constitute many-to-1 Relations (Entity-Entity, or Entity-Object).

Relations can be backed by any Component type – even their Targets themselves or Shareable Components on them!

Both have low, usually zero cost – at runtime, as well as compile and design times. This is foundational for how Archetype-based ECS can be so fast, intuitive, and efficient.

Teaser ​

Together, Types and Targets unlock Entity relations and interactions in elegant, expressive ways that in many other ECS might seem like pure science fiction.

a fennec wearing a futuristic VR headset

The 3-Body-Problem recipe and N-Body-Problem demo illustrate how to use Match Expressions to simulate complex systems of mutually interacting Entities.

Building a Query ​

A match expression combines a Match Type and a Target (or Identity). Any number of expressions can be passed to the QueryBuilder to specify what to include and exclude.

Neofox: peek_owo DARE TO PEEK: What an expressive smörgåsbord!

Query Your Friends! ​

This query is a party invitation! It includes each entity with a Name and PlayList component, but then gets picky:

cs
var partyGoers = world.Query<Name, PlayList>() // "()" means Match.Plain
    .Has<Fox>()                    // must be a fox
    .Has<Friendship>(you)          // has friendship relation to "you"
    .Has<Friendship>(me)           // and to "me"
    .Not<Sleeping>()               // not asleep
    .Any<Pet>()                    // has either a pet...
    .Any<Plush>()                  // ...or a plush
    .Has<Pizza>()                  // has pizza
    .Not<Likes>(pineapple)         // no Likes relation to the pineapple Entity
    .Stream();

// Last minute additions via stream filters!
var checkedGoers = partyGoers
    .Has(Comp<Vaccinated>.Plain)
    .Not(Comp<Sick>.Plain);

checkedGoers.For((ref name, ref playlist) =>
{
    DeeJay.Instruct($"{name} is coming, play something from {playlist.entries}");
});

Match Types ​

From the start, a Query includes only Entities that match all of its Stream Types. This applies regardless of whether it's a Plain Component, Entity-Entity Relation, or Object Link – unless expressly specified in the QueryBuilder.

Neofox: magnify BEHIND THE SCENES: What does a Query even DO?

Each compiled Query maintains a collection of all Archetypes it matches (and a filtered subset). When iterating, the Query processes each Archetype in deterministic order.

Whenever a new Archetype materializes, the World notifies all matching Queries of its existence.

The Three Main Match Type Expressions ​

ExpressionLogicDescription
Has<C>()A AND B AND CInclude only Entities that have the component
Not<C>()NOT A AND NOT BExclude Entities that have the component
Any<C>()A OR B OR CMatch Entities with at least one of the Any components
cs
var query = world.Query<Position, Velocity>()
    .Has<Player>()        // must have Player
    .Not<Dead>()          // must not have Dead
    .Any<Buff>()          // must have either Buff...
    .Any<PowerUp>()       // ...or PowerUp (or both)
    .Stream();
Neofox: magnify BEHIND THE SCENES: QueryBuilder?

Technically, these are methods on QueryBuilder<> instances. In practice, you acquire them via World.Query<>() and chain the fluent interface to configure and compile immediately.

Match Targets ​

Stream Types and Match Expressions can both specify targets. world.Query<ST1, ST2>() is equivalent to world.Query<ST1, ST2>(Match.Plain, Match.Plain) — to match Relations or Object Links, say so explicitly.

Neofox: solder THIS IS OUR MAIN TOOL

In ECS, the presence of a component often carries meaning in itself. Queries expose powerful, performant matching based on such presence.

Wildcards ​

WildcardMatches
Match.PlainOnly plain components (no relations) (default for Stream Types)
Match.AnyAny target, including plain
Match.TargetAny actual target (Object or Entity, not Plain)
Match.ObjectOnly Object Links
Match.EntityOnly Entity Relations
cs
// Match entities with any Damage relation (to any entity)
var damaged = world.Query<Health>()
    .Has<Damage>(Match.Entity)
    .Stream();

// Match entities with a specific relation target
var followersOfBob = world.Query<Position>()
    .Has<Following>(bob)
    .Stream();

Neofox: knives WILDCARDS CUT BOTH WAYS

Wildcards aren't just for matching – Remove<C>(Match) accepts them on Entities, EntityRefs (inside runners), Batches, and Templates to strip all matching components at once.

Conflicting Match Expressions ​

Some expressions can cause a Query to always be empty:

Neofox: think CONFLICT EXAMPLES

cs
var enemies = world.Query<Enemy>()
    .Has<Position>().Not<Position>()  // Always empty!
    .Has<Objective>(Match.Target).Not<Objective>(Match.Entity)  // Only Object Links match
    .Stream();

Neofox: book SAFETY NOTE

As of fennecs 0.5.1, the safety check that threw on conflicting expressions is removed (feedback welcome!).

Query vs. Filter ​

Sometimes you need to narrow down a Query dynamically:

  • Perform an action on all followers of a specific character
  • Pre-fetch data and pass it as a Uniform to a Query Runner

Neofox: think HARD-BAKED CRITERIA

Exclusion criteria in the QueryBuilder are immutable. What if bob despawns? You'd need a whole new Query!

cs
var friendsInNeed = world.Query<Friend>()
    .Has<Owes>(Match.Entity)
    .Has<Owes>(bob)  // Hard-baked! Problematic if bob despawns
    .Not<Owes>(me)
    .Stream();

Neofox: thumbsup USE FILTERS INSTEAD!

Filters have similar performance but can be reconfigured on the fly. Streams are lightweight value types – Has/Not create a filtered view, and the original stream stays around, unfiltered:

cs
var friendsInNeed = world.Query<Friend>()
    .Has<Owes>(Match.Entity)
    .Stream();

// Dynamic filtering!
var owingBobNotMe = friendsInNeed
    .Has(Comp<Owes>.Matching(bob))
    .Not(Comp<Owes>.Matching(me));
owingBobNotMe.For(PayOffDebt);

// Reconfigure when needed - just make another filtered view!

When to Use Filters ​

Use CaseRecommendation
Target entity may despawnUse Filters
Many entities share same relationUse Filters (faster iteration)
Relations change frequentlyUse broader Query + Filters
Static, unchanging criteriaHard-baked Query is fine