Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions doc/cookbook/appetizers/Thanos.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: 5. Thanos (Filters)
outline: [2, 3]
order: 5
description: Teaches fennecs query filtering with Subset and Exclude, then uses Stream.Despawn to bulk-remove half the entity population in one call.
description: Teaches fennecs stream filtering with Has and Not, then uses FilteredStream.Despawn to bulk-remove half the entity population in one call.
---

# How to Snap ~25% of the World away
Expand All @@ -19,16 +19,16 @@ All `.csproj` and `.cs` files are [over here on Github!](https://github.com/outf
### Premise
Hey there, mighty Titan (who flunked probabilitics)! Ready to bring perfect balance to your `fennecs.World`?

In this example, we'll show you how to use fennecs' `Query.Subset` and `Query.Exclude` methods to ~~snap away~~ `Despawn` half the entities in your world.
In this example, we'll show you how to use fennecs' `Has` and `Not` stream filters to ~~snap away~~ `Despawn` half the entities in your world.
Comment thread
thygrrr marked this conversation as resolved.
Resolved

::: details SPOILER
Well... randomly half of randomly half!
:::

### How it works
First we create a bunch of entities, give some of them a "Lucky" and some of them a "Unlucky" component, then we filter our Query to target the Unlucky and move the Lucky ones out of harm's way.
First we create a bunch of entities, then flip two coins for each one to hand out "Lucky" and "Unlucky" components. Next, we narrow down our Stream with the `Has` and `Not` filters — targeting the Unlucky while leaving anyone Lucky out of harm's way.

Finally, we unleash the power of the ~~Infinity Gauntlet~~ `Stream<>.Despawn()` to bring an awkward balance to the Universe.
Finally, we unleash the power of the ~~Infinity Gauntlet~~ `FilteredStream.Despawn()` to bring an awkward balance to the Universe.

I'm sure you already see that nothing can go wrong! Let's get snapping!

Expand Down
2 changes: 1 addition & 1 deletion doc/docs/Advanced/Expressions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Creates an expression for a "plain" component – one with no relation target.
var posExpr = Comp<Position>.Plain;

// Use in stream filters
stream.Subset = [Comp<Position>.Plain];
var filtered = stream.Has(Comp<Position>.Plain);
```

### `Comp<R>.Matching(Entity relation)`
Expand Down
22 changes: 9 additions & 13 deletions doc/docs/Basic/Queries/Matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,11 @@ var partyGoers = world.Query<Name, PlayList>() // "()" means Match.Plain
.Stream();

// Last minute additions via stream filters!
partyGoers = partyGoers with
{
Subset = [Comp<Vaccinated>.Plain],
Exclude = [Comp<Sick>.Plain],
};
var checkedGoers = partyGoers
.Has(Comp<Vaccinated>.Plain)
.Not(Comp<Sick>.Plain);

partyGoers.For((ref name, ref playlist) =>
checkedGoers.For((ref name, ref playlist) =>
{
DeeJay.Instruct($"{name} is coming, play something from {playlist.entries}");
});
Expand All @@ -104,7 +102,7 @@ partyGoers.For((ref name, ref playlist) =>
From the start, a Query includes only Entities that match all of its [Stream Types](../Streams/). This applies regardless of whether it's a Plain Component, Entity-Entity Relation, or Object Link – unless expressly specified in the QueryBuilder.

::: details :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](/docs/Intermediate/Filters/Subset.md)). When iterating, the Query processes each Archetype in deterministic order.
Each compiled Query maintains a collection of all Archetypes it matches (and a [filtered subset](/docs/Intermediate/Filters/Archetypes.md)). When iterating, the Query processes each Archetype in deterministic order.

Whenever a new Archetype materializes, the World notifies _all matching Queries_ of its existence.
:::
Expand Down Expand Up @@ -202,7 +200,7 @@ var friendsInNeed = world.Query<Friend>()

::: tip :neofox_thumbsup: USE FILTERS INSTEAD!
Filters have similar performance but can be reconfigured on the fly. Streams
are lightweight value types – `with` creates a filtered view, and the
are lightweight value types – `Has`/`Not` create a filtered view, and the
original stream stays around, unfiltered:

```cs
Expand All @@ -211,11 +209,9 @@ var friendsInNeed = world.Query<Friend>()
.Stream();

// Dynamic filtering!
var owingBobNotMe = friendsInNeed with
{
Subset = [Comp<Owes>.Matching(bob)],
Exclude = [Comp<Owes>.Matching(me)],
};
var owingBobNotMe = friendsInNeed
.Has(Comp<Owes>.Matching(bob))
.Not(Comp<Owes>.Matching(me));
owingBobNotMe.For(PayOffDebt);

// Reconfigure when needed - just make another filtered view!
Expand Down
2 changes: 1 addition & 1 deletion doc/docs/Basic/Streams/Stream.Job.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Choosing the right way to spread your workload across CPU cores can yield signif
:::warning :neofox_glare_sob: A GOOD TRADE-OFF LEAVES EVERYONE MAD!
Overhead for thread scheduling is real; as are context switches between threads. Experiment finding the right workload Chunk Size (start big - try 69,420, they say it's nice) and always consider giving [`Stream.For`](Stream.For.md) another look if you realize there's too much overhead or ==fragmentation==.

You can also set the [Filter State](/docs/Intermediate/Filters/Subset.md) of your Query to only include the Archetypes you want to process as a `Stream.Job`, and use `Stream.For` to do the rest. Or make it even easier: **Create Two Queries.**
You can also use [Stream Filters](/docs/Intermediate/Filters/Archetypes.md) on your Query to only include the Archetypes you want to process as a `Stream.Job`, and use `Stream.For` to do the rest. Or make it even easier: **Create Two Queries.**
:::

Scheduling Jobs has a certain overhead, so just splitting work across as many CPUs as possible sometimes slows down processing speeds.
Expand Down
51 changes: 51 additions & 0 deletions doc/docs/Intermediate/Filters/Archetypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Has & Not
layout: doc
outline: [2, 3]
order: 1
description: 'Archetype-level Stream filters in fennecs: Has and Not clauses on a FilteredStream prune whole Archetypes by component presence before iteration starts.'
---

# Has & Not Archetype Filters

The coarse (and fastest) way to narrow a Stream: additional component clauses that prune whole **Archetypes** before iteration even starts. Entire tables of Entities are skipped in one go – no per-Entity work at all.

## Creating a FilteredStream

`Stream<>.Has(...)` and `Stream<>.Not(...)` each return a `FilteredStream<>` applying the clause for all its operations. The original Stream and the underlying Query are untouched.

```csharp
var stream = world.Stream<Position, Velocity>();

var filtered = stream
.Has(Comp<Alive>.Plain)
.Not(Comp<OneRing>.Matching(TheOneRing));
```

## Has Clause
This works exactly like an additional `Has<>` clause on a QueryBuilder.

> `includes only` Entities whose Archetype has **all** of the given components or relations. Multiple expressions – whether passed in one call or chained – combine to a logical `A AND B AND C`.

## Not Clause
This works exactly like an additional `Not<>` clause on a QueryBuilder.

> `excludes` any Entities whose Archetype has **any** of the given components. Multiple expressions can be compared to a logical `NOT (A OR B OR C)`, aka. `(NOT A) AND (NOT B) AND (NOT C)`.

## Chaining Accumulates

`Has` and `Not` accumulate on a `FilteredStream` – each call unions its expressions into the existing clause:

```csharp
var picky = stream
.Has(Comp<Alive>.Plain)
.Has(Comp<Hungry>.Plain) // same as .Has(Comp<Alive>.Plain, Comp<Hungry>.Plain)
.Not(Comp<Dead>.Plain, Comp<Sleeping>.Plain);
```

::: tip :neofox_thumbsup: Need to filter by component *values* instead?
Has & Not decide by component *presence*. To skip Entities based on what their components *contain*, add a [Where](Where.md) lambda filter on top.
:::

## Future Features
The `Comp<T>` expression API used to create the necessary filter expressions is likely to have its API reviewed and tightened, to make the syntax more readable and easier to use. It might get unified into a Mask-like system as used internally to power QueryBuilders.
58 changes: 0 additions & 58 deletions doc/docs/Intermediate/Filters/Subset.md

This file was deleted.

29 changes: 15 additions & 14 deletions doc/docs/Intermediate/Filters/Where.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ title: Where (Lambda)
layout: doc
outline: [2, 3]
order: 2
description: 'LINQ-style per-entity Stream filters in fennecs: Stream<>.Where(lambda) predicates on component values, evaluated live during For and Job runs.'
description: 'LINQ-style per-entity Stream filters in fennecs: Where(lambda) predicates on component values return a FilteredStream, honored by For, Job, Count, enumeration, and Despawn.'
---

# `Where` Lambda Filters

Sometimes narrowing by Archetype isn't enough – you want to skip Entities based on their component **values**. `Stream<>.Where(...)` attaches a predicate for one of the Stream's component types, and the runners skip every Entity whose values don't pass.
Sometimes narrowing by Archetype isn't enough – you want to skip Entities based on their component **values**. `Where(...)` attaches a predicate for one of the Stream's component types and returns a `FilteredStream<>` whose operations skip every Entity whose values don't pass.

```csharp
var stream = world.Query<Health, Position, Velocity>().Stream();
Expand All @@ -27,7 +27,7 @@ wounded.For((ref health, ref position, ref velocity) =>
The semantics may feel similar, but there are no allocations and filters benefit from [.NET's PGO & inlining](https://devblogs.microsoft.com/dotnet/bing-on-dotnet-8-the-impact-of-dynamic-pgo/) at runtime. It's a short-cirquit way to omit excess memory transfers and function calls at the expense of one or two quick tests!
:::

It is up to you to examine your program's domain and decide whether Stream Filters, Stream Subsets, or fixed Query Matching are the way to go.
It is up to you to examine your program's domain and decide whether Where predicates, [Has & Not](Archetypes.md) clauses, or fixed Query Matching are the way to go.

Filters are best when they can *reject* a large number of Entities based on a simple test, saving you the most memory bandwidth and in turn sparing you fragmentation. In the above example, the alternative approach would be to add and maintain a `Wounded` Tag, putting all wounded Entities into a separate archetype from their normal one.

Expand All @@ -42,7 +42,7 @@ public delegate bool ComponentFilter<C>(in C c);

- return `true` to include the Entity, `false` to skip it
- `C` must be one of the Stream's type parameters; the lambda's parameter type selects the `Where` overload, so spell it out: `(in Health h) => ...`
- like [Subset & Exclude](Subset.md), `Where` returns a **new Stream** – the original Stream and the underlying Query are untouched
- like [Has & Not](Archetypes.md), `Where` returns a new **FilteredStream** – the original Stream and the underlying Query are untouched

## One Slot per Stream Type

Expand All @@ -59,28 +59,29 @@ var lowHp = apex.Where((in Health h) => h.Current < 10); // replaces the He
var goldilocks = stream.Where((in Health h) => h.Current > 10 && h.Current < 100);
```

## Where `Where` applies (and where it doesn't!)
## Where `Where` applies

The predicates run per Entity, live inside the runners:
A `FilteredStream` honors its predicates in everything it offers:

| Operation | Honors `Where`? |
|-----------|-----------------|
| `For` | ✅ per Entity, on every run |
| `For` | ✅ per Entity, on every run *(two-delegate overloads route rejected Entities to `excluded`)* |
| `Job` | ✅ per Entity *(predicate must be thread-safe, just like your delegate)* |
| `Raw` | ⛔ hands out whole memory blocks |
| `Blit` | ⛔ writes whole Archetypes |
| `foreach` / LINQ enumeration | ⛔ |
| `Count` | ⛔ counts the [Subset & Exclude](Subset.md)-filtered Archetypes |
| `foreach` / LINQ enumeration | ✅ yields only passing Entities |
| `Count` | ✅ counts passing Entities – O(n) when predicates are set |
| `Despawn` | ✅ despawns only passing Entities |
| `Raw` / `Blit` | 🚫 not offered – block operations can't ask per Entity; use the unfiltered view via `filtered.Stream` |

::: info :neofox_science: Live Evaluation
Predicates are evaluated fresh on every run – change your World's data, and the same filtered Stream processes a different set of Entities next frame. Keep them cheap: unlike [Subset & Exclude](Subset.md), which skips whole Archetypes, `Where` still visits each Entity to ask.
Predicates are evaluated fresh on every run – change your World's data, and the same FilteredStream processes a different set of Entities next frame. Keep them cheap: unlike [Has & Not](Archetypes.md), which skips whole Archetypes, `Where` still visits each Entity to ask.
:::

## Combining with Subset & Exclude
## Combining with Has & Not

Both mechanisms compose freely – the Archetype filters prune entire tables up front, then the predicates skim the Entities that remain:

```csharp
var survivors = (stream with { Exclude = [ Comp<Dead>.Plain ] })
var survivors = stream
.Not(Comp<Dead>.Plain)
.Where((in Health h) => h.Current < h.Max);
```
33 changes: 24 additions & 9 deletions doc/docs/Intermediate/Filters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,46 @@ title: Filters
layout: doc
outline: [2, 3]
order: 20
description: 'Stream filters in fennecs narrow a Query on the fly - archetype-level Subset & Exclude sets, and LINQ-style per-entity Where(lambda) predicates on component values.'
description: 'Stream filters in fennecs narrow a Query on the fly - FilteredStream views with archetype-level Has & Not clauses, and LINQ-style per-entity Where(lambda) predicates on component values.'
---

# Stream Filters

Sometimes, a dynamic on-the-fly filter is needed to process only a subset of Entities; this enables us to quickly adjust our ECS logic to do different subsets of work without requiring a growing amount of queries to be defined and tracked.

Streams are *lightweight views* over their Query, so filtering never mutates the original Stream nor the underlying Query – you simply use a new, narrower view to run.
Calling `Has`, `Not`, or `Where` on a Stream returns a `FilteredStream<>` – a *lightweight view* that carries all the filter state. Filtering never mutates the original Stream nor the underlying Query – you simply use a new, narrower view to run.

## Two Flavors of Filtering

| Flavor | Granularity | How it narrows | Honored by |
|--------|-------------|----------------|------------|
| [Subset & Exclude](Subset.md) | whole **Archetypes** | additional `Has`/`Not`-style clauses that prune Archetypes before iteration | everything: `For`, `Job`, `Raw`, `Blit`, enumeration, `Count` |
| [Where](Where.md) | individual **Entities** | LINQ-style lambda predicates on component *values*, evaluated live during the run | `For` and `Job` |
| Flavor | Granularity | How it narrows | Cost |
|--------|-------------|----------------|------|
| [Has & Not](Archetypes.md) | whole **Archetypes** | additional `Has`/`Not` clauses that prune Archetypes before iteration | one signature check per Archetype |
| [Where](Where.md) | individual **Entities** | LINQ-style lambda predicates on component *values*, evaluated live during the run | one predicate call per Entity |

Both compose freely: the Archetype filters prune entire tables up front, and the `Where` predicates skim the Entities that remain.
Both compose freely: the Archetype filters prune entire tables up front, and the `Where` predicates skim the Entities that remain. All of a `FilteredStream`'s operations honor all of its filters: `For`, `Job`, `Count`, enumeration, and `Despawn`.

```csharp
var stream = world.Stream<Position, Health>();

var wounded = (stream with { Exclude = [ Comp<Dead>.Plain ] }) // Subset & Exclude
.Where((in Health h) => h.Current < h.Max); // Where (lambda)
var wounded = stream
.Not(Comp<Dead>.Plain) // Has & Not (Archetypes)
.Where((in Health h) => h.Current < h.Max); // Where (lambda)
```

## Both Sides of the Cut

`FilteredStream` runners can visit the *complement*, too. Each `For` and `Job` variant has an overload taking two delegates: `included` runs on every Entity passing all filters, and `excluded` runs on every other Entity the underlying Query matches – whether its whole Archetype was pruned or it just failed a `Where` predicate. Together, the two delegates visit each Entity exactly once.

```csharp
wounded.For(
included: (ref Position p, ref Health h) => LimpTowardsHealer(ref p),
excluded: (ref Position p, ref Health h) => h.Regenerate());
```

::: info :neofox_science: One Pass, No Wildcards
Two-delegate runners partition the Query in a single pass – no second iteration, no allocation. Since the partition must visit each Entity exactly once, they are not available on Streams with Wildcard [Match Expressions](/docs/Basic/Queries/Matching.md).
:::

::: tip :neofox_floof_mug: Can I interest you in a Grape-Colored Example
There's an appetizer on this topic! Check out [Thanos](/cookbook/appetizers/Thanos.md) for an, ahem, "practical" example of using filters.
:::
6 changes: 2 additions & 4 deletions doc/docs/Intermediate/Keys/Link.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ var unbankedEntities = world
// Wildcard target, specific exclusion, stream filter
var entitiesExceptCustomersOfChase = world
.Query<Bank>(Link.Any)
.Stream() with // do this on-the-fly where needed
{
Exclude = [Comp<Bank>.Matching(chase)]
};
.Stream() // do this on-the-fly where needed
.Not(Comp<Bank>.Matching(chase));
```

## Removing Links
Expand Down
6 changes: 2 additions & 4 deletions doc/docs/Intermediate/Keys/Relation.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,8 @@ var entitiesThatAreDebtFree = world
// Wildcard target, specific exclusion, stream filter
var entitiesOwingAnyoneExceptEve = world
.Query<Owes>(Entity.Any)
.Stream() with // do this on-the-fly where needed
{
Exclude = [Comp<Owes>.Matching(eve)]
};
.Stream() // do this on-the-fly where needed
.Not(Comp<Owes>.Matching(eve));
```
::: danger :neofox_googly_reverse: DUDE, where's my ENTITY?
To query for Relations, you must either specify a concrete target Entity or use a wildcard:
Expand Down
Loading