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.
// Create a filter.
filter := ecs.NewFilter2[Position, Velocity](&world)
// Obtain a query.
query := filter.Query()
// Iterate the query.
for query.Next() {
pos, vel := query.Get()
pos.X += vel.X
pos.Y += vel.Y
}Query2.Get returns all queried components of the current entity.
The current entity can be obtained with 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. Further, Ark maintains a mapping from each component to the set of archetypes that include it. This is used to reduce the number of filter checks by pre-selecting archetypes by the most “rare” component of a query.
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 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:
// Create a filter.
filter := ecs.NewFilter1[Altitude](&world)
// Create a slice to collect entities.
// Ideally, store this permanently for re-use.
toRemove := []ecs.Entity{}
query := filter.Query()
for query.Next() {
alt := query.Get()
alt.Z--
if alt.Z < 0 {
// Collect entities to remove.
toRemove = append(toRemove, query.Entity())
}
}
// Do the removal.
for _, e := range toRemove {
world.RemoveEntity(e)
}
// Reset the slice for re-use.
toRemove = toRemove[:0]Advanced filters
Filters can be further specified using method chaining.
With
Filter2.With (and related methods) allow to specify components that the queried entities should possess,
but that are not used inside the query iteration:
// Create a filter.
filter := ecs.NewFilter1[Position](&world).
With(ecs.C[Velocity](), ecs.C[Altitude]())
// Obtain a query.
_ = filter.Query()
// ...With can also be called multiple times instead of specifying multiple components in one call:
// Create a filter.
filter := ecs.NewFilter1[Position](&world).
With(ecs.C[Velocity]()).
With(ecs.C[Altitude]())
// Obtain a query.
_ = filter.Query()
// ...Without
Filter2.Without (and related methods) allow to specify components that the queried entities should not possess:
// Create a filter.
filter := ecs.NewFilter1[Position](&world).
Without(ecs.C[Velocity](), ecs.C[Altitude]())
// Obtain a query.
_ = filter.Query()
// ...As with With, Without can be called multiple times:
// Create a filter.
filter := ecs.NewFilter1[Position](&world).
Without(ecs.C[Velocity]()).
Without(ecs.C[Altitude]())
// Obtain a query.
_ = filter.Query()
// ...Exclusive
Filter2.Exclusive (and related methods) make the filter exclusive on the given components,
i.e. it excludes all other components:
// Create a filter.
filter := ecs.NewFilter1[Position](&world).
Exclusive()
// Obtain a query.
_ = filter.Query()
// ...Optional
There is no Optional provided, as it would require an additional check in Query2.Get et al.
Instead, use Map.Has, Map.Get or similar methods in Map2 et al.:
// Create a filter.
filter := ecs.NewFilter2[Position, Velocity](&world)
// Create a component mapper.
altMap := ecs.NewMap[Altitude](&world)
// Obtain a query.
query := filter.Query()
for query.Next() {
// Get the current entity.
entity := query.Entity()
// Check whether the current entity has an Altitude component.
if altMap.Has(entity) {
alt := altMap.Get(entity)
alt.Z += 1.0
}
// Do other stuff...
}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
Filter2.Register:
// Create a filter.
filter := ecs.NewFilter2[Position, Velocity](&world).
Register() // Register it to the cache.
// Obtain a query.
_ = filter.Query()
// ...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
Filter2.Unregister.
However, this is rarely required as (registered) filters are usually used over an entire game session or simulation run.
Parallel queries
Ark in general is not thread-safe (see chapter Design, section Limitations). However, it is possible to execute queries in parallel. This is especially useful for two scenarios:
- Parallel execution of logic/systems that handle distinct sets of entities.
- Parallel execution inside a system, partitioning entities using entity relations.
For the second use case, a stand-alone example is available that demonstrates the approach.
It is the user’s responsibility to avoid access to the same entities from parallel queries, which could cause data races and performance degradation due to false sharing. For the two use cases given above, this is guaranteed because the parallel queries affect different archetypes or relation sub-tables, respectively.