Double Jump e Wall Jump no Godot 4: CharacterBody2D Passo a Passo

Aprenda a implementar double jump godot e wall jump em um CharacterBody2D, com contador de pulos, deteccao de parede e empurrao lateral em GDScript.
Se voce ja tem um personagem que anda e pula no Godot, o proximo passo natural costuma ser dar mais liberdade de movimento. Adicionar um double jump godot e um wall jump muda bastante a sensacao do jogo, porque o jogador deixa de depender so do chao para se reposicionar no ar. Neste post a gente parte de um CharacterBody2D simples e vai montando essas duas mecanicas com calma, explicando o porque de cada decisao e nao so colando o codigo final.
Double Jump e Wall Jump no Godot 4: CharacterBody2D Passo a Passo
A ideia aqui e construir em cima do basico. Vou assumir que voce ja conseguiu fazer seu personagem andar para os lados e dar um pulo simples. Se ainda nao chegou nesse ponto, vale ler primeiro o tutorial de movimento de personagem em plataforma 2D, porque os dois pulos extras dependem de uma base de movimento que ja funcione bem.
O ponto de partida: um CharacterBody2D que ja pula
O CharacterBody2D e o no que o Godot 4 recomenda para personagens controlados pelo jogador. Ele nao reage a forcas como um RigidBody2D, voce e quem define a velocity a cada quadro e chama move_and_slide() para resolver as colisoes. Isso da controle total, que e exatamente o que a gente quer em plataforma.
Esse e o script base, com pulo unico, que vamos expandir.
extends CharacterBody2D
const SPEED: float = 200.0
const JUMP_VELOCITY: float = -400.0
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
var direction: float = Input.get_axis("move_left", "move_right")
if direction != 0.0:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
move_and_slide()
Repare em alguns detalhes. A gravidade vem do ProjectSettings, entao se voce mudar a gravidade global do projeto o personagem acompanha. A velocidade y negativa significa subir, porque no Godot o eixo y cresce para baixo. E move_and_slide() ja usa a propria velocity do no, voce nao precisa passar nada como argumento na versao 4.x.
Contador de pulos para o double jump godot
O segredo do double jump nao e detectar dois cliques, e contar quantos pulos ainda restam. A gente guarda um numero de pulos disponiveis e desconta um a cada pulo no ar. Quando o personagem toca o chao, esse contador volta ao maximo.
Por que contar em vez de usar um simples booleano "ja pulei uma vez"? Porque o contador escala. Se um dia voce quiser um triple jump ou um item que da um pulo extra, e so mudar o numero maximo. A logica continua a mesma.
extends CharacterBody2D
const SPEED: float = 200.0
const JUMP_VELOCITY: float = -400.0
const MAX_JUMPS: int = 2
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
var jumps_left: int = MAX_JUMPS
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
else:
jumps_left = MAX_JUMPS
if Input.is_action_just_pressed("jump") and jumps_left > 0:
velocity.y = JUMP_VELOCITY
jumps_left -= 1
var direction: float = Input.get_axis("move_left", "move_right")
if direction != 0.0:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
move_and_slide()
Tem um detalhe sutil aqui. Quando o personagem anda para fora de uma plataforma sem pular, ele ainda esta com jumps_left cheio. Ao apertar pulo no ar, ele gasta um pulo e ainda tem o segundo. Isso costuma ser aceitavel, mas se voce quer que andar para fora "consuma" o primeiro pulo, vale combinar essa logica com tecnicas de tolerancia. Falo mais disso no post sobre coyote time e jump buffer, que resolve justamente esses momentos em que o jogador sente que o pulo "comeu".
Detectando a parede com is_on_wall
Para o wall jump a gente precisa saber quando o personagem esta encostado numa parede. O CharacterBody2D ja oferece is_on_wall(), que retorna true quando a ultima chamada de move_and_slide() detectou colisao numa superficie mais vertical que horizontal.
Um cuidado: is_on_wall() so funciona bem se o personagem estiver de fato empurrando contra a parede e tiver velocidade horizontal naquela direcao. Se ele so cair colado sem input, dependendo da geometria a deteccao pode falhar. Por isso muita gente combina com a checagem de nao estar no chao, porque so faz sentido grudar na parede quando esta no ar.
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
else:
jumps_left = MAX_JUMPS
var on_wall: bool = is_on_wall() and not is_on_floor()
if on_wall:
velocity.y = min(velocity.y, WALL_SLIDE_SPEED)
# ... resto do processamento
Adicionei ali uma constante WALL_SLIDE_SPEED. A ideia e que, quando o personagem esta na parede, a queda fica mais lenta, como se ele escorregasse. Isso da uma janela para o jogador reagir e pular. Sem isso o wall jump fica quase impossivel de acertar, porque o personagem despenca rapido demais.
Empurrao lateral no wall jump
O pulo de parede tem duas componentes ao mesmo tempo. Tem a forca vertical, igual a um pulo normal, e tem um empurrao horizontal que joga o personagem para longe da parede. Sem esse empurrao lateral o jogador gruda de novo na mesma parede no quadro seguinte e nao consegue subir um corredor vertical alternando entre duas paredes.
Para saber em qual direcao empurrar a gente usa get_wall_normal(). A normal aponta para fora da parede, ou seja, na direcao oposta a superficie. Se a parede esta a direita do personagem, a normal aponta para a esquerda, que e exatamente para onde queremos lancar.
extends CharacterBody2D
const SPEED: float = 200.0
const JUMP_VELOCITY: float = -400.0
const MAX_JUMPS: int = 2
const WALL_SLIDE_SPEED: float = 60.0
const WALL_JUMP_PUSH: float = 280.0
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
var jumps_left: int = MAX_JUMPS
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
else:
jumps_left = MAX_JUMPS
var on_wall: bool = is_on_wall() and not is_on_floor()
if on_wall and velocity.y > 0.0:
velocity.y = min(velocity.y, WALL_SLIDE_SPEED)
if Input.is_action_just_pressed("jump"):
if on_wall:
var wall_normal: Vector2 = get_wall_normal()
velocity.y = JUMP_VELOCITY
velocity.x = wall_normal.x * WALL_JUMP_PUSH
jumps_left = MAX_JUMPS - 1
elif jumps_left > 0:
velocity.y = JUMP_VELOCITY
jumps_left -= 1
var direction: float = Input.get_axis("move_left", "move_right")
if direction != 0.0:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
move_and_slide()
Olhe a ordem das checagens dentro do is_action_just_pressed. Primeiro testamos a parede, depois o pulo normal. Isso importa: estando na parede, o pulo deve ser sempre o wall jump, com empurrao, mesmo que ainda tenha pulos no contador. Ao fazer o wall jump a gente reseta jumps_left para MAX_JUMPS - 1, ou seja, o jogador ainda tem o double jump disponivel depois de sair da parede. Isso deixa o controle generoso.
O conflito entre input e empurrao lateral
Tem um bug classico que aparece exatamente nesse ponto e que confunde muita gente. Voce monta tudo certinho, o empurrao acontece, mas o personagem nao se afasta da parede. O motivo esta na linha que processa o direction.
Repare na sequencia. Dentro do if Input.is_action_just_pressed("jump") a gente define velocity.x com o empurrao. Mas logo depois, no bloco do direction, se o jogador ainda estiver segurando o botao em direcao a parede, velocity.x e sobrescrito de volta e o empurrao some no mesmo quadro. O codigo acima ja esta com a ordem correta, mas se voce inverter os blocos vai cair nessa armadilha.
A forma mais limpa de resolver isso e separar input de fisica. Em vez de aplicar o controle horizontal todo quadro de forma crua, a gente da ao empurrao do wall jump uma janela de tempo em que o input do jogador tem peso reduzido. Assim o lancamento "respira" antes do controle voltar ao normal.
const WALL_JUMP_CONTROL_TIME: float = 0.12
var wall_jump_timer: float = 0.0
func _physics_process(delta: float) -> void:
if wall_jump_timer > 0.0:
wall_jump_timer -= delta
if not is_on_floor():
velocity.y += gravity * delta
else:
jumps_left = MAX_JUMPS
var on_wall: bool = is_on_wall() and not is_on_floor()
if on_wall and velocity.y > 0.0:
velocity.y = min(velocity.y, WALL_SLIDE_SPEED)
if Input.is_action_just_pressed("jump"):
if on_wall:
var wall_normal: Vector2 = get_wall_normal()
velocity.y = JUMP_VELOCITY
velocity.x = wall_normal.x * WALL_JUMP_PUSH
wall_jump_timer = WALL_JUMP_CONTROL_TIME
jumps_left = MAX_JUMPS - 1
elif jumps_left > 0:
velocity.y = JUMP_VELOCITY
jumps_left -= 1
var direction: float = Input.get_axis("move_left", "move_right")
if wall_jump_timer <= 0.0:
if direction != 0.0:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
move_and_slide()
Agora o input horizontal so volta a mandar na velocity.x quando o wall_jump_timer zera. Durante aquela fracao de segundo, o que vale e o empurrao lateral do wall jump. O valor de 0.12 segundos e um ponto de partida. Se o personagem se afasta demais da parede e fica dificil reescalar, baixe esse numero. Se ele gruda de volta cedo demais, suba um pouco.
Por que separar input de fisica importa
Pode parecer detalhe, mas esse principio de ler o input em um lugar e aplicar a fisica em outro e o que evita meia duzia de bugs irritantes em qualquer mecanica de movimento mais avancada. Quando voce mistura tudo no mesmo bloco, uma mecanica nova quase sempre briga com outra que ja existia, como o empurrao brigando com o controle horizontal.
A mesma logica de "abrir uma janela onde o input nao manda" reaparece em outras mecanicas. Um dash, por exemplo, tambem precisa ignorar o controle normal enquanto o impulso acontece. Se voce quiser ir por esse caminho depois, o post sobre dash no Godot 2D usa exatamente esse padrao de timer e estado, e vai parecer familiar depois de ter feito o wall jump.
Ajustando os numeros para o seu jogo
Os valores que usei sao razoaveis para um personagem de tamanho medio, mas tratam tudo como ponto de partida, nao como verdade. A JUMP_VELOCITY controla a altura, a WALL_JUMP_PUSH controla o quanto o personagem se distancia da parede, e a WALL_SLIDE_SPEED define se o jogador escorrega devagar ou rapido.
A melhor forma de calibrar e brincar com um valor de cada vez enquanto joga. Mude so a WALL_SLIDE_SPEED e sinta. Depois so o empurrao. Mexer em tudo ao mesmo tempo costuma confundir, porque voce nao sabe qual numero causou a mudanca na sensacao. Anote os valores que parecerem bons antes de seguir mexendo, para nao perder um ajuste que ja estava agradavel.
Fechando o ciclo
Com esse script voce tem um CharacterBody2D que pula do chao, faz um segundo pulo no ar via contador, escorrega pela parede com is_on_wall() e da o wall jump com empurrao lateral usando get_wall_normal(), tudo sem o input do jogador brigar com a fisica do lancamento. A partir daqui da para somar coyote time, jump buffer ou um dash, e cada uma dessas mecanicas vai encaixar melhor justamente porque a base ja separa leitura de input de aplicacao de movimento. Abra o projeto, cole o script no seu personagem, ajuste os numeros e veja como muda a sensacao de subir um corredor vertical pulando de parede em parede.


