Skip to content
This repository has been archived by the owner on Sep 7, 2023. It is now read-only.
Reese Schultz edited this page Sep 12, 2021 · 1 revision

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.

Index

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)

	var query := DynamicQuery.new()
	query.with_component(ECS.TransformComponent, MUTABLE)
	query.with_component(ECS.Velocity_gd, IMMUTABLE)
	with_query(query)

func _execute(frame_time, query):
	while query.next():
		query[&"TransformComponent"].basis = query[&"TransformComponent"].basis.rotated(
			query[&"Velocity_gd"].velocity.normalized(),
			query[&"Velocity_gd"].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, or DynamicQuery for GDScript.

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.

To explore these, let's take the chess as example, and 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]
....
....
....

Base filter

The default filter can be thought as With. Indeed, it fetches all the entities if they have all the specified components.

For example, we can take all the alive black pieces by using this query:

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

Not

The Not is similar to !is_valid or not is_valid, so it inverts the meaning. For example, we can mutate the default filter meaning to without by using Not.

Let's count the dead pieces this time:

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

💡 Note, this filter can be nested with other filters.

  • For example: Not<Changed<Position>>: It returns the Position if not changed.

Maybe

This filter, instead of exclude the Entity when not satisfied (like Not does) it put a Null but the other Entity's components are fetched anyway. It's like a crossover between With and Not. 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 is 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():
	var query := DynamicQuery.new()
	query.with_component(ECS.Pawn, IMMUTABLE)
	query.with_component(ECS.Alive, IMMUTABLE)
	query.maybe_component(ECS.White, IMMUTABLE)
	with_query(query)
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 not filter returns only the second Entity, because the not filter requires that the component is missing:

[Pawn][Alive][_____]

💡 Note, this filter can be nested with other filters.

  • For example Maybe<Changed<Position>>: It returns the component if changed, otherwise null.

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 it like follow:

func _prepare():
	var query := DynamicQuery.new()
	query.with_component(ECS.Piece, IMMUTABLE)
	query.changed_component(ECS.TransformComponent, IMMUTABLE)
	with_query(query)
Query<Piece, Changed<TransformComponent>> query;

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

Note 📝 Each frame the changed flag is reset, so unless it changes again, the query will not fetch it.

💡 Note, this filter can be nested with other filters.

  • Not<Changed<Position>>: It returns the Position if not changed. For example:
  • Maybe<Changed<Position>>: It returns the component if changed, otherwise null.

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.

💡 Note, this filter can be nested with other filters:

  • Query<Batch<Changed<Colour>>>: Fetches the colours if changed.
  • Query<Batch<Not<Changed<Colour>>>>: Fetches the colours if NOT changed.
  • Query<Batch<Not<Changed<const Colour>>>>: Fetches the colours IMMUTABLE if NOT changed.

Any

Inside the Any filter you can specify many components, and it returns all if at least one is found.

Query<Any<Sword, Shield, Helm, Armor> query;

✅ You can read it like: Take all if one is satisfied.

This filter is really useful when used with the Changed filter.

For example, let's pretend our combat mechanics has the concept of element resistance, so we have the following components:

  • FireResistance(amount: float)
  • IceResistance(amount: float)
  • BaseArmore(amount: float)

When one of those change, we have to recompute the base armor, so we can write:

Query<BaseArmore, Any<Changed<const FireResistance>, Changed<const IceResistance>>> query;
for( auto [armor, fire, ice] : query ) {
    armor.amount = fire.amount + ice.amount;
    armor.amount *= 0.2;
}

The Any filter allow us to treat many components as one, so we can split a concept to many components. A really obvious example is if our Transform would be split to its tree sub parts: Position Rotation Scale.

// We can recompute the transform, if one of those components changes:
Query<Any<Changed<Position>, Changed<Rotation>, Changed<Scale>>> query;
for( auto [position, rotation, scale] : query ){
    // ...
}

📝 Note, the query returns a tuple that allow to easily unpack the components in one row.

💡 Note, This filter can be nested with other filters too:

  • Query<Any<Changed<Position>, Not<Changed<Rotation>>: Fetches the Position and the Rotation, if the Position is changed OR the Rotation is Not changed.

⚠️ Beware, you can't nest multiple Any, it doesn't make sense:

  • Query<Any<Position, Any<Rotation, Scale>>

Join

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

Query<Join<TagA, TagB>>

✅ You can read it like: Take TagA OR TagB.

Let's say the enemies can have two teams, and for each team we have a component:

  • EnemyTeam1
  • EnemyTeam2

Our enemies will have one or the other depending on the team, so we can fetch it in this way:

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

🔥 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.

💡 Note, This filter can be nested with other filters too:

  • Query<Join<Changed<const TagA>, Changed<TagB>>>: Take the TagA OR the TabB if one of those changed.
  • Query<Person, Any<Hat, T-shirt, Join<BlackShoes, Whiteshoes>>>: Take the Person if it wear an Hat or a T-Shirt- or BlackShoes or Whiteshoes.

Create

This filter is useful when you want to create a component for the entities that match the query.

Let's suppose you have a Car component, and you want that by default the entities with such component have also the FuelTank component. Instead of letting the user to manually add it, you can use the Create filter: godex will create and add the component if it doesn't exist.

void move_car(Query<Car, Create<FuelTank>> &p_query) {
	for(auto [car, fuel_tank] : p_query) {
		if(fuel_tank.fuel_level > 0.0) {
			// Move the car
		}
	}
}

This filter is an ergonomic improvement, the user can just add the Car component and not know about FuelTank at all.

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():
	var query := DynamicQuery.new()
	query.with_component(ECS.Piece, IMMUTABLE)
	query.with_component(ECS.Knight, IMMUTABLE)
	with_query(query)

func _execute(query):
	while query.next():
		var entity = query.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.begin(world) # This is not needed if the query is used in a system
query.with_component(ECS.Piece, IMMUTABLE)
query.count()
Query<Piece> query(world);
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():
	var query := DynamicQuery.new()
	query.with_component(ECS.TransformComponent, IMMUTABLE)
	with_query(query)

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

Fetch specific entity

Iterate it's useful, however it's not the only way to fetch the Entity information. Sometimes, it's needed to operate on a specific Entity for which we know the ID (EntityID).

In these case we can use the following syntax:

var entity_id = 1
var query = DynamicQuery()
query.begin(world) # This is not needed if the query is used in a system

query.with(ECS.Transform, IMMUTABLE)

if query.has(entity_id):
    query.fetch(entity_id)

    var entity_1_transform = query[&"transform"]
EntityID entity_id(1);

Query<Transform> query(world);
if( query.has(entity_id) ) {
    auto [entity_1_transform] = query[entity_id];
}

Tuple get

The Query returns the QueryResultTuple, that can be unpacked using the structured bindings.

auto [ comp_1, comp_2 ] = query_result;

Sometimes it's useful to extract just one component from it, so you can use the function get<>() in C++ (or in GDscript query.get_component(0)):

Query<Transform, Mesh> query;

auto result = query[entity_1];

Transform* transform = get<0>(result);
Mesh* mesh = get<1>(result);

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 ✌️.

ExampleQueries

Clone this wiki locally