Area2d stays hit after player dies in a certain way

Godot Version

4.7 (latest steam version)

Issue:

This is a pretty niche issue i haven’t been able to figure out, I have a custom timer system that allows a timer of set time to be started and then if it hasn’t ended by the time it runs out the player dies. this is used in-game as like a race thing where the player has to get to the end before the time runs out.

When the timer is active and the player dies, for some reason the signal related to the hitbox doesn’t kill the player after touching anything that should kill the player even though the timer doesn’t touch the signal, basically if you die with the timer active you become invincible.

if anyone could help then that would be like really appreciated, this is my first game that i started making and i haven’t worked on it for months and i’m coming back to finish it so i’m still getting re-used to the project if that makes sense

Relevant code:

timer system script:

extends CanvasLayer

@onready var timer_display: Label = $TimerDisplay
@onready var timer: Timer = $Timer
@onready var animation_player: AnimationPlayer = $AnimationPlayer

@export var timer_active:bool = false

var player:Player

var animation_check:bool

func _ready() -> void:
	if get_tree().get_first_node_in_group("player") and get_tree().get_first_node_in_group("player") is Player:
		player = get_tree().get_first_node_in_group("player")
	
	timer_display.text = ""
	animation_player.play("enter", -1, -10, true)

func _process(_delta: float) -> void:
	if timer_active and timer.time_left > 0:
		if animation_check == false:
			animation_check = true
			animation_player.play("enter", -1, 1.5)
		timer_display.text = str(timer.time_left)
		if player and player.dying:
			end_timer()
		if Input.is_action_just_pressed("Back"):
			end_timer()
	else:
		if animation_check:
			animation_check = false
			if player and player.dying == false:
				timer_display.label_settings.font_color = Color(0.0, 0.573, 0.0, 1.0)
			
			animation_player.play_backwards("enter")
			await animation_player.animation_finished
			timer_display.label_settings.font_color = Color(1.0, 0.945, 0.91)
			timer_display.text = ""

func start_timer(time):
	if timer_active == true: return
	timer_active = true
	if timer.time_left == 0:
		timer.start(time)

func end_timer():
	timer_active = false
	timer.stop()

func _on_timer_timeout() -> void:
	if timer_active and player:
		print("out of time")
		player.die()
	end_timer()

script for the level objects that trigger and stop the timer:

extends Area2D

@export var time:float = 30.0
@export var finish:bool = false

@onready var sprite: Sprite2D = $Sprite2D

func _ready() -> void:
	if finish == false:
		sprite.region_rect = Rect2(0,0,8,32)
	else:
		sprite.region_rect = Rect2(8,0,8,32)

func _on_body_entered(body: Node2D) -> void:
	if not body is Player: return
	TimerController.player = body
	if finish == false:
		TimerController.start_timer(time)
	else:
		TimerController.end_timer()

part of the player script that handles death and the signal from the hurtbox:

# handle death
func _on_hurtbox_body_entered(_body: Node2D) -> void:
	print("dying maybe")
	die()

func die():
	if dying:
		print("cant die")
		return
	dying = true
	print("die")
	WalkFx.emitting = false #particles
	ColorFx.emitting = false
	set_physics_process(false)
	AudioController.play_die()
	Camera.add_trauma(0.2) #screenshake
	add_effect(ExplosionScene, true)
	Sprite.visible = false
	velocity = Vector2.ZERO
	await get_tree().create_timer(0.4).timeout
	get_tree().call_group("reset", "reset")
	await get_tree().create_timer(0.1).timeout	
	position = Spawn.position
	color = 0
	Sprite.visible = true
	set_physics_process(true)
	can_change_color = true
	dying = false

Get rid of awaits. Use regular signal handlers instead.

What does animation_check track? I think if it were handled some other way or in some other place, the flow would be clearer.

when the timer starts it plays an animation to slide in the screen and same but opposite when it ends, the animation check is for if the animation has been played

where though? the ones in the death function or the part where it waits for the timer animation to finish? i already use a signal function for the main actual timer node timeout

Best to not have any awaits.

damn, why? its convenient

also i doubt removing every use of await will fix the bug

Or so it seems. Until you start getting bugs you cannot wrap your head around.

Maybe, but how can you know that if you can’t even locate the bug?

Besides, you have a nasty case of coupling where player and timer mess with each other’s state. The timer should be subordinate to the player. It doesn’t have any business altering the player state directly. Let the player script control the timer and let the timer send relevant signals to the player script. The player script should then decide what to do when it receives a signal.

If animation_check is just trying to track whether you think an animation is playing, you should ask the animation player rather than trying to externally track it (IMO)

if animation_player.is_playing() and animation_player.current_animation == "enter":
    ...

I also think it has something to do with the awaits. Either get rid of them or if for some reason you want to keep them, try to manually stop the timer and emit the timeout signal from the player scene die()-function, if dying when the timer is active. You’d have to do it somewhere at the beginning of the script, before returning if dying. If it even works. Tbh this looks like a big mess to me and i would recommend rewriting it entirely as it will continue causing issues whenever you least expect it when you try to add something new or change something existing.

Biggest red flag is this in the timer _process:
await animation_player.animation_finished

