Infinite jump when implementing Coyote Time in Finite State Machine

Godot Version

Godot 4.4.1-stable

Question

Hi everyone. I’ve been working on my first platformer and run into an issue where attempting to implement Coyote Time in a Finite State Machine, based on this one, results in an infinite jump while in the air.

Here’s the code, which I currently have in the Jump_state script

extends State
#region Variables
@export 
var idle_state: State
@export 
var run_state: State
@export 
var fall_state: State
@export 
var attack_state: State
@export
var jump_velocity: int = -400
@onready var coyote_timer: Timer = %CoyoteTimer
var coyote_jump: bool = false
#endregion

#region Functions
func enter() -> void:
	super()
	parent.velocity.y = jump_velocity
	
	

func process_physics(delta: float) -> State:
	if !parent.is_on_floor():
		parent.velocity.y += gravity * delta
		if coyote_timer.is_stopped():
			coyote_timer.start()
		else:
			coyote_jump = true
			coyote_timer.stop()
	
	if parent.velocity.y > 0:
		return fall_state
			
	if Input.is_action_just_pressed("Jump") && coyote_jump:
			parent.velocity.y = jump_velocity
			coyote_jump = false
			print("Coyote Jump check")
		
	var movement:= Input.get_axis("Left", "Right") * speed
	
	if movement != 0:
		parent.animations.flip_h = movement < 0
	parent.velocity.x = movement
	var was_on_floor := parent.is_on_floor()
	parent.move_and_slide()
	
	if was_on_floor && !parent.is_on_floor():
		coyote_jump = true
	
	if parent.is_on_floor():
		if movement != 0:
			return run_state
		return fall_state
	
	return null

func _on_coyote_timer_timeout() -> void:
	print("Timer works")
	coyote_jump = false
#endregion

Picture a player that is infinitely falling. On frame 1, the player is in the air. Therefore, the first check (if !parent.is_on_floor()) succeeds. If the coyote timer has stopped, it is started.

On frame 2, the player is still falling. The coyote_jump variable is set to true and the timer is stopped. The player can now jump in the air once. Let’s say that the player does.

Frame 3, the player is still in the air. The timer is stopped, so it is once again started.

Frame 4. Player is in the air. Timer is running, so coyote_jump is set to true and the timer is stopped. The player can now jump in the air again.

You should probably want to keep track of the number of times the player is allowed to jump while in the air, and prevent a jump from being processed if the player tries to jump more often than allowed. The number of total jumps in the air can be reset to zero whenever the player makes contact with the ground.

1 Like

Thank you for the response!

I’ve managed to implement a crude solution based on your answer. Is there any input on how I could improve this? Thank you again

Fall state:

extends State
#region Variables
@export 
var run_state: State
@export
var idle_state: State
@onready
var jump_velocity: int = -420
var jump_count: int = 0
var airtime: float = 0.0
var coyote_time_active: bool = true
#endregion

#region Functions

func process_physics(delta: float) -> State:
	airtime += delta
	if !parent.is_on_floor():
		parent.velocity.y += gravity * delta
		if %CoyoteTimer.is_stopped():
			%CoyoteTimer.start()
	else:
		coyote_time_active = true
		%CoyoteTimer.stop()
	

	var movement := Input.get_axis('Left', 'Right') * speed
	if movement !=0:
		parent.animations.flip_h = movement < 0
	
	parent.velocity.x = movement
	parent.move_and_slide()
	
	if Input.is_action_just_pressed("Jump") and coyote_time_active:
		parent.velocity.y = jump_velocity
	
	if parent.is_on_floor():
		airtime = 0.0
		jump_count = 0
		if movement != 0:
			return run_state
		return idle_state
	return null
	
func _on_coyote_timer_timeout() -> void:
	coyote_time_active = false
#endregion

Jump state

extends State
#region Variables
@export 
var idle_state: State
@export 
var run_state: State
@export 
var fall_state: State
@export 
var attack_state: State
@export
var jump_velocity: int = -450
var jump_count: int = 0
var airtime: float = 0.0
#endregion

#region Functions
func enter() -> void:
	super()
	parent.velocity.y = jump_velocity

func process_physics(delta: float) -> State:
	airtime += delta
	if !parent.is_on_floor():
		parent.velocity.y += gravity * delta
		
	if Input.is_action_just_pressed("Jump") and jump_count < 1 and airtime < 0.1:
			parent.velocity.y = jump_velocity
		
	var movement:= Input.get_axis("Left", "Right") * speed
	
	if movement != 0:
		parent.animations.flip_h = movement < 0
	parent.velocity.x = movement
	parent.move_and_slide()
	

	if parent.is_on_floor():
		airtime = 0.0
		jump_count = 0
		if movement != 0:
			return run_state
		return fall_state
	
	return null
#endregion

My main concern right now is if I need to have jump count logic in two states, and which state should get the coyote time logic.