Not sure why the shot count goes to negative

Godot Version

4.8.dev5

Question

I wrote my code to where dice are rolled that the resulting value adds that number of shots that the orb can shoot at incoming mobs. However, that count keeps going negative for some reason. What I am trying to achieve is for the orb to constantly “scan” for mobs within its range and fire shots only if there are shots available. The dice rolls to add to the shot count automatically.

The screenshot below shows how my nodes are arraigned:

Below is the code of the Line2D node:

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)
	shooting_mob()
		
func _on_orb_area_shape_entered(_area_rid: RID, area: Area2D, _area_shape_index: int, _local_shape_index: int):
	var mob = area.get_parent()
	if mob.has_method("when_attacked"):
		Globals.mobs_in_range.append(mob)
	
func shooting_mob():
	for mob in Globals.mobs_in_range:
		if Globals.shoot_counter > 0:
			var targeted_mob = Globals.mobs_in_range[0]
			add_point(Vector2(0,0))
			add_point(to_local(targeted_mob.position))
			await get_tree().create_timer(shot_duration).timeout
			clear_points()
			await get_tree().create_timer(shot_delay).timeout
			Globals.shoot_counter -= 1
			print(str(Globals.shoot_counter) + " shots left")
			targeted_mob.when_attacked()
			Globals.mobs_in_range.erase(targeted_mob)
			shot_sound.play()

The dice are made in a separate scene. Below is the screenshot of how I have my dice scene’s nodes arraigned:

Below is the code for the hovering dice scene:

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
]

var timer_started: bool = false

# 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])
	match Globals.index_min_face:
		#Index of max faces of bone dice
		0: index_max_face = 1
		2: index_max_face = 4
		5: index_max_face = 8
		9: index_max_face = 13
		14: index_max_face = 19
		#Index of max faces of gold dice
		20: index_max_face = 21
		22: index_max_face = 24
		25: index_max_face = 28
		29: index_max_face = 33
		34: index_max_face = 39
		#Index of max faces of diamond dice
		40: index_max_face = 41
		42: index_max_face = 44
		45: index_max_face = 48
		49: index_max_face = 53
		54: index_max_face = 59

func _process(_delta: float):
	if timer_started == false and Globals.shoot_counter < 1:
		roll_timer.start()
		timer_started = true
	if roll_timer.time_left > 0:
		index_chooser = randi_range(Globals.index_min_face, index_max_face)
		die_face.set_texture(die_texture_array[index_chooser])

func _on_roll_timer_timeout():
	if index_chooser >= 0 and index_chooser <= 19:
		material_multiplier = 1
	if index_chooser >= 20 and index_chooser <= 39:
		material_multiplier = 5
	if index_chooser >= 40 and index_chooser <= 59:
		material_multiplier = 10
	Globals.shoot_counter += (num_of_shots[index_chooser] * material_multiplier)
	print(str((num_of_shots[index_chooser] * material_multiplier)) + " shots added")
	timer_started = false

Below shows the output. It would explain why the orb isn’t able to shoot at the mobs, but I have no idea why the number of shots go into the negative.

It’s probably because you are awaiting the shot_duration and shot_delay timers in the loop.

So the for each loop is running and checking shoot_counter is > 0 for each mob in range before you reduce the shot counter.

You probably want to remove the await statements, I’m not sure how you want this code to flow but awaits are clearly adding confusion. Using await as part of _process is always a bad idea and your shooting_mob is called in _process.

You probably don’t expect the Globals.shoot_counter to go negative because of the if > 0: but using await means another call to this function happens and is queued with the same await, these get backed up and eventually time out finally subtracting from the counter, but other coroutines are waiting too and they have already passed the greater than zero check.

Understood. Removing those lines DID fix the issue but I had those lines originally because I wanted the shots to show as rays of light against each mob and for a time delay to happen between shots. How can I achieve that if “await” isn’t an option?

Sounds more like you want tweens if you are preparing a visual effect. Typically you can connect to the desired signal, not sure what you’d want in this case. I’d bet you’re code would be much better without await and without get_tree().create_timer(...) in general.

I’d assume this portion is better as a Timer node with it’s timeout connected to clear_points(), there’s no reason this should delay the entire function just to clear points after a half second

add_point(Vector2(0,0))
add_point(to_local(targeted_mob.position))
$ClearTimer.start()

Await inside of _process is an odd way to do delays between shots, do you want a Timer node and it’s timeout signal instead?

Understood. I added a timer just for the shots duration. The result is below:

It seems to be making lines continuously, instead of a single shot. The timeout method is not being triggered for some reason. Updated code below:

var targeted_mob

