Queries and iteration
Iterating over entities can be done via
classic for
loops applied to queries,
or via an apply
operation, which applies a given function to all entities
conforming to a query.
Queries
Queries
allow to iterate over all
entities with or without a specific
set of components. To create a query, we can use
the query
method of
World
. The parameters used in this method are the components
that each entity we look for must have. For example, if we want to
iterate over all entities with a Position
and a Velocity
component,
we can do this as follows:
# Add entities with different components
_ = world.add_entity()
_ = world.add_entity(Position(0, 0))
_ = world.add_entity(Velocity(1, 0))
_ = world.add_entity(Position(1, 0), Velocity(1, 0))
# Query all entities that have a position
query = world.query[Position]()
# Of the entities we have just added,
# two have a position component
print(len(query)) # "2"
# Now let us iterate over the queried entities
for entity in query:
pos = entity.get_ptr[Position]()
print(
"Entity at position: ("
+ String(pos[].x) + ", " + String(pos[].y) + ")"
)
Queries can be adjusted to also exclude entities that have
certain components. For example, if we want to iterate
over all entities that have a Position
component
but not a Velocity
component, we can do this
using the without
method:
excluding_query = world.query[Position]().without[Velocity]()
print(len(excluding_query)) # "1"
Furthermore, we can also query for entities that have
exactly the components we are looking for but no more.
This can be done using the exclusive
method. For example, if we want to iterate
over all entities that have only a Position
component,
we can do this as follows:
excluding_query = world.query[Position]().exclusive()
print(len(excluding_query)) # "1"
Note
Determining the length of a query is not a trivial operation
and may require an internal iteration if the ECS involves many components.
Therefore, it is advisable to avoid applying the len
function
to queries in “hot” code. Nonetheless, the len
function
is much faster than counting entities manually by iterating over a query.
Iterating over queries
As we have seen, we can iterate over queries using a for loop.
Here, the control variable (“entity”) is an EntityAccessor
object, i.e., not technically an Entity
, which is
merely an identifier of an entity. Instead, the EntityAccessor
directly provides methods to get, set, and check the existence
of components, so that we do not need to call the world’s
methods for this, making the code more efficient.
for entity in world.query[Position]():
pos = entity.get_ptr[Position]()
print(
"Entity at position: ("
+ String(pos[].x) + ", " + String(pos[].y) + ")"
)
if entity.has[Velocity]():
vel = entity.get_ptr[Velocity]()
# Also print the velocity
print(
" - with velocity ("
+ String(vel[].dx) + ", " + String(vel[].dy) + ")"
)
Note
The EntityAccessor
is a temporary object that is
created for each iteration. Therefore, it should not be
stored in a container. Use EntityAccessor.get_entity
instead if you need to store the entity for later use.
Note
The EntityAccessor
can be implicitly
converted to an Entity
object and hence be used
wherever an Entity
is required.
Preventing iterator invalidation: the locked world
Adding/removing entities to/from the world or components to/from entities while iterating could invalidate the iterator. That is, the iterator could leave out some entities or consider some entities multiple times. To prevent this, Larecs🌲 locks the world during iterations. This means that methods that change how many entities exist in the world or which components entities have will raise exceptions if called during iteration.
for entity in world.query[Position]():
# Adding entities to the world while iterating
# is forbidden.
with assert_raises():
_ = world.add_entity(Velocity(1, 0)) # Raises an exception
# Changing components of an entity while iterating
# is forbidden.
with assert_raises():
_ = world.add(entity, Position(1, 2)) # Raises an exception
If we want to add or remove components from entities while iterating,
we need to store the entities in an intermediate
container and iterate over them in
a separate loop. Consider the following example, where we
add a Velocity
component to all entities that have a Position
but no Velocity
component:
# A container for the entities
entities = List[Entity]()
for entity in world.query[Position]().without[Velocity]():
# Store the entity for later use
# The implicit conversion to `Entity`
# allows us to use `entity` directly
entities.append(entity)
# Add a velocity component to all stored entities
for entity in entities:
# We can add components to the entity
# because we are not iterating over the world
_ = world.add(entity[], Velocity(1, 0))
Note
In a later release, Larecs🌲 will provide
a batched version of the add
and remove
methods
that will allow adding or removing components
from multiple entities at once.
Applying functions to entities in queries
We may want to apply a certain operation to all entities
that have certain components. This can be achieved with
the apply
method. This method
iterates over all entities conforming to a query and
calls the provided function with the entities as arguments.
The function must take a MutableEntityAccessor
(an alias for EntityAccessor[True]
)
as its only argument. Applying a function to all entities
can be more convenient and also faster than iterating over the entities
manually, especially if the function is vectorized, as is shown
in the vectorization chapter.
For example, if we want to apply a function that moves all entities
with a Position
and a Velocity
component, we can do this as follows:
# Define the move function
fn move(entity: MutableEntityAccessor) capturing:
try:
move_pos = entity.get_ptr[Position]()
move_vel = entity.get_ptr[Velocity]()
move_pos[].x += move_vel[].dx
move_pos[].y += move_vel[].dy
except:
# We could do proper error handling here
# but for now, we just ignore the error
pass
# Apply the move function to all entities with a position and a velocity
world.apply[move](world.query[Position, Velocity]())
Note
Currently, the applied operation can not raise exceptions. Therefore, we need to catch exceptions in the function itself. This is due to current limitations of Mojo and will be changed as soon as possible.
Caution
The world is locked during the iteration, and
accessing variables outside a locally defined
function is an immature feature in Mojo. Do not
attempt to access the world
from inside the operation.