Query - reeseschultz/godex GitHub Wiki
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.
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.
When you compose a scene like this:
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 System
s are fetching the data using a Query
, or DynamicQuery
for GDScript.
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.
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 suchEntity
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
orBlack
. - 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] .... .... ....
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;
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.
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 firstEntity
, 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, otherwisenull
.
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, otherwisenull
.
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.
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 multipleAny
, it doesn't make sense:
Query<Any<Position, Any<Rotation, Scale>>
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 itteam.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.
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.
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]
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();
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)) {
// ...
}
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];
}
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);
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.
This is the conclusion for this overview of the Query mechanism in Godex, if you have any question join the community on discord ✌️.