Ark
Filters & queries

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.