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:

Screenshot from 2021-02-07 14-28-58

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.

Screenshot from 2021-01-26 11-54-00

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

ezgif com-video-to-gif(7)

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