Jump caused by jump_buffer_timer is not a variable jump

Godot Version

Godot 4.6 Beta-2 (32-bit)

Question

Jump caused by jump_buffer_timer is not a variable jump.

Hello! I hope that you are doing well!

I am pretty new to Godot and game dev, so correct my silly mistakes (if any, of course).

I have created a 2D platformer from a video of brackeys (How to make a game), I added a nice amount of things, two of them were jump buffering and variable jump.

As the question says, whenever I jump normally, when on the floor, it’s a variable jump, but when the jump is triggered by the jump buffer, it’s a full jump, which I don’t want.

player.gd (I think only the physics_process function is important):

extends CharacterBody2D
class_name Player
signal health_changed
signal hit
signal died

const SPEED: float = 130.0
const JUMP_VELOCITY: float = -300.0
const JUMP_BUFFER_TIME: float = 0.12
const MAX_HEALTH: int = 3
@export var health: int = 3:
	set(desired_value):
		health = clamp(desired_value, 0, MAX_HEALTH)
		health_changed.emit()
var direction: float
var jump_available: bool = true
var knockback: Vector2 = Vector2.ZERO # Direction and speed of knockback.
var knockback_timer: float = 0.0
var jump_buffer_timer: float = 0.0
#region @onready var
@onready var state_machine: Node = $StateMachine
@onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D
@onready var death_timer: Timer = $Timers/DeathTimer
@onready var hit_timer: Timer = $Timers/HitTimer
@onready var _animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var _jump_sound: AudioStreamPlayer = $Sounds/JumpSound
@onready var _hurt_sound: AudioStreamPlayer = $Sounds/HurtSound
@onready var _coyote_timer: Timer = $Timers/CoyoteTimer
#endregion

func _ready() -> void: # Testing purposes ONLY.
	Engine.time_scale = 0.5

func _physics_process(delta: float) -> void:
#region if dead or hit
	# Avoid running movement when player is dead.
	if state_machine.current_state is PlayerDeath:
		if not is_on_floor():
			velocity += get_gravity() * delta

		move_and_slide()
		return

	elif state_machine.current_state is PlayerHit:
		if not is_on_floor():
			velocity += get_gravity() * delta

		if knockback_timer > 0.0: # Turn on knockback and tick down timer.
			velocity = knockback
			knockback_timer -= delta
			if knockback_timer <= 0.0: # Turn off knock back.
				knockback = Vector2.ZERO

		if jump_buffer_timer > 0.0:
			jump_buffer_timer = 0.0

		move_and_slide()
		return
#endregion

	# Implement jump_buffer.
	if jump_buffer_timer > 0.0:
		jump_buffer_timer -= delta # Reduce by delta if _jump_buffer_timer > 0.0.

	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta
		if _coyote_timer.is_stopped():
			_coyote_timer.start()
	else: # Do when on floor.
		if not jump_available:
			_coyote_timer.stop()
			jump_available = true

	# Handle jump.
	if jump_available and jump_buffer_timer > 0.0:
		print("Jump pressed!")
		velocity.y = JUMP_VELOCITY

	elif velocity.y < 0.0 and Input.is_action_just_released("jump"):
		print("Jump released!")
		velocity.y *= 0.4

	# Find direction: -1, 0, 1
	direction = Input.get_axis("move_left", "move_right")

	# Flip the AnimatedSprite2D horizontally accordingly.
	if direction > 0:
		_animated_sprite_2d.flip_h = false
	elif direction < 0:
		_animated_sprite_2d.flip_h = true

	# Apply movement logic.
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

func _input(event: InputEvent) -> void:
	if event.is_action_pressed("jump"):
        if jump_buffer_timer > 0.0: return
		jump_buffer_timer = JUMP_BUFFER_TIME

func _on_coyote_timer_timeout() -> void:
	jump_available = false

func _on_death_timer_timeout() -> void:
	Engine.time_scale = 1.0
	get_tree().reload_current_scene()

func apply_knockback(knockback_direction: Vector2, force: float, knockback_duration: float) -> void:
	knockback = knockback_direction * force
	knockback_timer = knockback_duration

func hurt() -> void:
	health -= 1
	if health == 0:
		die()
		return
	hit.emit()

func die() -> void:
		died.emit()

func play_animation(animation_name: String) -> void:
	_animated_sprite_2d.play(animation_name.to_lower())

func play_sound(sound_name: String) -> void:
	match sound_name.to_lower():
		"jump":
			_jump_sound.play()
		"hurt":
			_hurt_sound.play()
		_:
			print("WARNING: Sound named '" + sound_name + "' not found.")

player_idle.gd:

extends State
class_name PlayerIdle
var player: Player

func physics_update(_delta: float) -> void:
	if player.jump_available and player.jump_buffer_timer > 0.0:
		Transitioned.emit(self, "Jump")
		return

	if player.is_on_floor():
		if player.direction == 0:
			player.play_animation("idle")
		else:
			player.play_animation("run")

player_jump.gd:

extends State
class_name PlayerJump
var player: Player

func enter() -> void:
	player.play_animation("jump")
	player.play_sound("jump")
	player.jump_available = false
    player.jump_buffer_time = 0.0

func physics_update(_delta: float) -> void:
	if player.is_on_floor():
		Transitioned.emit(self, "IdleNRun")

Thanks a lot for your precious time!

LL

Is a jump buffer the same as coyote time? Because it looks like it is, but then in your code I see a reference to a coyote timer.