func shooting_mob():
for mob in Globals.mobs_in_range:
if Globals.shoot_counter > 0:
targeted_mob = Globals.mobs_in_range[0]
add_point(Vector2(0,0))
add_point(to_local(targeted_mob.position))
shot_timer.start()

func _on_shot_timer_timeout():
clear_points()
Globals.shoot_counter -= 1
print(str(Globals.shoot_counter) + " shots left")
targeted_mob.when_attacked()
Globals.mobs_in_range.erase(targeted_mob)
shot_sound.play()

Nodes have been updated, shown below:

If you are still calling shooting_mob every frame under _process then it’s going to restart the timer every frame too, shot_timer.start() restarts the timer so it may never timeout. I’d recommend also connecting shooting_mob to a timer, instead of looping through every enemy just target the first one found.

Ah, gotcha. I added another timer, shown below:

Updated code 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 balance: Label = $"../../../Main_CanvasLayer/VBoxContainer/HBoxContainer/Balance"

@onready var shot_timer: Timer = $"../Shot_timer"
@onready var scanning_timer: Timer = $"../Scanning_timer"

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):
	var mob = area.get_parent()
	if mob.has_method("when_attacked"):
		Globals.mobs_in_range.append(mob)

var targeted_mob

func shooting_mob():
	if Globals.shoot_counter > 0:
		targeted_mob = Globals.mobs_in_range[0]
		add_point(Vector2(0,0))
		add_point(to_local(targeted_mob.position))
		shot_timer.start()

#Ends each shot from the orb
func _on_shot_timer_timeout():
	clear_points()
	Globals.shoot_counter -= 1
	print(str(Globals.shoot_counter) + " shots left")
	targeted_mob.when_attacked()
	shot_sound.play()
	Globals.mobs_in_range.erase(targeted_mob)

#Constantly 'looks' for enemies within range
func _on_scanning_timer_timeout():
	if Globals.mobs_in_range.is_empty() == false:
		shooting_mob()

However, the game crashes with the following error:
“E 0:00:23:342 _on_shot_timer_timeout: Attempt to call function ‘when_attacked’ in base ‘previously freed’ on a null instance.
line_2d.gd:37 @ _on_shot_timer_timeout()
line_2d.gd:37 @ _on_shot_timer_timeout()”

This has also been the output:

Maybe pop_front() would be better than mobs_in_range[0] and attempting to delete it later, it seems like your mobs queue_free() as part of .when_attacked()?

func shooting_mob():
	if Globals.shoot_counter > 0:
		targeted_mob = Globals.mobs_in_range.pop_front()
		add_point(Vector2(0,0))
		add_point(to_local(targeted_mob.position))
		shot_timer.start()

Make sure your timers are set to “One Shot” if you don’t intend for them to repeat over and over after starting

Understood. Below is the updated code:

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”

@onready var shot_timer: Timer = $“../Shot_timer”
@onready var scanning_timer: Timer = $“../Scanning_timer”

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):
var mob = area.get_parent()
if mob.has_method(“when_attacked”):
Globals.mobs_in_range.append(mob)

var targeted_mob

func shooting_mob():
if Globals.shoot_counter > 0:
targeted_mob = Globals.mobs_in_range.pop_front()
add_point(Vector2(0,0))
add_point(to_local(targeted_mob.position))
shot_timer.start()

#Ends each shot from the orb
func _on_shot_timer_timeout():
clear_points()
Globals.shoot_counter -= 1
print(str(Globals.shoot_counter) + " shots left")
targeted_mob.when_attacked() #<--- Error happens here
shot_sound.play()

#Constantly ‘looks’ for enemies within range
func _on_scanning_timer_timeout():
if Globals.mobs_in_range.is_empty() == false:
shooting_mob()

I get the following error:
E 0:00:19:557 _on_shot_timer_timeout: Attempt to call function ‘when_attacked’ in base ‘previously freed’ on a null instance.
line_2d.gd:37 @ _on_shot_timer_timeout()
line_2d.gd:37 @ _on_shot_timer_timeout()

The mob scene has the following code:

extends CharacterBody2D

@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D

var SPEED: int
const DIRECTION = -1
var hit_points: int = 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():
hit_points -= 1
if hit_points < 1:
SPEED = 0
$AnimatedSprite2D.play(“death”)
Globals.balance += 1
await $AnimatedSprite2D.animation_finished
call_deferred(“queue_free”)

These are the nodes of the mob scene:

For what ever reason it appears _on_shot_timer_timeout is called again before targeted_mob has time to update. Maybe the timer’s wait_time is allows this. If you want to ignore the error you could set targeted_mob = null after using the attacked function and check if targeted_mob == null: before using the attacked function

I just figured it out. The “shot_timer” one_shot property was not enabled. The timer was repeating itself, which would explain why it tried to call a method of a mob that was freed.

Thanks for your help!

Thank you for your help!