The label and sound don't play when all ships are destroyed

Hi, I’m using Godot Version 4.3 for my shoot-em-up, but I’m having another problem. I added an explosion system to my ships, and when all the ships are destroyed, it should display the label “CompletedLevelLabel.” This stops the AudioStreamPlayer Level1BackGroundMusic and plays the AudioStreamPlayer LevelCompletedOST, but when all the ships are destroyed, nothing happens.

Here is the video that illustrates the problem.

Here is the code for forming and destroying ships.

extends Node2D

# --- Instanciation de la formation ---
@export var enemyun_scene: PackedScene = preload("res://ennemyun.tscn")
@export var ennemydeux_scene: PackedScene = preload("res://ennemydeux.tscn")

# Formation : 3 rangées et 5 colonnes
var rows: int = 3
var cols: int = 5

# Position de départ (locale) et espacement entre les ennemis
@export var start_position: Vector2 = Vector2(150, 100)
@export var spacing: Vector2 = Vector2(150, 120)

# Tableau stockant, pour chaque colonne, la liste des ennemis instanciés
var columns: Array = []

# --- Paramètres de déplacement de la formation ---
@export var enemy_speed: float = 100.0  # Vitesse horizontale de la formation
var direction: int = -1                 # -1 pour aller vers la gauche, 1 pour aller vers la droite

func _ready() -> void:
	# Initialisation du tableau des colonnes
	columns.resize(cols)
	for i in range(cols):
		columns[i] = []
	
	# Instanciation des ennemis dans une formation 3 × 5
	for row in range(rows):
		for col in range(cols):
			var enemy_instance: Node
			# Pour la rangée du milieu (row == 1), on utilise ennemydeux, sinon enemyun.
			if row == 1:
				enemy_instance = ennemydeux_scene.instantiate()
			else:
				enemy_instance = enemyun_scene.instantiate()
			# Positionnement en coordonnées locales
			enemy_instance.position = start_position + Vector2(col * spacing.x, row * spacing.y)
			add_child(enemy_instance)
			columns[col].append(enemy_instance)
	
	# Connexion des timers pour le tir par colonne
	for col in range(cols):
		var t: Timer = Timer.new()
		t.wait_time = float(col + 1)
		t.one_shot = false
		t.autostart = true
		add_child(t)
		match col:
			0: t.connect("timeout", Callable(self, "_on_column0_timeout"))
			1: t.connect("timeout", Callable(self, "_on_column1_timeout"))
			2: t.connect("timeout", Callable(self, "_on_column2_timeout"))
			3: t.connect("timeout", Callable(self, "_on_column3_timeout"))
			4: t.connect("timeout", Callable(self, "_on_column4_timeout"))
	
	# Connexion aux signaux des murs (adaptés à votre arborescence)
	var left_wall = get_node("../LeftWall")
	var right_wall = get_node("../RightWall")
	
	if left_wall:
		var left_callable = Callable(self, "_on_left_wall_entered")
		if left_wall.is_connected("area_entered", left_callable):
			left_wall.disconnect("area_entered", left_callable)
		left_wall.connect("area_entered", left_callable)
	else:
		push_error("LeftWall non trouvé !")
		
	if right_wall:
		var right_callable = Callable(self, "_on_right_wall_entered")
		if right_wall.is_connected("area_entered", right_callable):
			right_wall.disconnect("area_entered", right_callable)
		right_wall.connect("area_entered", right_callable)
	else:
		push_error("RightWall non trouvé !")
	
	# --- Gestion de fin de niveau ---
	# Connecte le signal "node_removed" pour surveiller la disparition des ennemis.
	get_tree().connect("node_removed", Callable(self, "_on_node_removed"))
	
	# On s'attend à ce que dans la scène se trouvent les nœuds suivants :
	# - CompletedLevelLabel (Label) : caché au départ.
	# - Level1BackGroundMusic (AudioStreamPlayer).
	# - LevelCompletedOST (AudioStreamPlayer).

func _physics_process(delta: float) -> void:
	# Déplacement simple du conteneur de la formation
	position.x += enemy_speed * direction * delta

