[Composition] How should I emit a signal to external nodes?

Godot Version

4.3

Question

I’m learning Godot and now I’m trying to figure out how to proper use composition with signals.

I do have an “enemy” entity with HealthComposable, HitboxComposable and so on.
I wish to notify to external scenes when that enemy dies (For handling a wave system when all entities are dead). So I emit a signal on the HealthComposable when the health reaches to zero, but how should I notify outside of the “enemy” scene?

Should I connect the parent script with the signal, and then re-emit again from the parent node? I do not know if this is a proper way or will have performance issues.

class_name HealthComposable extends Node2D

signal update_health(value: float)
signal entity_died

@export var max_health = 100
@export var min_health = 0

var health: float:
	set(value):
		health = value
		update_health.emit(get_owner(), health)
		if health <= 0:
			entity_died.emit()
			kill_entity()

func _ready() -> void:
	health = max_health
	
func damage(amount: float):
	health -= amount
	
func heal(amount: float):
	health += clamp(amount, 0, max_health)

func kill_entity():
	get_parent().queue_free()

HitboxComposable

class_name HitboxComposable extends Area2D

@export var health_composable: HealthComposable

signal hit

func _ready() -> void:
	pass


func _on_area_entered(area: Area2D) -> void:
	if area is Sword:
		if health_composable:
			health_composable.damage(Sword.DAMAGE)
        hit.emit(Sword.DAMAGE)

What node is spawning these enemies?
You can also save the amount of enemies spawned in a static variable and whenever a enemy dies it reduces this static variable by 1

Currently I’m using a WaveManager Scene with a timer that does instantiate the enemy. I was thinking to connect to the signal from the instantiated object. But I do not have access to the signal from there.

func spawn(name: String):
	var radius = 100
	var direction = Vector2(randi_range(-1, 1), randi_range(-1, 1))
	var new_enemy: Enemy = load(characters_path + /' + name + '.tscn').instantiate()
	new_enemy.position = Game.player.global_position + direction * radius
	get_tree().root.add_child(new_enemy)

you can access the enemy from the health-component (with @export for example) and call a “die()”-method and have the enemy emit a signal called “died” and then the wavespawner can connect this

I guess then there’s no way around to just pass the signal up without having to write the code on the top-level node of each entity.

Maybe I will extend from a BaseEntity class. Thanks

As i said you technically only need one signal, if i understand you correctly

Yes, I meant if I make other enemy types, I should write the die() function on each. But anyway, not a big deal. I could write the BaseEnemy and extend from it in each type of enemy.

1 Like

You could create a class that manages those signals. Make it an autoload or a singleton to access its signals from anywhere if you don’t want or can’ use static variables for this.
This way you can also connect those signals in any object.