Coyote Time e Jump Buffer no Godot 4

Aprenda a implementar coyote time godot e jump buffer no CharacterBody2D para um pulo justo, com Timer, is_on_floor e valores em segundos sensatos.
Se voce ja jogou um plataforma e teve a sensacao de que o pulo "comeu" o seu comando, provavelmente faltava coyote time godot ou jump buffer no jogo. Esses dois truques resolvem dois problemas distintos de timing entre o que o jogador aperta e o que o personagem realmente faz. Eles nao deixam o jogo mais facil de forma trapaceira, apenas alinham o controle com a intencao da pessoa que esta segurando o controle. Neste post voce vai ver por que o pulo parece injusto sem isso e como implementar as duas tecnicas em um CharacterBody2D no Godot 4.
Coyote Time e Jump Buffer no Godot 4
Antes do codigo, vale entender o que esta acontecendo na cabeca do jogador e o que acontece na fisica do jogo. Os dois raramente concordam, e essa diferenca de milissegundos e o que separa um controle que responde bem de um que parece travado.
Por que o pulo parece injusto sem isso
Imagine duas situacoes comuns em qualquer plataforma.
Na primeira, o jogador corre ate a borda de uma plataforma e aperta pulo uma fracao de segundo depois de os pes saírem do chao. Para ele, o personagem ainda estava em cima da plataforma. Para o jogo, is_on_floor() ja retornou false naquele frame, entao o comando de pulo e ignorado. O personagem cai. O jogador jura que apertou no tempo certo, e ele apertou, do ponto de vista humano.
Na segunda situacao, o jogador esta caindo em direcao a uma plataforma e aperta pulo um pouquinho antes de tocar o chao. No frame em que o botao foi pressionado, is_on_floor() ainda era false, entao nada acontece. Quando o personagem finalmente toca o chao, o jogador ja soltou o botao. Resultado: o pulo nao sai, e parece que o jogo perdeu o comando.
As duas falhas vem da mesma causa. A entrada do jogador e continua e baseada em intencao, mas a verificacao do chao acontece em frames discretos. Coyote time resolve o primeiro caso, dando uma pequena janela de tolerancia depois de sair da plataforma. Jump buffer resolve o segundo, lembrando do comando de pulo por um instante antes de o personagem poder pular.
Vale lembrar que tudo isso depende de movimento estavel. Se o seu personagem se move de forma inconsistente entre frames, ajuste isso primeiro lendo sobre delta time e movimento por frame, senao essas janelas vao parecer aleatorias.
A base: um CharacterBody2D com pulo simples
Comece com um pulo padrao, sem nenhuma tolerancia. Esse e o ponto de partida que vamos melhorar.
extends CharacterBody2D
@export var velocidade := 300.0
@export var forca_pulo := -600.0
@export var gravidade := 1600.0
func _physics_process(delta: float) -> void:
# aplica gravidade quando esta no ar
if not is_on_floor():
velocity.y += gravidade * delta
# pulo direto, sem tolerancia nenhuma
if Input.is_action_just_pressed("pular") and is_on_floor():
velocity.y = forca_pulo
# movimento horizontal
var direcao := Input.get_axis("esquerda", "direita")
velocity.x = direcao * velocidade
move_and_slide()
Esse codigo funciona, mas sofre exatamente dos dois problemas descritos acima. O pulo so acontece se o botao for pressionado no mesmo frame em que is_on_floor() for true. Vamos abrir essa janela nos dois sentidos.
Coyote time com Timer
A ideia do coyote time e simples: depois que o personagem deixa o chao, ele ainda pode pular por uma fracao de segundo. O nome vem do personagem de desenho que corre para fora do penhasco e fica suspenso no ar por um instante antes de cair.
A forma mais limpa no Godot 4 e usar um Timer como node filho. Adicione um Timer chamado CoyoteTimer, marque One Shot como ativo e deixe o Wait Time em algo entre 0.1 e 0.15 segundos. Esse intervalo e curto o bastante para ser imperceptivel como "trapaca", mas longo o bastante para cobrir o erro humano.
extends CharacterBody2D
@export var velocidade := 300.0
@export var forca_pulo := -600.0
@export var gravidade := 1600.0
@onready var coyote_timer: Timer = $CoyoteTimer
# guarda se o personagem estava no chao no frame anterior
var estava_no_chao := false
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravidade * delta
# detecta o momento exato de sair do chao
if estava_no_chao and not is_on_floor():
coyote_timer.start()
var pode_pular := is_on_floor() or not coyote_timer.is_stopped()
if Input.is_action_just_pressed("pular") and pode_pular:
velocity.y = forca_pulo
coyote_timer.stop() # evita pulo duplo dentro da janela
var direcao := Input.get_axis("esquerda", "direita")
velocity.x = direcao * velocidade
estava_no_chao = is_on_floor()
move_and_slide()
O ponto central e a variavel pode_pular. Ela e verdadeira quando o personagem esta no chao, mas tambem quando o coyote_timer ainda esta rodando. O metodo is_stopped() retorna false enquanto o timer conta, entao not coyote_timer.is_stopped() significa "ainda estou dentro da janela de tolerancia". Assim que o jogador pula usando a janela, chamamos stop() para que ele nao consiga reaproveitar o mesmo coyote time.
Repare tambem na deteccao da borda de descida. So iniciamos o timer no frame exato em que o personagem passa de no chao para no ar, comparando estava_no_chao com is_on_floor(). Sem isso, o timer reiniciaria toda vez que o personagem estivesse no ar, o que nao faria sentido.
Jump buffer: lembrar do comando antes da hora
O jump buffer ataca o outro lado do problema. Quando o jogador aperta pulo no ar, pouco antes de aterrissar, guardamos esse comando por um curto periodo. Se o personagem tocar o chao dentro dessa janela, o pulo sai automaticamente.
Da para implementar com outro Timer, mas aqui prefiro um contador manual em segundos para mostrar a alternativa sem node. Adicione um valor que conta o tempo restante do buffer e o reduz a cada frame.
@export var jump_buffer_tempo := 0.12
var jump_buffer_contador := 0.0
func _physics_process(delta: float) -> void:
# registra a intencao de pular assim que o botao e pressionado
if Input.is_action_just_pressed("pular"):
jump_buffer_contador = jump_buffer_tempo
# o buffer escorre com o tempo
jump_buffer_contador -= delta
var pode_pular := is_on_floor() or not coyote_timer.is_stopped()
if jump_buffer_contador > 0.0 and pode_pular:
velocity.y = forca_pulo
jump_buffer_contador = 0.0
coyote_timer.stop()
Aqui, jump_buffer_contador recebe o tempo total assim que o botao e pressionado, mesmo que o personagem esteja no ar. A cada frame ele perde delta. Enquanto for maior que zero e o personagem puder pular (no chao ou em coyote time), o pulo dispara. Usar delta no contador mantem o tempo consistente independentemente da taxa de frames, o que e importante para o buffer durar sempre os mesmos 0.12 segundos reais.
Juntando tudo num script so
Combinando as duas tecnicas, o script final fica enxuto e cobre os dois casos de injustica. Note que separei a verificacao do pulo numa unica condicao para deixar claro como coyote time e jump buffer se complementam.
extends CharacterBody2D
@export var velocidade := 300.0
@export var forca_pulo := -600.0
@export var gravidade := 1600.0
@export var jump_buffer_tempo := 0.12
@onready var coyote_timer: Timer = $CoyoteTimer
var estava_no_chao := false
var jump_buffer_contador := 0.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravidade * delta
# inicia coyote time ao deixar o chao
if estava_no_chao and not is_on_floor():
coyote_timer.start()
# registra o buffer de pulo
if Input.is_action_just_pressed("pular"):
jump_buffer_contador = jump_buffer_tempo
jump_buffer_contador -= delta
var pode_pular := is_on_floor() or not coyote_timer.is_stopped()
if jump_buffer_contador > 0.0 and pode_pular:
velocity.y = forca_pulo
jump_buffer_contador = 0.0
coyote_timer.stop()
var direcao := Input.get_axis("esquerda", "direita")
velocity.x = direcao * velocidade
estava_no_chao = is_on_floor()
move_and_slide()
A ordem das operacoes importa. Registramos o buffer antes de checar pode_pular, e atualizamos estava_no_chao no fim do frame, depois de toda a logica de pulo ter rodado. Trocar essa ordem pode criar pulos perdidos ou janelas que nunca abrem.
Valores sensatos em segundos
Os numeros fazem toda a diferenca na sensacao. Como ponto de partida, use estes intervalos:
- Coyote time: entre 0.1 e 0.15 segundos. Abaixo de 0.08 o jogador quase nao percebe; acima de 0.2 comeca a parecer que o personagem flutua e o pulo vira algo "magico".
- Jump buffer: entre 0.1 e 0.15 segundos. Valores maiores fazem o personagem pular sozinho de formas que confundem, como pular logo apos aterrissar quando o jogador nao queria.
Ajuste testando com o jogo rodando, nao no papel. A percepcao de justica e subjetiva e muda conforme a velocidade do personagem e o ritmo das plataformas. Um jogo rapido e nervoso tolera janelas menores; um jogo mais lento e exploratorio aguenta janelas um pouco maiores.
Por que usar segundos e nao frames
Voce vera muitos tutoriais antigos contando frames em vez de segundos, algo como "6 frames de coyote time". O problema e que a contagem em frames assume uma taxa fixa, normalmente 60 FPS. Se o jogo roda a 30 ou a 144, a janela muda de duracao real e a sensacao do pulo varia entre maquinas. Trabalhar em segundos com Timer ou com delta mantem o comportamento estavel em qualquer hardware, que e exatamente o motivo de o _physics_process receber delta.
Proximos passos para o pulo
Coyote time e jump buffer formam a base de um pulo que parece justo, mas eles convivem bem com outras tecnicas. Quando essa base estiver afiada, vale adicionar variacoes como pulo duplo e pulo na parede, que estao detalhadas no post sobre double jump e wall jump no Godot. A partir dai, controlar a altura do pulo conforme o tempo que o botao fica pressionado, e cortar a velocidade vertical ao soltar o botao, completa o conjunto.
O importante e tratar o pulo como uma negociacao entre a intencao do jogador e a fisica do jogo. As duas tecnicas deste post nao mudam quanto o personagem sobe ou cai. Elas apenas dao uma margem de tolerancia para o erro humano que existe em todo input. Implemente, teste com gente de verdade jogando e ajuste os valores ate o pulo deixar de "comer" comandos.


