How to start a timer when the player RELEASES a button?

Godot Version

4.2

Question

So, I have to make a timer to add a cooldown after the character has ended it’s jump action, but it would be better to start the timer when the character has finished it’s jump instead of at the start, because I have made a system that adds gravity to the character during the jump, depending on wether the player is still pressing the jump button or is attacking, changing the duration of the jump.

I made this script but it does not work. I am out of ideas, to me the logic makes sense but during the game, when i spam the jump button, the character jumps every time and the timer does not do what it should, when I print the state of the boolean that I called “jump_cooldown” it goes crazy, sometimes it works but sometimes it doesn’t

I configured it so “jump_cooldown” has to be FALSE for the jump to occur, so when the character is not on_floor, the condition becomes TRUE, and when when the character lands on the floor, the timer starts, and after 1 second it becomes FALSE again. But as I said, it does unexpected things…

why doesn’t it work??

	move_and_slide()
	if Input . is_action_just_pressed ("jump"):
		jump_pressed = true
		get_tree() . create_timer (jump_buffer_time) .timeout . connect (jump_pressed_timeout)
	if is_on_floor():
		if Input . is_action_pressed ("jump") and jump_pressed and not jump_cooldown and not do_attack:
			jump()
		if jump_cooldown:
			get_tree() . create_timer (jump_cooldown) .timeout . connect (jump_cooldown_timeout)
	else:
		do_jump = false
		jump_cooldown = true
func jump() -> void:
	do_jump = true
	jump_pressed = false
	jump_cooldown = true
func jump_pressed_timeout() -> void:
	do_jump = false
func jump_cooldown_timeout() -> void:
	jump_cooldown = false

Thanks for reading :slight_smile:

