Double jumps are interfering with coyote time

Godot Version

4.3

Question

i have followed a tutorial on a movement system in godot and changed it to suit my needs and also added some other things, one of those things are coyote time. when i have double jumps disabled it works just fine but when double jumps are active, instead of getting a free coyote jump when you’re falling off a ledge, it uses your double jump. i tried everything from making canDjump=false whenever you can coyote jump but i got nothing, all my attemps either resulted in infinite double jumps or one jump taking the player to the moon and inverting their gravity.

here is my code, i’ve attempted to make it readable…lol
(there’s also this thing where the dash curve doesnt do much…would be a nice bonus if someone got it but eh, im fine with how it is right now)

extends CharacterBody2D

#unlocks
@export var Wall_jumpU:bool=false
@export var DashU:bool=false
@export var GroundPoundU:bool=false
@export var DoubleJumpsU:bool=false

#speed stuff
@export var speed = 200
@export_range(0,1) var acceleration = 0.1
@export_range(0,1) var deceleration = 0.1

#jump stuff
@export var jump_force = -400.0
@export_range(0,1) var decelerate_on_jump_release = 0.5
@export var jump_buffer_time = 0.1

#double jump stuff
@export var Double_jump_force= -300
@export var Djumps= 1

#coyote time stuff
@export_range(0,1) var coyote_time=0.15
@onready var coyote_timer: Timer = $"Coyote Timer"

#wall jump stuff
@export var wall_jump_H_force = 300
@export var wall_jump_V_force = -500

#dash stuff
@export var dash_speed = 1000
@export var dash_distance = 100
@export var dash_curve: Curve
@export var dash_cooldown = 1.0

#ground pound stuff
@export var Gpound_speed = 1000.0


var is_touching_left_wall:bool=false
var is_touching_right_wall:bool=false

var is_dashing=false
var dash_start_position=0
var dash_direction=0
var dash_timer=0

var timesJumped=0
var CanDjump:bool=true
var JumpAvailable:bool=true
var jump_buffer:bool=false
var canCjump=false

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		if JumpAvailable:
			if coyote_timer.is_stopped():
				canCjump=true
				coyote_timer.start(coyote_time)
			else:
				velocity-= get_gravity() * delta
		velocity += get_gravity() * delta
	
	# Handle jump and Coyote Time.
	if Input.is_action_just_pressed("jump"):
		if JumpAvailable:
			Jump()
		else:
			jump_buffer=true
			get_tree().create_timer(jump_buffer_time).timeout.connect(_on_jump_buffer_timeout)
	
	if not is_on_floor() and canCjump:
		if Input.is_action_just_pressed("jump"):
			Jump()
	
	# Double Jump handling
	if Input.is_action_just_pressed("jump") and not is_on_floor() and DoubleJumpsU==true and CanDjump==true and not is_on_wall():
		velocity.y = Double_jump_force
		timesJumped+=1
		if timesJumped==Djumps:
			CanDjump=false
	
	if is_on_floor():
		timesJumped=0
		JumpAvailable=true
		coyote_timer.stop()
		CanDjump=true
		if jump_buffer:
			Jump()
			jump_buffer=false
		
	if is_on_wall() and Wall_jumpU==true:
		 # Check which side
		var wall_normal = get_wall_normal()
		is_touching_left_wall = is_on_wall() and wall_normal.x > 0
		is_touching_right_wall = is_on_wall() and wall_normal.x < 0
	else:
		is_touching_left_wall = false
		is_touching_right_wall = false
	if Input.is_action_just_pressed("jump"):
		if is_touching_left_wall:
			velocity.x = wall_jump_H_force
			velocity.y = wall_jump_V_force
		elif is_touching_right_wall:
			velocity.x = -wall_jump_H_force
			velocity.y = wall_jump_V_force
	
	
	if Input.is_action_just_released("jump") and velocity.y < 0:
		velocity.y *= decelerate_on_jump_release
		

	# Get the input direction and handle the movement/deceleration.
	var direction := Input.get_axis("left", "right")
	if direction:
		velocity.x = move_toward(velocity.x, direction * speed, speed * acceleration)
	else:
		velocity.x = move_toward(velocity.x, 0, speed * deceleration)
		
	if direction and (is_on_floor() or is_on_wall()):
		$Sprite2D/WalkParticles.emitting=true
	else:
		$Sprite2D/WalkParticles.emitting=false
		
	#dash activation
	if Input.is_action_just_pressed("dash") and direction and not is_dashing and dash_timer <=0 and DashU:
		is_dashing = true
		dash_start_position = position.x
		dash_direction = direction
		dash_timer = dash_cooldown
	
	#actual dash	
	if is_dashing==true:
		var current_distance= abs(position.x - dash_start_position)
		if current_distance >= dash_distance or is_on_wall():
			is_dashing=false
		else:
			velocity.x = dash_direction * dash_speed * dash_curve.sample(current_distance / dash_distance)
			velocity.y = 0
			
	#dash timer management
	if dash_timer > 0:
		dash_timer-=delta
		
	move_and_slide()
	
	#Gpound
	if (Input.is_action_just_pressed("down") or Input.is_action_just_pressed("Gpound")) and not is_on_floor():
		velocity.y=Gpound_speed 
		