Don’t await inside _process. That function runs every frame, so you stack overlapping coroutines when the timer ends. That can easily mess with die() / dying and leave you “invincible.”

Move the exit animation to a normal function (or animation_finished signal) instead:

func end_timer() -> void:
	timer_active = false
	timer.stop()
	_play_exit_anim()
func _play_exit_anim() -> void:
	if player and not player.dying:
		timer_display.label_settings.font_color = Color(0.0, 0.573, 0.0, 1.0)
	animation_player.play_backwards("enter")

Same idea on the player. The await timers inside die() are convenient, but if anything interrupts that function, dying can stay true and every later hit just prints "cant die".

Also, I’d stop the timer from calling player.die() / reading player.dying directly; emit a signal like time_up and let the player handle death. Less tangled.

Thanks for the long explanation! I will try this tomorrow because currently its late

Honestly i would do re-write but its an old project im just trying to get it done and this is the last thing i need to add for it to be complete, its also the first game i started making in godot so it will be a bit scuffed

i followed your suggestion and removed the awaits, now my code is this:

extends CanvasLayer

@onready var timer_display: Label = $TimerDisplay
@onready var timer: Timer = $Timer
@onready var animation_player: AnimationPlayer = $AnimationPlayer

@export var timer_active:bool = false

var player:Player
var animation_check:bool

signal timer_done

func _ready() -> void:
	if get_tree().get_first_node_in_group("player") and get_tree().get_first_node_in_group("player") is Player:
		player = get_tree().get_first_node_in_group("player")
	
	timer_display.text = ""
	animation_player.play("enter", -1, -10, true)

func _process(_delta: float) -> void:
	if timer_active and timer.time_left > 0:
		if animation_check == false:
			animation_check = true
			animation_player.play("enter", -1, 1.5)
		timer_display.text = str(timer.time_left)
		if player and player.dying:
			end_timer()
		if Input.is_action_just_pressed("Back"):
			end_timer()

func start_timer(time):
	if timer_active == true: return
	timer_active = true
	if timer.time_left == 0:
		timer.start(time)

func end_timer():
	timer_active = false
	timer.stop()
	_play_exit_anim()

func _on_timer_timeout() -> void:
	if timer_active and player:
		print("out of time")
		timer_done.emit()
	end_timer()

func _play_exit_anim() -> void:
	if player and not player.dying:
		timer_display.label_settings.font_color = Color(0.0, 0.573, 0.0, 1.0)
	animation_check = false
	animation_player.play_backwards("enter")

func _on_animation_player_animation_finished(_anim_name: StringName) -> void:
	if animation_check == true: return
	timer_display.text = ""
	timer_display.label_settings.font_color = Color(1.0, 0.945, 0.91)

and moved the death due to time up to the player script. connecting the timer_done signal to the die function on ready, death still works after the glitch but for some reason the player hurtbox won’t fire the signal when it touches something

i have checked the actual hitboxes and everything seems fine:

(the red hitbox is the players hurtbox, and as you can see it is over the spikes (which should be killing the player))

i have also checked if the signal itself is being disconnected but it is not

How did you determine that? Put a print statement into the signal handler to see if it’s getting called. If it does get called - it means that the signal fires but your handling logic is bad. If it doesn’t get called then your collision or signal connection is not properly set up.

i have put a print in the signal function:

# handle death
func _on_hurtbox_body_entered(_body: Node2D) -> void:
	print("dying maybe")
	die()

func die():
	if dying:
		print("cant die")
		return
	dying = true
	print("die")
	WalkFx.emitting = false #particles
	ColorFx.emitting = false
	set_physics_process(false)
	AudioController.play_die()
	Camera.add_trauma(0.2) #screenshake
	add_effect(ExplosionScene, true)
	Sprite.visible = false
	velocity = Vector2.ZERO
	await get_tree().create_timer(0.4).timeout
	get_tree().call_group("reset", "reset")
	await get_tree().create_timer(0.1).timeout	
	position = Spawn.position
	color = 0
	Sprite.visible = true
	set_physics_process(true)
	can_change_color = true
	dying = false

Does it print? If it does not then the problem is with your setup. Make sure you’ve properly set collision layers/masks and that the signal is actually connected.

Btw you still have those awaits in die(). Several people told you so far that awaits are the likely culprit. If you get weird bugs - awaits are the most probable origin. If you don’t think they can be the source of the bug, you probably don’t have the full understand of how they actually work. Await bugs are sneaky and they always misleadingly appear in some “other” place that seems to be unrelated to awaits. Just get rid of them, at least until you deal with the bug, to make sure they don’t contribute to any weird behavior.

i tried getting rid of the awaits but idk how else to make it wait and if there is no wait time on death way more buggy stuff happens.

When the bug where the player can’t die happens there is no prints in the output

Try to determine why. Start by making sure that the signal is actually connected to the function.

Use a signal handler.
Instead of:

# code part 1
await get_tree().create_timer(...).timeout
# code part 2

Do:

func die():
	# code part 1
	get_tree().create_timer(...).timeout.connect(die_part_2)

func die_part_2(): 
	# code part 2

ok i will try that, i also said in an earlier reply that the signal is still connected to the function