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.

Important

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.

Tip

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.