Coyote Time with CharacterBody2D

Godot Version

v4.2.1.stable.official

Question

Hi, probably a repeatedly asked question:
How can I implement Coyote Time with CollisionPolygon2D and CharacterBody2D?
I’m new to Godot and GDScript, sorry.

Here’s my (very messy) code:

extends CharacterBody2D

var speed = 300.0
var jump_speed = -350.0
var dir = "right"

@onready var _animated_sprite = $AnimatedSprite2D



var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")



func _physics_process(delta):
	# Add the gravity.
	velocity.y += gravity * delta

	# Handle Jump.
	if Input.is_action_just_pressed("up") and is_on_floor():
		velocity.y = jump_speed
	
	
	# Get the input direction.
	var direction = Input.get_axis("left", "right")
	velocity.x = direction * speed

	move_and_slide()
	
	if not(Input.is_action_pressed("anyControl")):
		if is_on_floor():
			_animated_sprite.play("idleCycle")
			
	if not(Input.is_action_just_pressed("anyControl")):
		if dir == "right":
			_animated_sprite.scale.x = 0.3
		else:
			_animated_sprite.scale.x = -0.3	
	if Input.is_action_pressed("left"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "left"
	if Input.is_action_pressed("right"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "right"
	if Input.is_action_just_pressed("up"):
		_animated_sprite.stop()
		
	if Input.is_action_pressed("quit"):
		get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
		get_tree().quit(1)

If anyone thinks my code is extremely bad, please tell me, as I’m not that good at keeping it good & formatted!

Hazel

1 Like

First let’s make a var called something like can_jump

var can_jump = true

Add this to the top of your code somewhere near your other variables.

Let’s make a new function so the code doesn’t get so confusing called jump(). We’re going to move some of your code under # Handle Jump here. We’re going to also change is_on_floor() with can_jump. We’re doing this because when you’re in “Coyote Time” you’re not on the ground so that won’t help us here.

func _physics_process(delta):
...
    if Input.is_action_just_pressed("up") and can_jump:
        jump()
...

func jump():
    velocity.y = jump_speed
    can_jump = false

Setting can_jump to false will prevent your players from being able to jump all the time in the air, but now when you land can_jump will still be false so let’s fix that by adding another check in your _physics_process

func _physics_process(delta):
...
if can_jump == false and is_on_floor():
        can_jump = true
...

Okay so now when your players land back on the ground they can jump again. Now we can implement Coyote Time.

Add a Timer node to your scene and change it’s wait time to something pretty small like 0.3 seconds. We also want to check the One Shot box so it doesn’t automatically start again. You can rename it to something like “CoyoteTimer” if you’d like.
Connect the timeout() node to your script. If you named it CoyoteTimer it will default to making a function called _on_coyote_timer_timeout().

We’re going to make it so you can no longer jump once this time runs out like so:

func _on_coyote_timer_timeout():
    can_jump = false

Now we just need to trigger this timer. So let’s add another check in our _physics_process. We only want this to activate when the following conditions are met:

  1. We’re not on the ground
  2. We can still jump (as in, we didn’t jump to get off the ground)
  3. The timer isn’t running

So let’s add another check to know if we need to start the timer.

func _physics_process(delta):
...
    if (is_on_floor() == false) and can_jump and $CoyoteTimer.is_stopped():
        $CoyoteTimer.start()
...

I know this was kind of a lot, so feel free to ask if anything is confusing.

5 Likes

Woah… That’s alot! Thanks! I will go and add this later, busy right now.

This has worked, however it’s given me a double jump now!!
Have I just implemented the code wrong?

extends CharacterBody2D

var speed = 300.0
var jump_speed = -350.0
var dir = "right"
var can_jump = true
@onready var _animated_sprite = $AnimatedSprite2D



var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")


func jump():
	velocity.y = jump_speed
	can_jump = false

func _on_coyote_timer_timeout():
	can_jump = false

func _physics_process(delta):
	# Add the gravity.
	velocity.y += gravity * delta

	# Handle Jump.
	if Input.is_action_just_pressed("up") and can_jump:
		jump()
		
	if can_jump == false and is_on_floor():
		can_jump = true
		
	if (is_on_floor() == false) and can_jump and $CoyoteTimer.is_stopped():
		$CoyoteTimer.start()
	
	# Get the input direction.
	var direction = Input.get_axis("left", "right")
	velocity.x = direction * speed

	move_and_slide()
	
	if not(Input.is_action_pressed("anyControl")):
		if is_on_floor():
			_animated_sprite.play("idleCycle")
			
	if not(Input.is_action_just_pressed("anyControl")):
		if dir == "right":
			_animated_sprite.scale.x = 0.3
		else:
			_animated_sprite.scale.x = -0.3	
	if Input.is_action_pressed("left"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "left"
	if Input.is_action_pressed("right"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "right"
	if Input.is_action_just_pressed("up"):
		_animated_sprite.stop()
		
	if Input.is_action_pressed("quit"):
		get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
		get_tree().quit(1)


1 Like

Since I wrote all that code without having Godot open I did miss something haha.

We currently have

if can_jump == false and is_on_floor():
		can_jump = true

AFTER all of the times when can_jump would be set to false. So when your character jumps during that same frame they are still on the floor and since we are checking if they are on the floor AFTER they jump it gets reset back to being able to jump.

Simply move that part of the code to the top of your func _physics_process(delta): and it should fix it.

extends CharacterBody2D

var speed = 300.0
var jump_speed = -350.0
var dir = "right"
var can_jump = true
@onready var _animated_sprite = $AnimatedSprite2D



var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")


func jump():
	velocity.y = jump_speed
	can_jump = false

func _on_coyote_timer_timeout():
	can_jump = false

func _physics_process(delta):
	if can_jump == false and is_on_floor():
		can_jump = true

	# Add the gravity.
	velocity.y += gravity * delta

	# Handle Jump.
	if Input.is_action_just_pressed("up") and can_jump:
		jump()
		
	if (is_on_floor() == false) and can_jump and $CoyoteTimer.is_stopped():
		$CoyoteTimer.start()
	
	# Get the input direction.
	var direction = Input.get_axis("left", "right")
	velocity.x = direction * speed

	move_and_slide()
	
	if not(Input.is_action_pressed("anyControl")):
		if is_on_floor():
			_animated_sprite.play("idleCycle")
			
	if not(Input.is_action_just_pressed("anyControl")):
		if dir == "right":
			_animated_sprite.scale.x = 0.3
		else:
			_animated_sprite.scale.x = -0.3	
	if Input.is_action_pressed("left"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "left"
	if Input.is_action_pressed("right"):
		_animated_sprite.stop()
		_animated_sprite.play("walk")
		dir = "right"
	if Input.is_action_just_pressed("up"):
		_animated_sprite.stop()
		
	if Input.is_action_pressed("quit"):
		get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
		get_tree().quit(1)