Godot Version
Godot v4.6 Beta 3
Question
Hello! I hope that you are doing well.
When the player dies, they respawn, and die again. Only twice, not more. Using await twice for physics frame is a workaround to the the issue, but I have read that it’s not ideal and should be avoided wherever possible, especially twice.
My platformer game has a LevelManager who handles levels, a StateMachine, and Killzone for death and hits. There is a world boundary Killzone that triggers when the player falls below it.
My theory is that when the player dies, they fall into the world boundary killzone, then they transition to the idle state, but because they are still in world boundary killzone, it triggers and causes a second death when they are teleported. Means the killzone death check is insufficient.
I can possibly add a boolean to fix this, but I think that’s more of a workaround than a real fix.
Sorry if the code below is a bit too large, I have tried my best to make it concise. It’s in order of functions called, so it should be fairly easier to navigate.
killzone.gd:
extends Area2D
enum Function { HIT, DIE }
@export var current_function: Function = Function.HIT
func _on_body_entered(player: Player) -> void:
# Don't run logic if player is dead or hit.
if player.state_machine.current_state in [PlayerHit, PlayerDeath]: #and current_function == Function.HIT:
print("Skipping Killzone Logic.") # Never runs?
return
... # Knockback Logic.
if current_function == Function.HIT: # Reduce health by 1.
print("Hurt.")
player.hurt()
else: # Kill the player instantly.
print("Die.")
player.die()
player.gd (Relevant part):
func hurt() -> void:
health -= 1 # Uses a clamp of 0 to max_health( which is 3).
if health == 0:
die()
return
hit.emit()
func die() -> void:
died.emit()
state_machine.gd:
extends Node
@export var _initial_state: State
var current_state: State
var states: Dictionary = {}
@onready var player: Player = get_parent()
func _ready() -> void: # Add states and connect transition signals.
player.died.connect(on_player_died)
player.hit.connect(on_player_hit)
for child in get_children(): # Add states and connect transition signals.
if child is State:
states[child.name.to_lower()] = child
child.player = player
child.Transitioned.connect(on_child_transition)
if _initial_state: # Initialize initial state.
_initial_state.enter()
current_state = _initial_state
func _process(delta: float) -> void: # current_state's _process.
if current_state:
current_state.update(delta)
func _physics_process(delta: float) -> void: # current_state's _physics_process.
if current_state:
current_state.physics_update(delta)
func _transition_state_to(new_state_name: String) -> void:
var new_state = states.get(new_state_name.to_lower())
if not new_state:
return
if current_state:
current_state.exit()
new_state.enter()
current_state = new_state
func on_child_transition(state: State, new_state_name: String) -> void:
# Return if current state doesn't match the state that called the function.
if state != current_state:
return
_transition_state_to(new_state_name)
func on_player_hit() -> void:
_transition_state_to("Hit")
func _on_hit_timer_timeout() -> void:
_transition_state_to("IdleNRun")
func on_player_died() -> void:
_transition_state_to("Death")
func _on_death_timer_timeout() -> void:
EventBus.death_timer_timeout.emit()
player_death.gd:
extends State
class_name PlayerDeath
var player: Player
func enter() -> void:
print("Death: ENTER.")
Engine.time_scale = 0.5
player.play_animation("death")
player.play_sound("hurt")
player.collision_shape_2d.set_deferred("disabled", true)
player.death_timer.start()
func exit() -> void:
print("Death: EXIT.")
Engine.time_scale = 1.0
player.collision_shape_2d.set_deferred("disabled", false)
level_manager.gd:
extends Node
class_name LevelManager
var _current_level: Level
var _level_counter: int = 1
@onready var player: Player = $Player
func _ready() -> void:
EventBus.level_changed.connect(_on_level_changed)
EventBus.death_timer_timeout.connect(_on_death_timer_timeout)
_load_level()
func _on_level_changed() -> void:
_level_counter += 1
_load_level()
func _load_level() -> void:
if _current_level:
_current_level.queue_free()
# Add Level as a child of LevelManager.
var _level_source: PackedScene = load("res://scenes/levels/level_" + str(_level_counter) + ".tscn")
_current_level = _level_source.instantiate()
call_deferred("add_child", _current_level)
_current_level.call_deferred("set_spawn_point", player) # Set's a Marker2D's global position to player's global position.
func _on_death_timer_timeout() -> void:
_load_level()
#await get_tree().physics_frame # Uncommenting these two worksaround the issue.
#await get_tree().physics_frame
player.health = player.max_health
player.state_machine.call_deferred("_transition_state_to", "IdleNRun")
player_idle.gd:
extends State
class_name PlayerIdle
var player: Player
func enter() -> void:
print("Idle.")
func physics_update(_delta: float) -> void:
... # Some Jump Handling
... # Idle and Run animation handling.
Output:
Idle.
Die.
Death: ENTER.
Death: EXIT.
Idle.
Die.
Death: ENTER.
Death: EXIT.
Idle.
Thanks in advance!
LL