Filters & queries
Queries are the core feature for writing logic in an ECS. A query iterates over all entities that possess all the component types specified by the query.
Queries are constructed from filters. While queries are one-time use iterators that are cheap to create, filters are more costly to create and should be stored permanently, e.g. in your systems.
Filters and queries
With basic filters, queries iterate all entities that have the given components, and any additional components that are not of interest.
In the example below, the filter would match any entities that have
Position
and Velocity
, and potentially further components like Altitude
.
1// Create a filter.
2filter := ecs.NewFilter2[Position, Velocity](&world)
3// Obtain a query.
4query := filter.Query()
5// Iterate the query.
6for query.Next() {
7 pos, vel := query.Get()
8 pos.X += vel.X
9 pos.Y += vel.Y
10}
ecs.Query2.Get
returns all queried components of the current entity.
The current entity can be obtained with ecs.Query2.Entity
.
Query performance
Queries iteration is what an archetype-based ECS is optimized for, and it is really fast. This has two reasons.
Firstly, all entities with the same component composition are stored in the same archetype, or “table”. This means that filters only need to be checked against archetypes, and the entities of a matching archetype can be iterated without any further checks.
Secondly, all components of the same type (like Position
) are stored in a dedicated column of the archetype.
A query only accesses the required components (i.e. columns), although entities may possess many more components.
Memory access is therefore completely linear and contiguous, and the CPUs cache is used as efficiently as possible.
World lock
The world gets locked for component operations when a query is created.
The lock is automatically released when query iteration has finished.
When breaking out of the iteration, the query must be closed manually with ecs.Query2.Close
.
The lock prevents entity creation and removal, as well as adding and removing components. Thus, it may be necessary to collect entities during the iteration, and perform the operation afterwards:
1// Create a filter.
2filter := ecs.NewFilter1[Altitude](&world)
3// Create a slice to collect entities.
4// Ideally, store this permanently for re-use.
5toRemove := []ecs.Entity{}
6
7query := filter.Query()
8for query.Next() {
9 alt := query.Get()
10 alt.Z--
11 if alt.Z < 0 {
12 // Collect entities to remove.
13 toRemove = append(toRemove, query.Entity())
14 }
15}
16
17// Do the removal.
18for _, e := range toRemove {
19 world.RemoveEntity(e)
20}
21// Reset the slice for re-use.
22toRemove = toRemove[:0]
Advanced filters
Filters can be further specified using method chaining.
With
ecs.Filter2.With
(and related methods) allow to specify components that the queried entities should possess,
but that are not used inside the query iteration:
1// Create a filter.
2filter := ecs.NewFilter1[Position](&world).
3 With(ecs.C[Velocity](), ecs.C[Altitude]())
4
5// Obtain a query.
6_ = filter.Query()
7// ...
With
can also be called multiple times instead of specifying multiple components in one call:
1// Create a filter.
2filter := ecs.NewFilter1[Position](&world).
3 With(ecs.C[Velocity]()).
4 With(ecs.C[Altitude]())
5
6// Obtain a query.
7_ = filter.Query()
8// ...
Without
ecs.Filter2.Without
(and related methods) allow to specify components that the queried entities should not possess:
1// Create a filter.
2filter := ecs.NewFilter1[Position](&world).
3 Without(ecs.C[Velocity](), ecs.C[Altitude]())
4
5// Obtain a query.
6_ = filter.Query()
7// ...
As with With
, Without
can be called multiple times:
1// Create a filter.
2filter := ecs.NewFilter1[Position](&world).
3 Without(ecs.C[Velocity]()).
4 Without(ecs.C[Altitude]())
5
6// Obtain a query.
7_ = filter.Query()
8// ...
Exclusive
ecs.Filter2.Exclusive
(and related methods) make the filter exclusive on the given components,
i.e. it excludes all other components:
1// Create a filter.
2filter := ecs.NewFilter1[Position](&world).
3 Exclusive()
4
5// Obtain a query.
6_ = filter.Query()
7// ...
Optional
There is no Optional
provided, as it would require an additional check in ecs.Query2.Get
et al.
Instead, use ecs.Map.Has
, ecs.Map.Get
or similar methods in ecs.Map2
et al.:
1// Create a filter.
2filter := ecs.NewFilter2[Position, Velocity](&world)
3// Create a component mapper.
4altMap := ecs.NewMap[Altitude](&world)
5
6// Obtain a query.
7query := filter.Query()
8for query.Next() {
9 // Get the current entity.
10 entity := query.Entity()
11 // Check whether the current entity has an Altitude component.
12 if altMap.Has(entity) {
13 alt := altMap.Get(entity)
14 alt.Z += 1.0
15 }
16 // Do other stuff...
17}
Filter caching
Although queries are highly performant, a huge number of archetypes (like hundreds or thousands) may cause a slowdown.
To prevent this slowdown, filters can be registered to the world’s filter cache via
ecs.Filter2.Register
:
1// Create a filter.
2filter := ecs.NewFilter2[Position, Velocity](&world).
3 Register() // Register it to the cache.
4
5// Obtain a query.
6_ = filter.Query()
7// ...
For registered filters, a list of matching archetypes is cached internally. Thus, no filter evaluations are required during iteration. Instead, filters are only evaluated when a new archetype is created.
When a registered filter is not required anymore, it can be unregistered with
ecs.Filter2.Unregister
.
However, this is rarely required as (registered) filters are usually used over an entire game session or simulation run.