Queries
Queries allow to select Entities that have a certain set of Components and to manipulate them.
Queries are the heart of every ECS and the main reason for its flexibility and performance. In Ark, queries are blazing fast and should be used to write the game or model logic where possible. For cases where components of a particular entity are required, see section Accessing components.
Basic queries
By default, a Query lets you iterate over all entities that have the query's components, and provides efficient access to these components.
Here, we are interested only in non-exclusive sets. So the entities that are processed may have further components, but they are not of interest for that particular piece of game or model logic.
for (entities, positions, velocities) in @Query(world, (Position, Velocity))
@inbounds for i in eachindex(entities)
pos = positions[i]
vel = velocities[i]
positions[i] = Position(pos.x + vel.dx, pos.y + vel.dy)
end
endNote the nested loop! In the outer loop, the query iterates the archetypes, and for each one returns a vector of entities and the columns for the queried components. In the inner loop, we iterate over the entities within the archetype and perform the actual logic.
Also not the last line in the inner loop. Here, we assign a new Position value to the current entity. This is necessary as Position is immutable, which is the default and highly recommended. See section Component types for details.
The @inbounds macro in front of the inner loop is optional, but it is safe to use here and makes the iteration faster as it allows the compiler to skip bounds checks.
Advanced queries
with
Queries provide an optional with argument that filters for additional components that entities must have, but that are not used by the query.
for (entities, positions, velocities) in @Query(world,
(Position, Velocity),
with=(Health,)
)
@inbounds for i in eachindex(entities)
# ...
end
endwithout
The optional without argument allows to exclude entities that have certain components:
for (entities, positions, velocities) in @Query(world,
(Position, Velocity),
without=(Health,)
)
@inbounds for i in eachindex(entities)
# ...
end
endexclusive
The optional exclusive argument excludes entities that have any other then the query's components and those specified by with. So it acts like an exhaustive without:
for (entities, positions, velocities) in @Query(world,
(Position, Velocity),
exclusive=true
)
@inbounds for i in eachindex(entities)
# ...
end
endoptional
The optional optional argument makes components optional. 😉
Entities are included irrespective of presence of these components on them. Note that, in contrast to the other arguments, this one is not related to additional components, but refers to components of the query's "normal" components set.
Optional components are still included in the iterator's columns tuple, but they are nothing if the current archetype does not have them.
for (entities, positions, velocities) in @Query(world,
(Position, Velocity),
optional=(Velocity,)
)
has_velocity = velocities !== nothing
@inbounds for i in eachindex(entities)
# ...
end
endNote that it is possible to branch already outside of the inner loop, as all entities in an archetype either have a component or don't.
World lock
During query iteration, the World is locked for modifications like entity creation and removal and component addition and removal. This is necessary to prevent changes to the inner storage structure of the World that would result in undefined behavior of the query.
If the necessity for these forbidden iteration arises, the respective entities must be collected into a Vector to apply the operations after query iteration has finished.
# vector for entities to be removed from te world
to_remove = Entity[]
for (entities, positions) in @Query(world, (Position,))
@inbounds for i in eachindex(entities)
pos = positions[i]
# collect entities for removal
if pos.y < 0
push!(to_remove, entities[i])
end
end
end
# actual removal
for entity in to_remove
remove_entity!(world, entity)
end
# clear the vector for re-use
resize!(to_remove, 0)For the best performance, such a Vector should be stored persistently and re-used to avoid repeated memory allocations.
The world is automatically unlocked when query iteration finishes. When breaking out of a query loop, however, it must be unlocked by calling close! on the query.