In your input method you set jump_buffer_timer every time jump is pressed. I suspect you should only be setting that timer when there is not a jump already requested.

Try this:

func _input(event: InputEvent) -> void:
	if event.is_action_pressed("jump"):
		if jump_requested: return
		jump_requested = true
		_jump_buffer_timer = JUMP_BUFFER_TIME
1 Like

No, the jump buffer is an internal timer (in script), whereas the coyote timer is a node. (in editor)

I added the if statement, it didn’t fix anything, but a nice safeguard to have!

Can you explain then what the jump buffer is supposed to do?

It’s supposed to store the player’s jump for a short period of time in case the player is in the air (off the platform), and apply it when the player reaches the ground.

That question made me think a bit, and I tested around a little bit more, and found that if you hold the jump button until the player touches the ground AND the jump buffer is active, then it’s a variable jump.

This creates a new issue, when the player releases the jump button before touching the ground, it should be a short hop. I can’t think of a way to implement this, so help will be appreciated.

Also thanks for helping me clarify the issue!

1 Like

So basically when the player hits the jump button before landing, they should immediately jump again? Basically fudging the timing for the jump?

Put print statements in relevant parts of your code to see if they get executed as you expect them to.
Start with:

	# Handle jump.
	if jump_available and jump_requested and _jump_buffer_timer > 0.0:
		PRINT HERE
		velocity.y = JUMP_VELOCITY
		_jump_buffer_timer = 0.0

	elif velocity.y < 0.0:
		PRINT HERE
		if Input.is_action_just_released("jump"):
			PRINT HERE
			velocity.y *= 0.5

Printing is one of the most fundamental debugging tools. Any tutorial titled “How to make a game” that’s worth its salt should mention that.

3 Likes

Yeah, it’s supposed to be a small forgiving window for the player’s jump to be registered even though they are not on the ground, makes the experience better.

Because of those print statements, I noticed that elif velocity.y < 0.0 is running multiple times, so I changed that block of code into this-

	# Handle jump.
	if jump_available and jump_requested and _jump_buffer_timer > 0.0:
		print("Jump pressed!")
		velocity.y = JUMP_VELOCITY
		_jump_buffer_timer = 0.0

	elif velocity.y < 0.0 and Input.is_action_just_released("jump"):
		print("Jump released!")
		velocity.y *= 0.5

This is more efficient, and if I implement falling animation I can always go back, but this made debugging easier.

Here are the results-

  • When making a short non-jump-buffer-caused jump, it prints both statements once.

  • When releasing the jump when the full jump is completed and the player starts to go downward, only the first print statement appears once.

  • When trying to make a short jump-buffer-caused jump, only first print statement appears once.

  • When holding the jump button while the jump buffer is ticking and releasing it when the player jumps causes a variable jump, and prints both statements once.

After seeing these results, I tried to experiment around a bit, but nothing helped.

	elif velocity.y > 0.0 and Input.is_action_just_released("jump") and _jump_buffer_timer > 0.0:
		print("Jump tried to released!")

I tried this, and I think it’s producing the output I want, but I am not sure, and I don’t know how to implement this anyway.

Thanks!

1 Like

Then you need to print more. Not only where the program is going but also the values of relevant variables; in this case at least jump_available, jump_requested and _jump_buffer_timer, possibly some others. You can even make labels and put those values in there every frame so you can conveniently monitor them in real time and see how they change in various scenarios.

The idea is to extract as much information on your program’s behavior as you can, until you fully understand what exactly it is doing. Then compare that to what you want/expect and you’ve pretty much found your bug.

Be like a spider. Gradually expand your printing “web” until you fully grok what’s happening. Sometimes new mysteries will appear and you’ll have to widen the web even more. Other times you’ll get some partial insights that will let you narrow down and focus, and then finally zero-in and catch the bug.

1 Like

Try to read out loud your #Handle Jump region.

You have two lines which do something similar but have very different results. And one condition is frame-perfect.

And I know you say you have a variable jump, but you define const JUMP_VELOCITY. Maybe making it an actual variable would help out.

1 Like
Reading region helped fixed other bugs.

Reading that region again made me realize that jump_buffer_timer was doing the job of jump_requested, so I removed it, and I also fixed an animation bug in jump_state.gd, it was transitioning to idle state when it was not on floor, which was obviously wrong.

Now, I managed to fix this issue by changing:

if velocity.y < 0.0 and Input.is_action_just_released("jump"):
    velocity.y *= 0.4

to:

if velocity.y < 0.0 and not Input.is_action_pressed("jump"):
    velocity.y *= 0.4

I kind of hate to say it, but ChatGPT suggested this, I guess that AI has its place (of course their code quality is meh, so I try to avoid them, but it has it’s place for brainstorming. It was the one who suggested me to use an FSM.)

This issue is fixed now!

Thanks!

Your code snippet is still in the boilerplate category. LLMs can handle those because there is million examples of it floating around the web. Once your code grows beyond that or becomes specific, no “ai” will be able to debug it and neither will you if you avoid developing your debugging skills and instead depend on “ai” to do it.

2 Likes

Can you buffer a big jump now?

I didn’t exactly understand what you said, but the jumps caused by jump_buffer_timer are now variable jumps, which is the result I wanted.

You are correct, I had also noticed that I was getting spoiled by using these chatbots, so I have started to cutoff access to the internet in order to prevent myself from cheating, but because even after plenty of trying, I wasn’t able to (think of/find) a solution, so I ended up using AI.