Queries
Queries (
ecs.Query
) are the heart of Arche’s query engine.
They allow for very fast retrieval and iteration of entities with certain components.
Queries are for one-time utilization. A query can be iterated through only once, and a new one must be created before every loop. Contrary, the underlying Filters should be stored and re-used.
Query creation & iteration
Queries are created through the
ecs.World
using a Filter (interface
ecs.Filter
).
The most basic type of filter is
ecs.Mask
. For more advanced filters, see chapter Filters.
Here, we create a filter that gives us all entities with all the given components, and potentially further components. Then, we create an
ecs.Query
(or generic QueryX, e.g.
generic.Query2
) and iterate it.
1world := ecs.NewWorld()
2
3filter := generic.NewFilter2[Position, Velocity]()
4query := filter.Query(&world)
5for query.Next() {
6 pos, vel := query.Get()
7 pos.X += vel.X
8 pos.Y += vel.Y
9}
1world := ecs.NewWorld()
2
3posID := ecs.ComponentID[Position](&world)
4velID := ecs.ComponentID[Velocity](&world)
5
6filter := ecs.All(posID, velID)
7query := world.Query(filter)
8for query.Next() {
9 pos := (*Position)(query.Get(posID))
10 vel := (*Velocity)(query.Get(velID))
11 pos.X += vel.X
12 pos.Y += vel.Y
13}
Where
ecs.Query.Get
(resp.
generic.Query2.Get
) return components of the entity at the
current query iterator position.
Comparing the two versions of the code above, one can clearly observe the advantages of the generic API over the ID-based API (see chapter on APIs). Firstly, the generic code is shorter and more readable. But even more importantly, it much safer. A little mistake in line 9 or 10 of the ID-based version could result in silently casting a component to the wrong type, which would lead to bugs that are hard to track down.
If you get error messages like “index out of range [-1]” or “invalid memory address or nil pointer dereference” from queries, you are probably using them in the wrong way. Try running with build tag debug
for more helpful error messages:
go run -tags debug .
World lock
When a query gets created, the
ecs.World
gets locked for modifications.
When locked, no entities can be created or removed, and no components can be added to
or removed from entities.
When a query is fully iterated, the world gets unlocked again. When a query is not fully iterated
for some reason (see next section for examples), it must be closed with
ecs.Query.Close
.
Due to the world lock, denied operations like entity creation or removal must be deferred:
1world := ecs.NewWorld()
2
3// Create some entities.
4for i := 0; i < 100; i++ {
5 world.NewEntity()
6}
7
8// A slice that we (re)-use to defer entity removal.
9var toRemove []ecs.Entity
10
11// A time loop.
12for time := 0; time < 100; time++ {
13 // Query... the world gets locked.
14 query := world.Query(ecs.All())
15 // Iterate, and collect entities to remove.
16 for query.Next() {
17 if rand.Float64() < 0.1 {
18 toRemove = append(toRemove, query.Entity())
19 }
20 }
21 // The world is unlocked again.
22 // Actually remove the collected entities.
23 for _, e := range toRemove {
24 world.RemoveEntity(e)
25 }
26 // Empty the slice, so we can reuse it in the next time step.
27 toRemove = toRemove[:0]
28}
Where
ecs.Query.Entity
returns the entity at the current query iterator position.
Other functionality
Besides
ecs.Query.Next
,
ecs.Query.Get
and
ecs.Query.Entity
that we used above, queries have a few more useful methods.
Query.Count
ecs.Query.Count
allows for counting the entities in a query, very fast:
1world := ecs.NewWorld()
2
3query := world.Query(ecs.All())
4
5cnt := query.Count()
6fmt.Println(cnt)
7
8query.Close()
Note that we need to call
ecs.Query.Close
here, as the query was not (fully) iterated!
After
ecs.Query.Count
, the query could also be iterated as usual.
Query.EntityAt
With
ecs.Query.EntityAt
, queries also support access by index.
This is particularly useful to select random entities from a query,
like in this example:
1world := ecs.NewWorld()
2
3// Create some entities.
4for i := 0; i < 100; i++ {
5 world.NewEntity()
6}
7
8// Query and count entities.
9query := world.Query(ecs.All())
10cnt := query.Count()
11
12// Draw random entities.
13for i := 0; i < 10; i++ {
14 entity := query.EntityAt(rand.Intn(cnt))
15 fmt.Println(entity)
16}
17
18query.Close()
Note that we need to close the query manually, again! To access components of the retrieved entities, see chapter World Entity Access.
Note that
ecs.Query.EntityAt
may be slow when working with a large number of archetypes.
Often, it is useful to register the underlying filter for speedup.
See chapter Filter, section Filter caching for details.
See the query benchmarks for some numbers on performance.