func _on_left_wall_entered(_area: Area2D) -> void:
	if direction < 0:
		print("Collision avec LeftWall – inversion vers la droite.")
		direction = 1

func _on_right_wall_entered(_area: Area2D) -> void:
	if direction > 0:
		print("Collision avec RightWall – inversion vers la gauche.")
		direction = -1

func fire_from_column(col: int) -> void:
	var col_enemies = columns[col]
	var bottom_enemy = null
	# Recherche de l'ennemi le plus bas dans la colonne
	for enemy in col_enemies:
		if not is_instance_valid(enemy):
			continue
		if bottom_enemy == null or enemy.position.y > bottom_enemy.position.y:
			bottom_enemy = enemy
	if bottom_enemy != null:
		print("Colonne", col, "-> bottom enemy:", bottom_enemy.name)
		if bottom_enemy.has_method("fire_laser"):
			bottom_enemy.fire_laser()
		else:
			print("L'ennemi", bottom_enemy.name, "n'a pas la méthode fire_laser()")
	else:
		print("Aucun ennemi valide dans la colonne", col)

func _on_column0_timeout() -> void:
	fire_from_column(0)

func _on_column1_timeout() -> void:
	fire_from_column(1)

func _on_column2_timeout() -> void:
	fire_from_column(2)

func _on_column3_timeout() -> void:
	fire_from_column(3)

func _on_column4_timeout() -> void:
	fire_from_column(4)

# --- Gestion de fin de niveau ---

# Appelé quand un nœud est retiré de l'arbre.
func _on_node_removed(node: Node) -> void:
	if node.is_in_group("enemy"):
		var remaining = get_tree().get_nodes_in_group("enemy")
		if remaining.size() == 0:
			_all_enemies_destroyed()

func _all_enemies_destroyed() -> void:
	# Tous les ennemis ont disparu : affiche le label, arrête le BGM, lance l'OST de fin, et passe au niveau 2.
	var label = get_node("CompletedLevelLabel")
	var bgm = get_node("Level1BackGroundMusic")
	var completed_ost = get_node("LevelCompletedOST")
	
	label.visible = true
	bgm.stop()
	completed_ost.play()
	
	if not completed_ost.is_connected("finished", Callable(self, "_on_level_completed_finished")):
		completed_ost.connect("finished", Callable(self, "_on_level_completed_finished"))

func _on_level_completed_finished() -> void:
	get_tree().change_scene_to_file("res://level2.tscn")

Votre problème est ici.

I don’t actually see it in your code, but I believe the problem is that you are calling queue_free() somewhere else and relying on the node_removed signal.

First, you can connect to the signal as of Godot 4 by using :

get_tree().node_removed.connect(_on_node_removed)

It’s a lot less typing and a lot clearer.

Second, queue_free() is cached until the end of the frame. So even though the object has left the tree, it actually hasn’t been destroyed, and so get_tree().get_nodes_in_group(“enemy”) actually equals 1, not 0 when you call it. (If you print it out you can confirm this.)

I recommend you do this:

func _on_node_removed(node: Node) -> void:
	if node.is_in_group("enemy"):
		var remaining = get_tree().get_nodes_in_group("enemy")
		if remaining.size() <= 1:
			_all_enemies_destroyed()

It will work because when there’s only one enemy left and it gets called, there will still be two enemies left according to this call. The other solution would be to use call_deferred()

func _on_node_removed(node: Node) -> void:
	call_deferred(_check_if_all_enemies_destroyed)

func _check_if_all_enemies_destroyed() -> void:
	if node.is_in_group("enemy"):
		var remaining = get_tree().get_nodes_in_group("enemy")
		if remaining.size() == 0:
			_all_enemies_destroyed()

This would defer the call to the end of the frame and should work.

Personally I prefer the first solution. Also,there’s no need to declare the remaining variable. It’s just extra work that takes up RAM and processing time. This is what I would do:

func _on_node_removed(node: Node) -> void:
	if node.is_in_group("enemy"):
		if get_tree().get_nodes_in_group("enemy").size() <= 1
			_all_enemies_destroyed()

I tried to modify the script to add tips but when I destroy all the ships nothing happens.

