Voltar para o Blog
Quest Log

Coyote Time e Jump Buffer no Godot 4

Personagem 2D pulando logo apos sair da borda de uma plataforma no Godot

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.

Ferramenta:Godot

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalĂ­cioGarantia de 7 dias

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.

Ver as janelas na tela enquanto testa

Ajustar coyote time e jump buffer no escuro e frustrante, porque a diferenca entre 0.1 e 0.15 segundos e pequena demais pra julgar de olhos fechados. Um jeito rapido de ganhar intuicao e jogar os dois contadores numa Label de depuracao e observar os numeros enquanto voce corre e pula.

Adicione uma Label como node filho e atualize o texto a cada frame, lendo o tempo restante direto do Timer e do contador manual:

@onready var debug_label: Label = $DebugLabel

func _atualizar_debug() -> void:
    debug_label.text = "coyote: %.3f\nbuffer: %.3f\nno chao: %s" % [
        coyote_timer.time_left,
        max(jump_buffer_contador, 0.0),
        str(is_on_floor()),
    ]

Chame _atualizar_debug() no fim do _physics_process, depois de toda a logica de pulo. Com os numeros na tela voce ve o coyote esvaziar no instante em que sai da borda e o buffer encher quando aperta pulo cedo demais. Ai os valores deixam de ser abstratos: voce ajusta ate o pulo parecer certo pra voce, nao pra mim. Terminou o tuning, e so esconder a Label.

Coyote time nao e pulo duplo

Um deslize comum e deixar a janela de coyote aberta depois que o jogador ja pulou de proposito. Sem cuidado, o fluxo vira este: o personagem esta no chao, o jogador pula, nos frames seguintes is_on_floor() fica false mas a janela de tolerancia ainda conta como "pode pular", e um segundo toque dispara um pulo no ar de graca. E por isso que o coyote_timer.stop() logo apos o pulo nao e um detalhe, e a linha que fecha essa brecha. A regra pratica: todo caminho que gasta um pulo (no chao, na borda ou pelo buffer) tem que zerar as duas janelas no mesmo frame. Se um dia voce quiser pulo duplo de verdade, ele entra como um contador separado de saltos, nunca como efeito colateral da tolerancia.

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. Outra mecanica de gancho 2D que combina com esse pulo justo e o grappling hook no Godot, que estende o alcance vertical sem quebrar a sensacao de controle. 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. Esse conjunto completo, com gravidade variavel e corte de pulo ja montados num unico controlador, esta no guia de como programar o pulo de plataforma.

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.