Not sure why my last mod is not being "killed"

Godot Version

4.7.stable

Question

I have code written to where the orb shots incoming orcs. The code always leaves one orc on screen tho, when it should’ve been shot and taken out. Screenshot below:

Below is how I have some of the nodes arrainged:

Code for MobSpawn below:

extends Node2D

@export var mob_scenes: Array[PackedScene] = []
@onready var spawn_points = [$Spawn1, $Spawn2, $Spawn3]
@onready var mob_timer: Timer = $"../Mob_timer"
@onready var wave: Label = $"../Main_CanvasLayer/VBoxContainer/HBoxContainer/Wave"

var mob_counter: int = 0
var max_mob_index: int = 0

func _on_mob_timer_timeout():
	# Create a new instance of the Mob scene.
	var mob_scene = mob_scenes[randi_range(0, max_mob_index)]
	var mob = mob_scene.instantiate()
	
	#Stores spawned mobs in Globals array
	Globals.mobs_array.append(mob)

	# Set the mob's position to the random location.
	var spawn = spawn_points.pick_random()
	mob.position = spawn.position
	
	# Spawn the mob by adding it to the Main scene.
	add_child(mob)
	
	#Tracks how many mobs have arrived
	mob_counter += 1
	
	#Ends the wave of mobs
	if mob_counter == Globals.mob_wave_capacity:
		mob_timer.stop()
		Globals.mob_wave_capacity += 10
		mob_counter = 0
		wave_tracker()

func wave_tracker():
	Globals.wave_tracker += 1
	wave.text = str(Globals.wave_tracker)
	match Globals.wave_tracker:
		5: max_mob_index += 1
		10: max_mob_index += 1
		15: mob_timer.wave_time = 0.9
		20: mob_timer.wave_time = 0.8
		25: mob_timer.wave_time = 0.7
		30: mob_timer.wave_time = 0.6
		35: mob_timer.wave_time = 0.5
		40: mob_timer.wave_time = 0.4

func _on_next_wave_pressed():
	mob_timer.start()

Code for Line2D below:

extends Line2D

var firing_status: bool = false
var shot_duration: float = 0.05

var shot_delay: float = 0.3

@onready var shot_sound: AudioStreamPlayer = $"../../../Shot_sound"
@onready var mob_spawn: Node2D = $"../.."
@onready var balance: Label = $"../../../Main_CanvasLayer/VBoxContainer/HBoxContainer/Balance"

var duplicate_array = []

func _ready():
	Globals.authorize_shots.connect(update_trajectory.bind())

func _process(_delta: float):
	#Updates the balance in the label
	balance.text = str(Globals.balance)

func update_trajectory():
	#Ensures the PackedVector2Array is cleared beforehand
	clear_points()
	
	duplicate_array = Globals.mobs_array.duplicate(true)
	
	for mob in duplicate_array:
		if Globals.shoot_counter > 0:
			add_point(Vector2(0,0))
			add_point(to_local(mob.position))
			await get_tree().create_timer(shot_duration).timeout
			clear_points()
			await get_tree().create_timer(shot_delay).timeout
			Globals.shoot_counter -= 1
			mob.when_attacked()
			Globals.mobs_array.erase(mob)
			shot_sound.play()

func _on_orb_area_shape_entered(_area_rid: RID, _area: Area2D, _area_shape_index: int, _local_shape_index: int):
	Globals.roll_dice.emit()

Code for the orc scene below:

extends CharacterBody2D

@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D

var SPEED: int
const DIRECTION = -1

func _ready():
	$AnimatedSprite2D.play()
	SPEED = Globals.mob_speed
	
func _physics_process(_delta: float):
	velocity.x = SPEED * DIRECTION

	if velocity.length() > 0:
		$AnimatedSprite2D.play("run")

	move_and_slide()

func _on_area_2d_body_entered(_body: Node2D):
	SPEED = 0
	attacking_mode()

func attacking_mode():
	while Globals.wizard_tower_health > 0:
		$AnimatedSprite2D.play("attack")
		await $AnimatedSprite2D.animation_finished
		Globals.attacked_signal.emit()

func when_attacked():
	SPEED = 0
	$AnimatedSprite2D.play("death")
	Globals.balance += 1
	await $AnimatedSprite2D.animation_finished
	queue_free()