extends Node2D

# --- Instanciation de la formation ---
@export var enemyun_scene: PackedScene = preload("res://ennemyun.tscn")
@export var ennemydeux_scene: PackedScene = preload("res://ennemydeux.tscn")

# Formation : 3 rangées et 5 colonnes
var rows: int = 3
var cols: int = 5

# Position de départ (locale) et espacement entre les ennemis
@export var start_position: Vector2 = Vector2(150, 100)
@export var spacing: Vector2 = Vector2(150, 120)

# Tableau stockant, pour chaque colonne, la liste des ennemis instanciés
var columns: Array = []

# --- Paramètres de déplacement de la formation ---
@export var enemy_speed: float = 100.0  # Vitesse horizontale de la formation
var direction: int = -1                 # -1 pour aller vers la gauche, 1 pour aller vers la droite

func _ready() -> void:
	# Initialisation du tableau des colonnes
	columns.resize(cols)
	for i in range(cols):
		columns[i] = []
	
	# Instanciation des ennemis dans une formation 3 × 5
	for row in range(rows):
		for col in range(cols):
			var enemy_instance: Node
			# Pour la rangée du milieu (row == 1), on utilise ennemydeux, sinon enemyun.
			if row == 1:
				enemy_instance = ennemydeux_scene.instantiate()
			else:
				enemy_instance = enemyun_scene.instantiate()
			# Positionnement en coordonnées locales
			enemy_instance.position = start_position + Vector2(col * spacing.x, row * spacing.y)
			add_child(enemy_instance)
			columns[col].append(enemy_instance)
	
	# Connexion des timers pour le tir par colonne
	for col in range(cols):
		var t: Timer = Timer.new()
		t.wait_time = float(col + 1)
		t.one_shot = false
		t.autostart = true
		add_child(t)
		match col:
			0: t.connect("timeout", Callable(self, "_on_column0_timeout"))
			1: t.connect("timeout", Callable(self, "_on_column1_timeout"))
			2: t.connect("timeout", Callable(self, "_on_column2_timeout"))
			3: t.connect("timeout", Callable(self, "_on_column3_timeout"))
			4: t.connect("timeout", Callable(self, "_on_column4_timeout"))
	
	# Connexion aux murs (en adaptant les chemins par rapport à votre hiérarchie)
	var left_wall = get_node("../LeftWall")
	var right_wall = get_node("../RightWall")
	
	if left_wall:
		var left_callable = Callable(self, "_on_left_wall_entered")
		if left_wall.is_connected("area_entered", left_callable):
			left_wall.disconnect("area_entered", left_callable)
		left_wall.connect("area_entered", left_callable)
	else:
		push_error("LeftWall non trouvé !")
		
	if right_wall:
		var right_callable = Callable(self, "_on_right_wall_entered")
		if right_wall.is_connected("area_entered", right_callable):
			right_wall.disconnect("area_entered", right_callable)
		right_wall.connect("area_entered", right_callable)
	else:
		push_error("RightWall non trouvé !")
	
	# --- Gestion de fin de niveau ---
	# Connexion du signal node_removed via Godot 4
	get_tree().node_removed.connect(_on_node_removed)

func _physics_process(delta: float) -> void:
	# Déplacement simple du conteneur de la formation
	position.x += enemy_speed * direction * delta

func _on_left_wall_entered(_area: Area2D) -> void:
	if direction < 0:
		print("Collision avec LeftWall – inversion vers la droite.")
		direction = 1

func _on_right_wall_entered(_area: Area2D) -> void:
	if direction > 0:
		print("Collision avec RightWall – inversion vers la gauche.")
		direction = -1

func fire_from_column(col: int) -> void:
	var col_enemies = columns[col]
	var bottom_enemy = null
	# Recherche de l'ennemi le plus bas dans la colonne
	for enemy in col_enemies:
		if not is_instance_valid(enemy):
			continue
		if bottom_enemy == null or enemy.position.y > bottom_enemy.position.y:
			bottom_enemy = enemy
	if bottom_enemy != null:
		print("Colonne", col, "-> bottom enemy:", bottom_enemy.name)
		if bottom_enemy.has_method("fire_laser"):
			bottom_enemy.fire_laser()
		else:
			print("L'ennemi", bottom_enemy.name, "n'a pas la méthode fire_laser()")
	else:
		print("Aucun ennemi valide dans la colonne", col)

