Skip to content
Andrea Catania edited this page Mar 8, 2021 · 24 revisions

One of the main concept of ECS is to organize the data inside the Storage. Usually, the data is fetched from the Systems (In godex you can also fetch the data from any Godot function, like _ready or _process) using a Query. This page, is fully dedicated to the Query and how to use it.

Query & DynamicQuery

Godex can be used from scripting but also from native code (C++). To guarantee both the best, it was chosen to have a dedicated mechanism for each approach:

  • The Query is used by C++ Systems; it's statically compiled so to guarantee the best performance.
  • The DynamicQuery is used by scripting; it can be composed at runtime and has the ability to expose the data to scripts. Despite the above differences, both extract the data from the storage in the same way and provide the exact same features and filters.

Since both provide the exact same features, with the difference that one can be created at runtime while the other is statically compiled, keep in mind that the below concepts and mechanisms apply to both.

Syntax

When you compose a scene like this: Screenshot from 2021-02-19 18-03-12 the components of each Entity (that you can see under the inspector components, at the right side of the above image), are added to the World storage. For example, the World of the above scene, has three storage (for simplicity just imagine each storage like an array):

  • TransformComponent
  • Velocity.gd
  • MeshComponent

Let's say, we want to move the Entities that have a Velocity.gd component: so we can write a system like this:

# VelocitySystem.gd
extends System

func _prepare():
	with_databag(ECS.FrameTime, IMMUTABLE)
	with_component(ECS.TransformComponent, MUTABLE)
	with_component(ECS.Velocity_gd, IMMUTABLE)

func _for_each(frame_time, transform, velocity):
	transform.transform.basis = transform.transform.basis.rotated(
		velocity.velocity.normalized(),
		velocity.velocity.length() * frame_time.delta)

Or in C++

void velocity_system(const FrameTime* p_frame_time, Query<TransformComponent, Velocity> &p_query) {
	for(auto [transform, velocity] : p_query) {
		// ...
	}
}

Both Systems are fetching the data using a Query (the GDScript system is creating the DynamicQuery for you, when you specify the component inside the function _prepare).

What a Query does.

Just before the System is executed, the query takes the entire storage Transform and Velocity, which are the one it uses. At this point, the System starts, so the query fetches the data from the storage: it returns the components pair, only for the Entities that have both. In the above example, since each Entity has both a TransformComponent and a Velocity.gd, all are fetched.

Query filters

The Query provides many filters, so to focus only on the needed information.

For example, let's say we are coding the chess; this is how I would organize my Entities:

  • I would use the component Piece to identify that such Entity is a piece.
  • I would use the component Alive to identify that such component is still on the board.
  • I would use the component White or Black.
  • I would set the piece type: Pawn, Knight, Bishop, etc...
[Piece][Alive][White][Pawn]
[Piece][Alive][White][Bishop]
[Piece][Alive][White][Knight]
[Piece][Alive][Black][Pawn]
[Piece][Alive][Black][Bishop]
[Piece][Alive][Black][Knight]
[Piece][Black][Bishop]
....
....
....

This is the storage view:

[Piece][Alive][_____][White][Pawn][______][______]
[Piece][Alive][_____][White][____][Bishop][______]
[Piece][Alive][_____][White][____][______][Knight]
[Piece][Alive][Black][_____][Pawn][______][______]
[Piece][Alive][Black][_____][____][Bishop][______]
[Piece][Alive][Black][_____][____][______][Knight]
[Piece][_____][Black][_____][____][______][Knight]
....
....
....

Without

Is a filter that requires the component is not assigned to the Entity. Let's count the dead pieces, for the black side:

func _prepare():
	with_component(ECS.Piece, IMMUTABLE)
	with_component(ECS.Black, IMMUTABLE)
	without_component(ECS.Alive, IMMUTABLE)
Query<Piece, Black, Without<Alive>> query;

Maybe

This filter is a crossover between With and Without: If the specified component is assigned it's returned, otherwise null. It's useful when it's necessary to fetch a set of entities but in addition to it, you want to fetch a component that maybe missing. For example, let's say we want to take all the alive Pawns in the match, and we want to know if it's white.

func _prepare():
	with_component(ECS.Pawn, IMMUTABLE)
	with_component(ECS.Alive, IMMUTABLE)
	maybe_component(ECS.White, IMMUTABLE)
Query<Pawn, Alive, Maybe<White>> query;

The above queries returns the alive Pawns, and the component White can be null if not assigned, thanks to the maybe filter.

[Pawn][Alive][White]
[Pawn][Alive][_____] # This can be NULL thanks to maybe filter.

Instead, using the with filter (like Query<Pawn, Alive, White> query;) we obtain only the first Entity, because the with filter requires that all the components are assigned:

[Pawn][Alive][White]

And the without filter returns only the second Entity, because the without filter requires that the component is missing:

[Pawn][Alive][_____]

Changed

This filter is useful when you want to fetch the Entities if the marked components changed. For example, if you want to take all the pieces that moved in that specific frame you can use the changed filter like follow:

func _prepare():
	with_component(ECS.Piece, IMMUTABLE)
	changed_component(ECS.TransformComponent, IMMUTABLE)
Query<Piece, Changed<TransformComponent>> query;

The above queries, returns something only when the TransformComponent change. Each frame the changed is reset. This is a lot useful when you want to execute code only when the state change.

Batch

The Batch Storage has the ability to store multiple Component (of the same type) per Entity; It's possible to retrieve those components, by using the Batch filter.