func Coyote_timeout():
	canCjump=false
	JumpAvailable=false

	
func Jump()->void:
	velocity.y = jump_force
	JumpAvailable=false
	

func _on_jump_buffer_timeout() ->void:
	jump_buffer=false

(sorry to whoever mustered up the patience to read all of that…)

So, I tried to streamline/simplify your implementation in your _physics_process(), I personally like to use external func so cut some of the bog of running through _physics_process for debugging and such. However, you can change that back if you want, and the structure will have still been cleaned up a bit. You had a lot of if and and and checks in your code. I tried to figure out what were the ones with the least checks that others depended on not being true. That is why I created a hierarchy → Wall Jump → Regular Jump → Coyote Jump → Double Jump. So, if any of the previous ones are true your character should play them and not play any of the others.

I eliminated the multiple checks for is_on_floor, as well as the scattered checks for input (“jump”). The reality is that as long as you set your jump parameters to be true only when not on floor, you shouldn’t have to check if is_on_floor() each time.

Setting canCjump to true should != is_on_floor(). Same with CanDjump, that is why I changed it so that it is set true when you can jump(), also I set it to false when you are on the floor.

One side note, this is currently allowing for a double jump after canCjump if canCjump is false, and not on floor yet.

Edit:
Also, do you want your coyote time jump to work where you get a regular jump, and then another regular jump? If not, if you only want your coyote time jump then your canCjump var is redundant. However, if you want the two jumps it is fine as is.

extends CharacterBody2D

#unlocks
@export var Wall_jumpU:bool = false
@export var DashU:bool = false
@export var GroundPoundU:bool = false
@export var DoubleJumpsU:bool = false

#speed stuff
@export var speed = 200
@export_range(0,1) var acceleration = 0.1
@export_range(0,1) var deceleration = 0.1

#jump stuff
@export var jump_force = -400.0
@export_range(0,1) var decelerate_on_jump_release = 0.5
@export var jump_buffer_time = 0.1

#double jump stuff
@export var Double_jump_force = -300
@export var Djumps = 1

#coyote time stuff
@export_range(0,1) var coyote_time = 0.15
@onready var coyote_timer: Timer = $"Coyote Timer"

#wall jump stuff
@export var wall_jump_H_force = 300
@export var wall_jump_V_force = -500

#dash stuff
@export var dash_speed = 1000
@export var dash_distance = 100
@export var dash_curve: Curve
@export var dash_cooldown = 1.0

#ground pound stuff
@export var Gpound_speed = 1000.0

var is_touching_left_wall := false
var is_touching_right_wall := false

var is_dashing := false
var dash_start_position := 0
var dash_direction := 0
var dash_timer := 0

var timesJumped := 0
var CanDjump := false
var JumpAvailable := true
var jump_buffer := false
var canCjump := false