func _on_column0_timeout() -> void:
	fire_from_column(0)

func _on_column1_timeout() -> void:
	fire_from_column(1)

func _on_column2_timeout() -> void:
	fire_from_column(2)

func _on_column3_timeout() -> void:
	fire_from_column(3)

func _on_column4_timeout() -> void:
	fire_from_column(4)

# --- Fin de niveau ---

# Appelé quand un nœud est retiré de l'arbre.
func _on_node_removed(node: Node) -> void:
	if node.is_in_group("enemy"):
		# Utilisation de la première solution : si le nombre d'ennemis restants est inférieur ou égal à 1...
		if get_tree().get_nodes_in_group("enemy").size() <= 1:
			_all_enemies_destroyed()

func _all_enemies_destroyed() -> void:
	print("Tous les ennemis ont été détruits. Fin du niveau !")
	var current_scene = get_tree().current_scene
	# Récupération des nœuds d'interface dans la scène Main
	var label: Label = current_scene.get_node("CompletedLevelLabel")
	label.visible = true
	print("Label CompletedLevelLabel rendu visible")
	
	var bgm: AudioStreamPlayer = current_scene.get_node("Level1BackGroundMusic")
	bgm.stop()
	print("BGM arrêté")
	
	var completed_ost: AudioStreamPlayer = current_scene.get_node("LevelCompletedOST")
	if completed_ost.stream:
		completed_ost.play()
		print("OST de fin lancé")
	else:
		push_error("Le stream audio de LevelCompletedOST est nul")
	
	if not completed_ost.is_connected("finished", Callable(self, "_on_level_completed_finished")):
		completed_ost.connect("finished", Callable(self, "_on_level_completed_finished"))
		print("Signal 'finished' de LevelCompletedOST connecté")

func _on_level_completed_finished() -> void:
	print("OST terminée, chargement du niveau 2.")
	get_tree().change_scene_to_file("res://level2.tscn")

I think I’ve solved the problem!

Indeed, a variable was missing each time the ships were destroyed, which meant the script was unable to know when the level had changed.

extends Node2D

# --- Instanciation de la formation ---
@export var enemyun_scene: PackedScene = preload("res://ennemyun.tscn")
@export var ennemydeux_scene: PackedScene = preload("res://ennemydeux.tscn")

# Formation : 3 rangées et 5 colonnes
var rows: int = 3
var cols: int = 5

# Position de départ (locale) et espacement entre les ennemis
@export var start_position: Vector2 = Vector2(150, 100)
@export var spacing: Vector2 = Vector2(150, 120)

# Tableau stockant, pour chaque colonne, la liste des ennemis instanciés
var columns: Array = []

# --- Paramètres de déplacement de la formation ---
@export var enemy_speed: float = 100.0  # Vitesse horizontale de la formation
var direction: int = -1                 # -1 pour aller vers la gauche, 1 pour aller vers la droite

# Variable de comptage des ennemis créés (qui seront décrémentés quand ils se détruiront)
var enemy_remaining: int = 0