The nodes for the orc scene is below:

image

Trying calling your queue_free like this

call_deferred("queue_free")

If there is always exactly one remaining it could be that you have initialized mob_counter too low. Try changing it to 1.

var mob_counter: int = 1

Or do you mean that you see the mob get shot and then nothing happens? In that case you probably have some game over stuff that includes pausing the tree, which runs before the mob queue_free does.

Either way I would avoid using counters such as these. Instead of counting mobs manually just use mobs_array.size(). It removes some clutter from your code and you will no longer need to check this or that or keep your int and array equal size manually. It will get annoying as you start adding other ways to add or remove mobs.

I implemented yours and silocoder’s suggestions, but I still see two mobs left attacking the wizard tower, despite being within the orb’s collisionshape:

On the “_on_orb_area_shape_entered” method, is there a signal where the methods are triggered WHILE the orcs are within the collisionshape? All I see as options are below:

So you removed mob_counter and instead use mobs_array.size() or what did you do?

I think the problem could be here:

await $AnimatedSprite2D.animation_finished
queue_free()

If you pause your tree when the wave is finished, your animations are likely paused too. You could either change process mode on the enemy node or wait with pausing the tree until the animation is finished. If this is the problem.

In my last reply i asked if you see the mob get shot and then nothing happens. Does that happen? Try to answer people’s questions. If not, it just waste everyone’s time and forces people to guess what could be wrong.

With the way my code is, the mods do get shot, then the “death” animation happens. Then the mobs get removed from the scene. However, not all mobs get shot at for some reason. Video link below, for more clarity:

Updated code are below:

Code for MobSpawn:

extends Node2D

@export var mob_scenes: Array[PackedScene] = []
@onready var spawn_points = [$Spawn1, $Spawn2, $Spawn3]
@onready var mob_timer: Timer = $"../Mob_timer"
@onready var wave: Label = $"../Main_CanvasLayer/VBoxContainer/HBoxContainer/Wave"

#var mob_counter: int = 0
var max_mob_index: int = 0

func _on_mob_timer_timeout():
	# Create a new instance of the Mob scene.
	var mob_scene = mob_scenes[randi_range(0, max_mob_index)]
	var mob = mob_scene.instantiate()
	
	#Stores spawned mobs in Globals array
	Globals.mobs_array.append(mob)

	# Set the mob's position to the random location.
	var spawn = spawn_points.pick_random()
	mob.position = spawn.position
	
	# Spawn the mob by adding it to the Main scene.
	add_child(mob)
	
	#Ends the wave of mobs
	if Globals.mobs_array.size() == Globals.mob_wave_capacity:
		mob_timer.stop()
		Globals.mob_wave_capacity += 10

func wave_tracker():
	Globals.wave_tracker += 1
	wave.text = str(Globals.wave_tracker)
	match Globals.wave_tracker:
		5: max_mob_index += 1
		10: max_mob_index += 1
		15: mob_timer.wave_time = 0.9
		20: mob_timer.wave_time = 0.8
		25: mob_timer.wave_time = 0.7
		30: mob_timer.wave_time = 0.6
		35: mob_timer.wave_time = 0.5
		40: mob_timer.wave_time = 0.4

func _on_next_wave_pressed():
	wave_tracker()
	mob_timer.start()

Code for Line2D:

extends Line2D

var firing_status: bool = false
var shot_duration: float = 0.05

var shot_delay: float = 0.3

@onready var shot_sound: AudioStreamPlayer = $"../../../Shot_sound"
@onready var balance: Label = $"../../../Main_CanvasLayer/VBoxContainer/HBoxContainer/Balance"

var duplicate_array = []

func _ready():
	Globals.authorize_shots.connect(update_trajectory.bind())

func _process(_delta: float):
	#Updates the balance in the label
	balance.text = str(Globals.balance)

func update_trajectory():
	#Ensures the PackedVector2Array is cleared beforehand
	clear_points()
	
	duplicate_array = Globals.mobs_array.duplicate(true)
	
	for mob in duplicate_array:
		if Globals.shoot_counter > 0:
			add_point(Vector2(0,0))
			add_point(to_local(mob.position))
			await get_tree().create_timer(shot_duration).timeout
			clear_points()
			await get_tree().create_timer(shot_delay).timeout
			Globals.shoot_counter -= 1
			mob.when_attacked()
			Globals.mobs_array.erase(mob)
			shot_sound.play()