The following code, erode the health by the damage.amount of each Entity.

Query<Health, Batch<const DamageEvent>> query;
for( auto [health_comp, damage] : query ){
	for(uint32_t i = 0; i < damage.get_size(); i+=1){
		health_comp.health -= damage.amount;
	}
}

📝 Note, this snippet computes the damage for all the Entities in the world no matter the type.

This filter, allow filter nesting, so you can combine in this way:

Query<Batch<Changed<const Colour>>> query;
for( auto [mesh_comp, colour] : query ){
	for(uint32_t i = 0; i < colour.get_size(); i+=1){
		print_line(colour);
	}
}

💡 The mutability matters (const).

Any

With the Any filter you can specify many components, and it returns the first valid component. The Any filter, fetches the data if at least one of its own filters is satisfied.

This filter is a lot useful, let's see an example. Let's say you have the following enemy teams:

  • Enemy team 1
  • Enemy team 2
  • Enemy team 3

One way to code this, is by creating a component that stores the team information, something like this:

EnemyTeam {
	team_id: int
}

Let's suppose we need to fetch the enemies of the team 1 and 3; we can do it by using a query that fetches all the Entities with the component EnemyTeam and using an if we can filter it. Something like this:

Query<Name, EnemyTeam> query;
for(auto [name, team] : query ){
	if(team.team_id == 1 or team.team_id == 3){
		print_line(name);
	}
}

As you can imagine, fetch all the data is not so optimal and however the code is ugly, but thanks to Any filter we have a more elegant and optimal way. So let's see how it looks like.

This time, we create three different components (instead to one):

  • EnemyTeam1
  • EnemyTeam2
  • EnemyTeam3

Our enemies will have the right component depending on the team, so we can fetch it in this way:

Query<name, Any<EnemyTeam1, EnemyTeam3>> query;
for(auto [name, team] : query ){
	print_line(name);
}

In this way, we can fetch directly the needed Entities, and the code is much beautiful.

🔥 Pro tip: If your fetched components derives all from a base type:

struct EnemyTeam {}
struct EnemyTeam1 : public EnemyTeam {}
struct EnemyTeam3 : public EnemyTeam {}

you don't even need to check the type using team.is<EnemyTeam1>(), rather you can just unwrap it team.as<EnemyTeam>().

This is a lot useful when integrating libraries that have polymorphic objects.

The Any filter, supports nesting. For example, you can use the Changed filter in this way:

Query<Any<const TagA, Changed<const TagB>>> query;

Remember that the fist valid filter is returned.

💡 The mutability matters (const).

⚠️ Known limitations: Query<Any<TagA, TagB>> if you have an Entity that satisfy more filters, like in the below case (Entity 2):

  [Entity 0, TagA, ___]
  [Entity 1, ___, TagB]
  [Entity 2, TagA,TagB]

The query fetches the Entity 2 twice, but the first specified component is always taken (in this case the TagA). Remove this limitation would be a lot more expensive than useful.

Other features

Fetch the EntityID

Sometimes, it's useful to know the EntityID you are fetching; You can extract this information in this way:

func _prepare():
	with_component(ECS.Piece, IMMUTABLE)
	with_component(ECS.Knight, IMMUTABLE)

func _for_each(piece, knight, alive):
	var entity = get_current_entity() # <--- Note
Query<EntityID, Piece, Knight> query;

The above query, returns the EntityID so you can perform operations like add or remove another Component or remove the Entity.

[EntityID 0][Piece]
[EntityID 1][Piece]
[EntityID 2][Piece]
[EntityID 3][Piece]
[EntityID 4][Piece]
[EntityID 5][Piece]
[EntityID 6][Piece]

Count the entities of this Query.

To count the Entities, is possible to use the function .count(): this function fetches the storage and return the count of the Entities.

var query = DynamicQuery()
query.with(ECS.Piece, IMMUTABLE)
query.count()
Query<Piece> query;
query.count();

Global and Local space.

There are components that can have a Local value and a Global value; for example the TransformComponent, can return the Entity local transformation (relative to its parent), or global (relative to the world). Check this for more info: Hierarchy.

It's possible to fetch the information of a specific space just by specifying it.

func _prepare():
	set_space(ECS.GLOBAL)
	with_component(ECS.TransformComponent, IMMUTABLE)

func _for_each(transform):
	pass
Query<TransformComponent> query;
for(auto [transform] : query.space(GLOBAL)) {
	// ...
}

Performances

Under the hood, the Query and the DynamicQuery are able to iterate only the needed Entities, depending on the filters that you provide, so no resource is wasted. For example, if you have a game with a million Entities that are using the TransformComponent, and you need to fetch only the changed one: the following query Query<Changed<TransformComponent>> will only fetch the changed components, thanks to a storage that keeps track of it.

On top of that, if you want to know the changed transforms for a specific set of entities, using Query<Changed<TransformComponent>, RedTeam>: notice that this query is not going to iterate over all the changed transforms as before, but will iterate only on the RedTeam entities, and will return only the one with the changed transform.

The query, has a mechanism that establish which is the best way to fetch the data, according to the data given in that particular moment. This mechanism works with all filters.

Even if it's blazing fast for a storage to keep track of the changed Component, not all are tracking the changes. At runtime, the Storage is marked to track or not to track the changed components: depending on if there is a System using the Changed filter for that storage.

Conclusion

This is the conclusion for this overview of the Query mechanism in Godex, if you have any question join the community on discord ✌️.