func _ready() -> void:
	# Initialisation du tableau des colonnes
	columns.resize(cols)
	for i in range(cols):
		columns[i] = []
	
	# Instanciation des ennemis dans une formation 3 × 5
	for row in range(rows):
		for col in range(cols):
			var enemy_instance: Node
			# Pour la rangée du milieu (row == 1), on utilise ennemydeux, sinon enemyun.
			if row == 1:
				enemy_instance = ennemydeux_scene.instantiate()
			else:
				enemy_instance = enemyun_scene.instantiate()
			# Incrementer notre compteur pour chaque ennemi créé
			enemy_remaining += 1
			# Positionnement en coordonnées locales
			enemy_instance.position = start_position + Vector2(col * spacing.x, row * spacing.y)
			add_child(enemy_instance)
			columns[col].append(enemy_instance)
	
	# Connexion des timers pour le tir par colonne
	for col in range(cols):
		var t: Timer = Timer.new()
		t.wait_time = float(col + 1)
		t.one_shot = false
		t.autostart = true
		add_child(t)
		match col:
			0: t.connect("timeout", Callable(self, "_on_column0_timeout"))
			1: t.connect("timeout", Callable(self, "_on_column1_timeout"))
			2: t.connect("timeout", Callable(self, "_on_column2_timeout"))
			3: t.connect("timeout", Callable(self, "_on_column3_timeout"))
			4: t.connect("timeout", Callable(self, "_on_column4_timeout"))
	
	# Connexion aux murs (en adaptant les chemins selon la hiérarchie)
	var left_wall = get_node("../LeftWall")
	var right_wall = get_node("../RightWall")
	
	if left_wall:
		var left_callable = Callable(self, "_on_left_wall_entered")
		if left_wall.is_connected("area_entered", left_callable):
			left_wall.disconnect("area_entered", left_callable)
		left_wall.connect("area_entered", left_callable)
	else:
		push_error("LeftWall non trouvé !")
		
	if right_wall:
		var right_callable = Callable(self, "_on_right_wall_entered")
		if right_wall.is_connected("area_entered", right_callable):
			right_wall.disconnect("area_entered", right_callable)
		right_wall.connect("area_entered", right_callable)
	else:
		push_error("RightWall non trouvé !")
	
	# Optionnel : vous pouvez aussi connecter ici le signal node_removed pour du debug, 
	# mais le comptage sera géré par enemy_destroyed() appelé par chaque ennemi lors de sa disparition.
	# get_tree().node_removed.connect(_on_node_removed)

func _physics_process(delta: float) -> void:
	# Déplacement simple du conteneur de la formation
	position.x += enemy_speed * direction * delta

func _on_left_wall_entered(_area: Area2D) -> void:
	if direction < 0:
		print("Collision avec LeftWall – inversion vers la droite.")
		direction = 1

func _on_right_wall_entered(_area: Area2D) -> void:
	if direction > 0:
		print("Collision avec RightWall – inversion vers la gauche.")
		direction = -1

func fire_from_column(col: int) -> void:
	var col_enemies = columns[col]
	var bottom_enemy = null
	# Recherche de l'ennemi le plus bas dans la colonne
	for enemy in col_enemies:
		if not is_instance_valid(enemy):
			continue
		if bottom_enemy == null or enemy.position.y > bottom_enemy.position.y:
			bottom_enemy = enemy
	if bottom_enemy != null:
		print("Colonne", col, "-> bottom enemy:", bottom_enemy.name)
		if bottom_enemy.has_method("fire_laser"):
			bottom_enemy.fire_laser()
		else:
			print("L'ennemi", bottom_enemy.name, "n'a pas la méthode fire_laser()")
	else:
		print("Aucun ennemi valide dans la colonne", col)

func _on_column0_timeout() -> void:
	fire_from_column(0)

func _on_column1_timeout() -> void:
	fire_from_column(1)

func _on_column2_timeout() -> void:
	fire_from_column(2)

func _on_column3_timeout() -> void:
	fire_from_column(3)

func _on_column4_timeout() -> void:
	fire_from_column(4)

# Fonction de décrémentation appelé par chaque ennemi quand il se détruit
func enemy_destroyed() -> void:
	enemy_remaining -= 1
	print("Ennemis restants :", enemy_remaining)
	if enemy_remaining <= 0:
		_all_enemies_destroyed()

func _all_enemies_destroyed() -> void:
	print("Tous les ennemis ont été détruits. Fin du niveau !")
	# Récupère les nœuds d'interface depuis la scène principale (Main)
	var current_scene = get_tree().current_scene
	var label: Label = current_scene.get_node("CompletedLevelLabel")
	label.visible = true
	print("Label CompletedLevelLabel rendu visible")
	
	var bgm: AudioStreamPlayer = current_scene.get_node("Level1BackGroundMusic")
	bgm.stop()
	print("BGM arrêté")
	
	var completed_ost: AudioStreamPlayer = current_scene.get_node("LevelCompletedOST")
	if completed_ost.stream:
		completed_ost.play()
		print("OST de fin lancé")
	else:
		push_error("Le stream audio de LevelCompletedOST est nul")
	
	if not completed_ost.is_connected("finished", Callable(self, "_on_level_completed_finished")):
		completed_ost.connect("finished", Callable(self, "_on_level_completed_finished"))
		print("Signal 'finished' de LevelCompletedOST connecté")