func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity += get_gravity() * delta
		#If coyote_time running, sets velocity else sets canCjump
		if is_coyote_time():
			velocity -= get_gravity() * delta
	else:
		#Resets jump once is_on_floor
		reset_jump()

	if Input.is_action_just_pressed("jump"):
		if Wall_jumpU and is_on_wall():
			#Sets wall_jump params for velocity, also prevents other jumps from being called if is_on_wall
			velocity = wall_jump_param()
		elif JumpAvailable:
			jump()
		elif canCjump:
			jump()
		elif CanDjump:
			velocity.y = Double_jump_force
			timesJumped += 1
			if timesJumped == Djumps:
				CanDjump = false
		else:
			jump_buffer = true
			get_tree().create_timer(jump_buffer_time).timeout.connect(_on_jump_buffer_timeout)

	if Input.is_action_just_released("jump") and velocity.y < 0:
		velocity.y *= decelerate_on_jump_release

	# Get the input direction and handle the movement/deceleration.
	var direction := Input.get_axis("left", "right")
	if direction:
		velocity.x = move_toward(velocity.x, direction * speed, speed * acceleration)
	else:
		velocity.x = move_toward(velocity.x, 0, speed * deceleration)
		
	if direction and (is_on_floor() or is_on_wall()):
		$Sprite2D/WalkParticles.emitting = true
	else:
		$Sprite2D/WalkParticles.emitting = false
		
	#dash activation
	if Input.is_action_just_pressed("dash") and direction and not is_dashing and dash_timer <= 0 and DashU:
		is_dashing = true
		dash_start_position = position.x
		dash_direction = direction
		dash_timer = dash_cooldown
	
	#actual dash	
	if is_dashing == true:
		var current_distance = abs(position.x - dash_start_position)
		if current_distance >= dash_distance or is_on_wall():
			is_dashing = false
		else:
			velocity.x = dash_direction * dash_speed * dash_curve.sample(current_distance / dash_distance)
			velocity.y = 0
			
	#dash timer management
	if dash_timer > 0:
		dash_timer -= delta
		
	move_and_slide()
	
	#Gpound
	if (Input.is_action_just_pressed("down") or Input.is_action_just_pressed("Gpound")) and not is_on_floor():
		velocity.y = Gpound_speed 

func wall_jump_param() -> Vector2:
	# Check which side
	var wall_normal = get_wall_normal()
	is_touching_left_wall = is_on_wall() and wall_normal.x > 0
	is_touching_right_wall = is_on_wall() and wall_normal.x < 0
	var vel_x : int
	var vel_y : int
	if is_touching_left_wall:
		vel_x = wall_jump_H_force
		vel_y = wall_jump_V_force
	elif is_touching_right_wall:
		vel_x = -wall_jump_H_force
		vel_y = wall_jump_V_force
	return Vector2(vel_x,vel_y)

func is_coyote_time() -> bool:
	if JumpAvailable:
		if coyote_timer.is_stopped():
			canCjump = true
			coyote_timer.start(coyote_time)
			return false
		else:
			return true
	return false

func coyote_timeout():
	canCjump = false
	JumpAvailable = false

func jump()->void:
	velocity.y = jump_force
	JumpAvailable = false
	if DoubleJumpsU:
		CanDjump = true

func reset_jump()->void:
	timesJumped = 0
	JumpAvailable = true
	coyote_timer.stop()
	CanDjump = false
	if jump_buffer:
		jump()
		jump_buffer = false

func _on_jump_buffer_timeout() ->void:
	jump_buffer = false

I wasn’t able to test it since there would be a lot of other setup needed to replicate your various things, but maybe this could be a start to streamline your code.

Also, never used the Curve, but I think looking at it briefly you need to set up points, so the starting point the character, and the end point of your dash (I think that is how it works). Otherwise how is it to sample and calculate the x coordinate that would exist along the curve, if you haven’t established any curve parameters. I could be wrong on this

Also, this is a problematic piece of your code. You check for the “jump” input 3 times, with out any mutually exclusive checks.

if not is_on_floor(), if jumpavailable, if canCjump, if not is_on_wall, if CanDjump, if DoubleJumpU then all three of these actions will be called in sequence within a single frame.

	if Input.is_action_just_pressed("jump"):
		if JumpAvailable:
			Jump()
		else:
			jump_buffer=true
			get_tree().create_timer(jump_buffer_time).timeout.connect(_on_jump_buffer_timeout)
	
	if not is_on_floor() and canCjump:
		if Input.is_action_just_pressed("jump"):
			Jump()
	
	# Double Jump handling
	if Input.is_action_just_pressed("jump") and not is_on_floor() and DoubleJumpsU==true and CanDjump==true and not is_on_wall():
		velocity.y = Double_jump_force
		timesJumped+=1
		if timesJumped==Djumps:
			CanDjump=false

**Also, depending on how you want your coyote time to work you could tweak jump() like this **

if DoubleJumpsU and not canCjump:
		CanDjump = true
	velocity.y = jump_force
	if JumpAvailable:
		JumpAvailable = false
	elif canCjump:
		canCjump = false

This should activate double jump only when regular jump is called when canCjump is not active (this would prohibit you from using double jump after coyote jump though). This would also allow two jumps when coyote time is active. One for the regular jump, and the second for the canCjump, afterwards deactivating Cjump.

1 Like