#Change code to where the signal will emit as long as there are scenes present in the collision shape
func _on_orb_area_shape_entered(_area_rid: RID, _area: Area2D, _area_shape_index: int, _local_shape_index: int):
	Globals.roll_dice.emit()

Code for the mob scene:

extends CharacterBody2D

@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D

var SPEED: int
const DIRECTION = -1

func _ready():
	$AnimatedSprite2D.play()
	SPEED = Globals.mob_speed
	
func _physics_process(_delta: float):
	velocity.x = SPEED * DIRECTION

	if velocity.length() > 0:
		$AnimatedSprite2D.play("run")

	move_and_slide()

func _on_area_2d_body_entered(_body: Node2D):
	SPEED = 0
	attacking_mode()

func attacking_mode():
	while Globals.wizard_tower_health > 0:
		$AnimatedSprite2D.play("attack")
		await $AnimatedSprite2D.animation_finished
		Globals.attacked_signal.emit()

func when_attacked():
	SPEED = 0
	$AnimatedSprite2D.play("death")
	Globals.balance += 1
	await $AnimatedSprite2D.animation_finished #<---- If I remove this line, then the mob disappears before the animation is finished
	call_deferred("queue_free")

I think the problem is in this area.
One would think that duplicating deep = true nets you a completely different set of mobs, but there is a caveat in the docs:

If deep is true, a deep copy is returned: all nested arrays and dictionaries are also duplicated (recursively). Any Resource is still shared with the original array, though.

I would test this by printing out the original and duplicated array both before and after the erase.
I can’t quite figure out how this would work to cause your issue but it looks suspect.

PS:
Globals.authorize_shots.connect(update_trajectory.bind())
If you aren’t binding anything you don’t need the bind().

Your gut feeling seems to be correct. Below is the updated code for the update_trajectory() method:

func update_trajectory():
#Ensures the PackedVector2Array is cleared beforehand

clear_points()

duplicate_array = Globals.mobs_array.duplicate(true)

print("Global mobs: ")
print(Globals.mobs_array)
print("Duplicated mobs: ")
print(duplicate_array)

for mob in duplicate_array:
	if Globals.shoot_counter > 0:
		add_point(Vector2(0,0))
		add_point(to_local(mob.position))
		await get_tree().create_timer(shot_duration).timeout
		clear_points()
		await get_tree().create_timer(shot_delay).timeout
		Globals.shoot_counter -= 1
		mob.when_attacked()
		Globals.mobs_array.erase(mob)
		print("Global mobs after shot: ")
		print(Globals.mobs_array)
		print("Duplicated mobs after shot: ")
		print(duplicate_array)
		shot_sound.play()