func _on_level_completed_finished() -> void:
	print("OST terminée, chargement du niveau 2.")
	get_tree().change_scene_to_file("res://level2.tscn")

extends Area2D

@export var laser_scene: PackedScene = preload("res://enemy_laser.tscn")
@export var laser_speed: float = 300.0

# Paramètres pour l'explosion
@export var boom_texture: Texture = preload("res://alienminigames/boomplane/boomtexture.png")
@export var explosion_sound: AudioStream = preload("res://musiqueetson/bombysound.mp3")  # Vérifie que le chemin est correct

var exploded: bool = false  # Indique que l'ennemi a été touché (fin de vie)
var original_texture: Texture
var is_exploding: bool = false  # Indique qu'une explosion est en cours

func _ready() -> void:
	# Ajoute l'ennemi au groupe "enemy"
	add_to_group("enemy")
	if has_node("Sprite2D"):
		original_texture = $Sprite2D.texture
		print(name, " : Sprite2D trouvé, texture sauvegardée.")
	else:
		print("Erreur : Aucun nœud 'Sprite2D' dans", name)
	# Vérification pour l'AudioStreamPlayer d'explosion
	if not has_node("ExplosionPlayer"):
		print("Erreur : Aucun nœud 'ExplosionPlayer' dans", name)
	print(name, " : prêt et ajouté au groupe 'enemy'.")

func fire_laser() -> void:
	# Si l'ennemi est en train d'exploser, il ne doit plus tirer
	if exploded or is_exploding:
		return
	print(name, " tente de tirer son laser.")
	if laser_scene:
		var laser_instance = laser_scene.instantiate()
		if has_node("Projectile"):
			laser_instance.global_position = $Projectile.global_position
		else:
			print(name, " n'a pas le nœud 'Projectile' !")
		get_tree().current_scene.add_child(laser_instance)
		print(name, " a tiré son laser!")
	else:
		push_error("La scène du laser n'est pas assignée pour " + name)

func take_damage() -> void:
	print(name, " : take_damage() appelée.")
	if exploded or is_exploding:
		print(name, " : déjà en explosion.")
		return
	is_exploding = true  # Verrouille tout comportement futur (tirs, collisions, etc.)

	# Retire l'ennemi du groupe "enemy" pour qu'il ne soit plus détecté par les lasers du joueur
	remove_from_group("enemy")
	
	# Désactiver la collision en différé (pour éviter les erreurs pendant les requêtes physiques)
	if has_node("CollisionShape2D"):
		$CollisionShape2D.set_deferred("disabled", true)
	
	# Changer la texture pour afficher l'explosion
	if has_node("Sprite2D"):
		$Sprite2D.texture = boom_texture
		print(name, " : texture changée en boom_texture.")
	else:
		print("Erreur : pas de Sprite2D dans", name)
	
	# Jouer le son d'explosion
	if has_node("ExplosionPlayer"):
		$ExplosionPlayer.stream = explosion_sound
		$ExplosionPlayer.play()
		print(name, " : son d'explosion joué.")
		if not $ExplosionPlayer.is_connected("finished", Callable(self, "_on_explosion_finished")):
			$ExplosionPlayer.connect("finished", Callable(self, "_on_explosion_finished"))
			print(name, " : signal 'finished' connecté.")
	else:
		print("Erreur : pas de ExplosionPlayer dans", name)
		queue_free()

func _on_explosion_finished() -> void:
	print(name, " : explosion terminée, suppression de l'ennemi.")
	# Informe la formation (le parent) que cet ennemi est détruit
	if get_parent() != null and get_parent().has_method("enemy_destroyed"):
		get_parent().enemy_destroyed()
	exploded = true
	queue_free()
1 Like