First of all, a formatting tip: On most forums where people post code, it’s possible to get the code formatted with a monospace font and syntax highlighting. The most common syntax for this is to either indent all of the lines of code with 4 extra spaces, or to surround the code with triple backticks (```). On this forum, there’s also a button that does it:
image
You can highlight your code and click that button, and it’ll automatically format your code, and preserve the indentation.

Secondly: There’s a function, Input.is_action_just_released, that tells you whether an action has just been released since last frame. That might be useful.

Finally, looking at your code, I can’t tell if jump_cooldown is meant to be a number or a boolean. In the line:

get_tree() . create_timer (jump_cooldown) .timeout . connect (jump_cooldown_timeout)

you’re using it as a number, but then in the jump and jump_cooldown_timeout functions, you set it to true and false respectively.

2 Likes

That is AWESOME, thanks so much for the tips, i will try these things now and update! :heart_eyes:

In fact I was trying to use a boolean as a number.

get_tree() . create_timer (jump_cooldown) .timeout . connect (jump_cooldown_timeout)

should have been:

get_tree() . create_timer (jump_cooldown_TIME) .timeout . connect (jump_cooldown_timeout)

they’re different variables :sweat_smile:

Ok, so looking at your code a bit more… I’m guessing that you’re leaving out some lines? Because I don’t see anything here that would actually make the player move upwards when pressing jump.

As for the logic of whether or not the player should be allowed to jump:

  • You gotta decide whether jump_cooldown is a boolean or a float - it can’t be both (well, technically I guess Godot will allow you to change it back and forth, but this will make your code much harder to work with, and I think it’s a big part of why it’s not working). Maybe you need an extra variable to hold the cooldown time.
  • I don’t think you’re actually checking whether the jump cooldown is active before calling the jump function.
  • Is do_jump ever used for anything?
1 Like

yes, I removed a lot of lines, I will include the whole code here in case it is important:



# VALUES:


# WALK:
@export var direction: Vector3 = script_global . get_axis()
@export var in_walk_speed: float = .678
@export var in_walk_acceleration: float = .15

# JUMP:
var gravity = ProjectSettings . get_setting ("physics/3d/default_gravity")
@export var jump_velocity: float = .45
@export var in_jump_acceleration: float = .03
@export var in_jump_speed: float = .6
@export var jump_buffer_time: float = .3
@export var jump_pressed: bool = false
@export var do_jump: bool = false


@export var jump_cooldown: bool = false
@export var jump_cooldown_time: float = 2.0


# ATTACK:
@export var attack_time: float = .15
@export var attack_pressed: bool = false
@export var do_attack: bool = false

# STATE MACHINE:
@export var random_number: int = script_global . randomnumber1()
@onready var animation_tree: AnimationTree = $AnimationTree





## PHYSICS PROCESS:


func _physics_process (_delta: float) -> void:
	update_animation_parameters()
	move_and_slide()

# JUMP LOGIC:
	if Input . is_action_just_pressed ("jump"):
		jump_pressed = true
		get_tree() . create_timer (jump_buffer_time) .timeout . connect (jump_pressed_timeout)

	elif Input . is_action_just_released ("jump"):
		get_tree() . create_timer (jump_cooldown_time) .timeout . connect (jump_cooldown_timeout)

	if is_on_floor():
		if Input . is_action_pressed ("jump") and jump_pressed and not jump_cooldown and not do_attack:
			jump()

	else:
		do_jump = false
		jump_cooldown = true
		print (jump_cooldown)






# ATTACK LOGIC:
	if Input . is_action_just_pressed ("attack"):
		attack_pressed = true
	elif not do_attack and attack_pressed and not do_jump:
		attack()

# DIRECTIONAL:
	direction = script_global . get_axis()

# JUMP PHYSICS:
	if not is_on_floor():
		velocity . x = move_toward (velocity . x , in_jump_speed * direction . x , in_jump_acceleration)
		velocity . z = move_toward (velocity . z , in_jump_speed * -direction . z , in_jump_acceleration)
		if Input . is_action_pressed ("jump"):
			if not do_attack:
				velocity . y -= (gravity * .3) * _delta
			else:
				velocity . y -= (gravity * .7) * _delta
		else:
			velocity . y -= (gravity * 1) * _delta
	else:
		if do_jump:
			velocity . y += jump_velocity

# ATTACK PHYSICS:
		elif do_attack:
			velocity = Vector3 . ZERO

# WALK PHYSICS:
		else:
			velocity . x = move_toward (velocity . x , in_walk_speed * direction . x , in_walk_acceleration)
			velocity . z = move_toward (velocity . z , in_walk_speed * -direction . z , in_walk_acceleration)

# ATTACK FUNC:
func attack() -> void:
	random_number = script_global . randomnumber1()
	do_attack = true
	attack_pressed = false
	get_tree() . create_timer (attack_time) .timeout . connect (attack_timeout)
func attack_timeout() -> void:
	do_attack = false

# JUMP FUNC:
func jump() -> void:
	do_jump = true
	jump_pressed = false
func jump_pressed_timeout() -> void:
	do_jump = false
func jump_cooldown_timeout() -> void:
	jump_cooldown = false
	print (jump_cooldown)





## ANIMATIONS:


#func _ready() -> void:
#	animation_tree . active = true
func update_animation_parameters() -> void:

# SPRITES "LOOK" LEFT OR RIGHT DEPENDING ON "X":
	if direction . x >0 and not do_attack:
		$Sprite3D_player . flip_h = true
	elif direction . x <0 and not do_attack:
		$Sprite3D_player . flip_h = false

# SPRITES "LOOK" UP OR DOWN DEPENDING ON "Z":
	if direction . z >0:
		animation_tree ["parameters/BS1D_walk/blend_position"] = 1
		animation_tree ["parameters/BS1D_jump/blend_position"] = 1
		animation_tree ["parameters/BS1D_idle/blend_position"] = 1
	elif direction . z <0 or direction . x != 0 or do_attack:
		animation_tree ["parameters/BS1D_walk/blend_position"] = 0
		animation_tree ["parameters/BS1D_jump/blend_position"] = 0
		animation_tree ["parameters/BS1D_idle/blend_position"] = 0

# JUMP:
	if not is_on_floor():
		animation_tree ["parameters/BS2D_attack/blend_position"] . y = 10
		animation_tree ["parameters/conditions/jump_condition"] = true
		animation_tree ["parameters/conditions/idle_condition"] = false
		animation_tree ["parameters/conditions/walk_condition"] = false
	else:
		animation_tree ["parameters/BS2D_attack/blend_position"] . y = 0
		animation_tree ["parameters/conditions/jump_condition"] = false

# IDLE / WALK:
		if (velocity == Vector3 . ZERO):
			animation_tree ["parameters/conditions/idle_condition"] = true
			animation_tree ["parameters/conditions/walk_condition"] = false
		else:
			animation_tree ["parameters/conditions/idle_condition"] = false
			animation_tree ["parameters/conditions/walk_condition"] = true

# ATTACK:
	if do_attack:
		animation_tree ["parameters/BS2D_attack/blend_position"] . x = random_number
		animation_tree ["parameters/conditions/attack_condition"] = true
		animation_tree ["parameters/conditions/idle_condition"] = false
		animation_tree ["parameters/conditions/walk_condition"] = false
		animation_tree ["parameters/conditions/jump_condition"] = false
	else:
		animation_tree ["parameters/conditions/attack_condition"] = false```

yes, It was but i edited it before posting this, just to test in case something else was broken but i forgot to include it back in there, i edited it now, it still doesn’t work, the last post is changed slightly but it still doesnt work, i am still looking into it myself too as we speak xD

what do_jump does is to stop you from jumping indefinitely if you hold the jump button, but i think the jump buffer TIMER does not really do anything anymore because i now made it so it will jump as long as you are holding the jump button and you haven’t already jumped once, i should change that too

You create lots of timers, but do you start any of them?
I don’t use them, but shouldn’t you create a timer e.g. in say ready(), then start the timer when doing your jump logic, rather than creating a timer each frame you want to use one?
Note that personally I think you’d be better off just having a variable and altering it by the delta rather than using timers.

1 Like

that sounds like a very good idea, i am going to investigate how to do that

create_timer makes a SceneTreeTimer, which automatically starts immediately, and is automatically dereferenced when it finishes, so that part is actually fine. Here’s the documentation.

1 Like

@tayacan
My bad - I presumed it just made a Timer, I did say I didn’t use them. :face_with_open_eyes_and_hand_over_mouth:

1 Like

In case this is read by someone in the distant future, I will keep updating because I think I am closer to solve it. I suspected that the clock was failing when i spammed the jump button because i was creating too many clocks, I still don’t really know if that is the case but i modified the code so the clock is only created if it is not already been created if that makes sense. I did that by adding “if not jump_cooldown:” as a condition for the creation of the new clock.

Now, it really works, but it feels a little inconsistent. I am still investigating what is going on. But it is really promising.

Super thanks to everyone who helped! (Especially for the is_action_just_released tip)

These are my first steps so feel free to give all the advice you want

now it looks like this:


	if Input . is_action_pressed ("jump") and jump_pressed and not jump_cooldown and not do_attack:
		jump()
	elif Input . is_action_just_pressed ("jump"):
		jump_pressed = true
	elif Input . is_action_just_released ("jump"):
		if not jump_cooldown:
			jump_cooldown = true
			get_tree() . create_timer (jump_cooldown_time) .timeout . connect (jump_cooldown_timeout)
	if not is_on_floor():
		jump_pressed = false
		do_jump = false```

I think you should switch to using a single Timer node, it will help prevent so many overlapping timers from triggering their connections. Or instead of tracking cooldown with a boolean + timer connections to use Time.get_ticks_msec() to track the last jump, something like this

var last_jump: int
const jump_cooldown_time = 2.0 * 1000 # 2 seconds coverted to milliseconds

func is_jump_ready() -> bool:
    return Time.get_ticks_msec() - last_jump > jump_cooldown_time

func did_jump() -> void:
    last_jump = Time.get_ticks_msec()
1 Like

That looks very good and I can understand it, I will give it a try tomorrow. I hope tomorrow is the last time i update this post hehe. Thanks :slight_smile:

The solution was to create only one timer at a time, because It seems like the overlapping timers don’t work reliably.

It wouldn’t have been possible without the is_action_just_released()

After a little bit of tweaking and simplification now works perfectly and I am really happy with how it feels

Thanks guys!!!

PD: I am going to include the final version:

var do_jump: bool = false
var jump_cooldown: bool = false
var jump_cooldown_time: float = .4

func _physics_process (_delta: float) -> void:
	move_and_slide()

# JUMP LOGIC:

	if Input . is_action_just_pressed ("jump"):
		jump_pressed = true
	elif Input . is_action_pressed ("jump") and jump_pressed and not do_attack:
		if not jump_cooldown:
			jump()
	elif Input . is_action_just_released ("jump"):
		if not jump_cooldown:
			jump_cooldown = true
			get_tree() . create_timer (jump_cooldown_time) .timeout . connect (jump_cooldown_timeout)
	elif not is_on_floor():
		do_jump = false

func jump() -> void:
	do_jump = true
	jump_pressed = false
func jump_cooldown_timeout() -> void:
	jump_cooldown = false