Hierarchy - reeseschultz/godex GitHub Wiki
Godex supports parentage. It's possible to combine Entities
so that the final result of some components is derived by the combination of all the parent Entities
.
To compose the parentage of an Entity
it's enough make the Entity
child of another Entity
:
To the nested entities (Entity2
, Entity3
, Entity4
, [Entity
is root so it's optional]), add the component Child
, to notify Godex that you want to nest those.
Godex resolve the parenting automatically, so you can just ignore the variable parent
.
Spatial concept
Some components, are sensible to the parentage of an Entity
. For example the TransformComponent
reacts to it: so the transform of an Entity
is combined with the one of its parent (if any).
This mean that the TransformComponent
is always relative to its parent.
Internally, Godex, has two versions of this component:
- Local
TransformComponent
: Relative to its parent (or relative to global if it's root). - Global
TransformComponent
: Relative to the global space.
This doesn't need more explanations, but worth to mention that it's possible to retrieve one or the other by setting the spatial on the query:
C++
void my_system(Query<const TransformComponent> &query) {
// Local transform
while (query.is_done() == false) {
auto [transf] = query.get();
}
// Global transform
while (query.is_done() == false) {
auto [mesh, transf] = query.get(Space::GLOBAL);
}
}
GDScript
extends System
func _prepare():
set_space(ECS.GLOBAL)
with_component(ECS.TransformComponent, IMMUTABLE)
func _for_each(trans_component):
# Global
print(trans_component.transform)
💡 Note that, if you modify a transform, the change is propagated at the end of the system, once the storage is released. If you need to get the global transform right away inside the same system that did the change, you have to trigger the flush manually. This is not common, so it's up to you flush it manually, in such case.
Complex setups
Despite the complexity of the above setup; notice that it's fully moved just by one system. Indeed, we have 4 Entities
all rotating with a different speed and different axis.
The System
is the following:
## 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)
This System
is reading the info from the Velocity.gd
component, so it rotates the local transform accordingly. The Entity
setup does all the rest, so I've the following hierarchy:
Entity Velocity(0, 1, 0)
|- Entity 2 Velocity(0, -3, 0)
|- Entity 3 Velocity(1, 0, 0)
|- Entity 4 Velocity(0, 0, -5)
You can download the example project here ⬇️
Deepening
The parenting mechanism is a really useful tool especially if you need to compose a level, a UI, or you have to build a complex hierarchy set-up, the aggregation is static, or there are few unique entities.
While it's totally fine use it, thanks to ECS there are more optimal ways. The key concept of ECS is the data transformation. The data travel from one system to the other, and each system modify the given data. Like in an assembly line, it's possible to build the hierarchy on the System
s execution, rather than the Entity
.
Let's say yours is a cooking game, and so your kitchen has many ovens. Following the Object-Oriented approach, you would put the food contained by each oven as the child of the oven object. So, you would have a function, that each frame, rises the temperature of the oven. The temperature is spread to the (child) food; and so it's established when the food is cooked or burned.
With ECS, however, you can decouple the oven temperature rising and the food cooking in two systems: OvenTemperatureRiser
and FoodCooking
. During the pipeline composition, the FoodCooking
System
is added after the OvenTemperatureRiser
System
. The OvenTemperatureRiser
will take care to spread the temperature on the food, while the FoodCooking
, takes care to change the food status (Cooked | Burned).
You achieved the same result, but without having a direct dependency between food and oven. The advantage is that the operation is optimal because it's done on a packed set of data.
The rule of thumb is, try to think about data transition rather than object structure (which is an Object-Oriented concept). If you can't find a way to do it, or that part of the game is static: use the hierarchy mechanism it's totally fine.