The last shot of the output showed this:
Global mobs:
[@CharacterBody2D@9:<CharacterBody2D#49576675277>, @CharacterBody2D@10:<CharacterBody2D#49643784145>]
Duplicated mobs:
[@CharacterBody2D@9:<CharacterBody2D#49576675277>, @CharacterBody2D@10:<CharacterBody2D#49643784145>]
Global mobs after shot:
[@CharacterBody2D@10:<CharacterBody2D#49643784145>]
Duplicated mobs after shot:
[@CharacterBody2D@9:<CharacterBody2D#49576675277>, @CharacterBody2D@10:<CharacterBody2D#49643784145>]

There’s an obvious mismatch on the number of mobs stored in the duplicate_array vs the Globals.mobs_array. This starts to happen at the beginning of the “shooting”.

Should I just remove the duplicate array in general? FYI, I have the mobs array in general, so the game can track how many remaining mobs are on screen. That way, the game will keep shooting any remaining mobs that are “in range” of the orb.

I don’t like the approach in general.
GODOT has a game loop built in most clearly represented by the process() loop.
The mobs use that loop to move.
But the function that controls the orb actions sidesteps the loop and builds a blob of code that runs in its own for loop mired with awaits.

When you use await, Godot does not freeze or pause the game. Instead, it pauses only the execution of that specific function, turns it into a coroutine, and yields control back to the engine.

This in itself isn’t wrong but I don’t like the mismatch here where the orcs are running using the process loop, while the orb is shooting them in a sort of independent side loop.

I would kill off the orcs one at a time, using the process loop; so no looping over all the orcs in one function.
This is a major refactor , but this method would allow greater flexibility in the long run.
Most obviously, the code in its current form cannot show the animation for two orcs being killed at the same time whereas eliminating orcs one at a time in the process loop would be able to show that.

That’s an interesting point. So to further this conversation, I have the collisionshape of the orb as the following:

This circular collisionshape for the orb is the orb’s shooting range. How do I write the code to where the orb fires shots while mobs are present in the circular collisionshape? As of now, my method is triggered when they enter the shape, but that’s it.

Connect the orbs collision shape body_entered signal to a function similar to what you already have with update_trajectory().
The difference is this one acts only on the body that entered, so a single orc.
Give your orcs a class name so you can filter them out.

func on_orc_entered(body): 
   if body is Orc:
       shoot_orc()  
       Globals.mobs_array.erase(mob)

Inside shoot_orc(), you will run the animation and queue_free() when it is done.
The code slips past to erasing the mob unit from the array and returns.
If there is another mob to process, the code will act on it when it crosses into the collision shape.

Thank you. I didn’t do the class name yet because I have only one type of enemy coming in, but I was able to update the code accordingly:

extends Line2D

var firing_status: bool = false
var shot_duration: float = 0.05

var shot_delay: float = 0.3

@onready var shot_sound: AudioStreamPlayer = $“../../../Shot_sound”
@onready var balance: Label = $“../../../Main_CanvasLayer/VBoxContainer/HBoxContainer/Balance”


func _process(_delta: float):
   #Updates the balance in the label
   balance.text = str(Globals.balance)

func _on_orb_area_shape_entered(_area_rid: RID, _area: Area2D, _area_shape_index: int, _local_shape_index: int):
   add_point(Vector2(0,0))
   add_point(to_local(Globals.mobs_array[0].position))
   await get_tree().create_timer(shot_duration).timeout
   clear_points()
   await get_tree().create_timer(shot_delay).timeout
   Globals.shoot_counter -= 1
   Globals.mobs_array[0].when_attacked()
   Globals.mobs_array.erase(Globals.mobs_array[0])
   shot_sound.play()

This causes the orb to fire at each orc individually. It worked! But now, I need to change the code to where the die rolls automatically, so the qty of shots can accumulate and from there, that would then determine how many orcs the orb can fire (I’ll add an if-then statement in the _on_orb_area_shape_entered method later). I have the code for the dice (as a separate scene) ready, but the timer doesn’t seem to start (which is required to make the “rolling” aesthetic). Code below:

extends Control

@onready var die_face: TextureRect = $Die
@onready var roll_timer: Timer = $Roll_Timer
@export var die_texture_array: Array[AtlasTexture]

var index_chooser: int
var shoot_auth: bool = false

var index_max_face: int

var material_multiplier: int = 1

var num_of_shots: Array = [
1, 2,
1, 2, 3,
1, 2, 3, 4,
1, 2, 3, 4, 5,
1, 2, 3, 4, 5, 6
]

Called when the node enters the scene tree for the first time.

func _ready():
die_face.set_texture(die_texture_array[Globals.index_min_face])

func _process(_delta: float):
   #Timer doesn’t start
   if Globals.shoot_counter == 0:
      roll_timer.start()
   if roll_timer.time_left > 0:
      print(roll_timer.time_left)
      index_chooser = randi_range(Globals.index_min_face, index_max_face)
die_face.set_texture(die_texture_array[index_chooser])
      match index_chooser:
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19: material_multiplier = 1
         20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39: material_multiplier = 5
         40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59: material_multiplier = 10
   if roll_timer.time_left <= 0:
      Globals.shoot_counter += (num_of_shots[index_chooser] * material_multiplier)

Because of the “print” statement, I was able to determine the timer doesn’t start. Even with the “Autostart” enabled. The Globals.shoot_counter is assigned a value of 0, so that if statement should’ve been triggered.

It’s probably restarting the timer over and over. You should add an extra condition like such. Otherwise your timer is getting reset on the next tick.

var timer_started: bool = false

func _process(delta: float) -> void:
    if not timer_started and some_condition_is_met():
        $Timer.start()
        timer_started = true

And of course make sure you set timer_started back to false when done.

Thank you! That worked.