Ledge climb no Godot 2D: como escalar a borda de uma plataforma

Tutorial de ledge climb no Godot 4: escalar borda 2D com dois RayCast2D, estado hanging, gravidade congelada e snap final em GDScript tipado.
Ledge climb é a mecânica de agarrar e escalar a borda de uma plataforma quando o pulo do jogador não alcançou o topo por pouco. Em vez de o personagem escorregar de volta feito uma pedra, ele engancha na quina, fica pendurado por um instante e sobe com um input. Nesse tutorial de ledge climb você vai aprender a escalar borda 2D no Godot 4 usando CharacterBody2D, move_and_slide e dois RayCast2D, tudo em GDScript tipado e sem gambiarra. É uma das mudanças de game feel que mais rendem: transforma pulos "quase certos" em vitórias em vez de frustração.
A ideia central é simples de enunciar e traiçoeira de detectar: existe uma parede logo à frente na altura da cabeça, mas essa parede acaba um pouco acima, virando uma borda pisável. O truque está em provar essa geometria com raios em vez de adivinhar. Vou montar a detecção primeiro, depois o estado de "hanging" com a gravidade congelada, a subida com input e o snap da posição final. Se o seu movimento base ainda não está firme, vale passar antes pelo guia de movimento de personagem em plataforma 2D e voltar aqui com o velocity já funcionando.
O que é uma borda agarrável e como detectar
Antes de código, a geometria. Imagine o personagem encostado numa parede, subindo. Uma borda agarrável tem uma assinatura específica: na altura do peito há sólido à frente (a parede), mas na altura logo acima da cabeça o caminho está livre (o ar acima da plataforma). Se os dois estivessem sólidos, seria uma parede alta demais. Se os dois estivessem livres, não haveria nada para agarrar.
Para ler isso, uso dois RayCast2D curtos, apontando na horizontal para a direção que o personagem encara:
- RayCast do peito (
ChestRay): posicionado na altura média do corpo. Ele PRECISA colidir com a parede. É a confirmação de que existe superfície à frente. - RayCast da cabeça (
HeadRay): posicionado um pouco acima da cabeça. Ele PRECISA estar livre, sem colidir. É a confirmação de que a parede termina ali e vira borda.
A cena fica assim:
Player (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D
├── ChestRay (RayCast2D)
└── HeadRay (RayCast2D)
Os dois raios têm target_position apontando na horizontal, algo curto tipo Vector2(16, 0) para a direita. Quando o personagem vira, você espelha o target_position.x. A distância vertical entre eles define qual "espessura" de borda o jogo considera agarrável: se HeadRay estiver logo acima de ChestRay, só bordas finas contam; se estiver bem mais alto, o personagem se pendura em degraus mais grossos.
Detectando a borda com dois RayCast2D em GDScript
Vamos ao código de detecção. O padrão é: os dois raios seguem o facing do personagem, e a borda existe quando ChestRay.is_colliding() é verdadeiro e HeadRay.is_colliding() é falso.
extends CharacterBody2D
const SPEED: float = 200.0
const JUMP_VELOCITY: float = -420.0
const GRAVITY: float = 980.0
const RAY_LENGTH: float = 16.0
@onready var chest_ray: RayCast2D = $ChestRay
@onready var head_ray: RayCast2D = $HeadRay
var facing: int = 1 # 1 = direita, -1 = esquerda
var is_hanging: bool = false
func _update_ray_direction() -> void:
chest_ray.target_position = Vector2(RAY_LENGTH * facing, 0.0)
head_ray.target_position = Vector2(RAY_LENGTH * facing, 0.0)
# force_raycast_update garante leitura no mesmo frame em que giramos
chest_ray.force_raycast_update()
head_ray.force_raycast_update()
func _detect_ledge() -> bool:
_update_ray_direction()
var chest_hits: bool = chest_ray.is_colliding()
var head_free: bool = not head_ray.is_colliding()
return chest_hits and head_free
O detalhe que engana muita gente é o force_raycast_update(). Por padrão o RayCast2D só atualiza a leitura no ciclo de física do próprio node, então se você acabou de mudar target_position no mesmo frame, is_colliding() ainda reflete o estado antigo. Forçar a atualização resolve isso e deixa a detecção confiável no exato instante em que o personagem encosta na parede subindo.
Uma condição extra vale a pena: só faz sentido tentar pendurar quando o personagem está no ar e se movendo para cima ou já em queda contra a parede. Checar is_on_wall() junto com os raios evita que ele grude em qualquer relevo. A regra fica: está no ar, encostou na parede (is_on_wall()), o ChestRay bate e o HeadRay está livre. Aí sim entra o estado de pendurado.
O estado hanging: congelando a gravidade na quina
Detectada a borda, o personagem entra no estado "hanging". Esse estado tem uma regra que quebra o resto do movimento: a gravidade não age. Enquanto pendurado, velocity fica zerado e nenhum GRAVITY * delta é somado. É isso que faz o corpo parar cravado na quina em vez de escorregar.
func _physics_process(delta: float) -> void:
if is_hanging:
_process_hanging()
return
# Movimento normal: gravidade age
if not is_on_floor():
velocity.y += GRAVITY * delta
var direction: float = Input.get_axis("move_left", "move_right")
if direction != 0.0:
velocity.x = direction * SPEED
facing = 1 if direction > 0.0 else -1
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
move_and_slide()
# Tenta pendurar: no ar, contra a parede, com borda detectada
if not is_on_floor() and is_on_wall() and velocity.y > 0.0:
if _detect_ledge():
_start_hanging()
Repare que a checagem de pendurar acontece depois do move_and_slide(), quando is_on_wall() já reflete a colisão daquele frame. Só entra em hanging na queda (velocity.y > 0.0), o que dá aquela sensação clássica de "pulei, não alcancei, mas enganchei na descida".
O _start_hanging() congela tudo:
func _start_hanging() -> void:
is_hanging = true
velocity = Vector2.ZERO
# Aqui voce dispara a animacao de pendurado, por exemplo:
# $Sprite2D.play("hang")
E o processamento enquanto pendurado só escuta dois inputs: subir ou largar. Nada de gravidade, nada de andar.
func _process_hanging() -> void:
velocity = Vector2.ZERO
if Input.is_action_just_pressed("jump") or Input.is_action_just_pressed("move_up"):
_climb_ledge()
elif Input.is_action_just_pressed("move_down"):
_drop_from_ledge()
Congelar a gravidade em um estado dedicado é a mesma lógica que sustenta o wall slide e o próprio dash: você isola um modo do personagem em que as forças normais não se aplicam. Se você já implementou double jump e wall jump no Godot, vai reconhecer o padrão de estado com regras próprias, e é exatamente aí que o ledge climb se encaixa.
Subindo a borda: input e o snap da posição final
Pendurado, o jogador aperta subir. Aqui vem a parte que define se o movimento parece polido ou quebrado: o "snap", ou seja, reposicionar o personagem para ficar em pé em cima da plataforma. Sem snap, ele sobe empurrando o velocity e trava na quina, porque o colisor ainda está do lado da parede. Com snap, você teleporta o corpo para o ponto certo de uma vez.
A posição final é: um pouco acima da altura atual (para limpar a borda) e um pouco à frente na direção do facing (para ficar sobre a plataforma, não pendurado ao lado dela). Os valores dependem do tamanho do seu personagem e da grade do tilemap, então trate como constantes ajustáveis.
const CLIMB_UP: float = 20.0 # quanto sobe para limpar a borda
const CLIMB_FORWARD: float = 14.0 # quanto avanca para pisar na plataforma
func _climb_ledge() -> void:
var target: Vector2 = global_position
target.y -= CLIMB_UP
target.x += CLIMB_FORWARD * facing
# Snap direto para a posicao final em cima da plataforma
global_position = target
is_hanging = false
velocity = Vector2.ZERO
# $Sprite2D.play("climb") # animacao de subida, opcional
func _drop_from_ledge() -> void:
is_hanging = false
# Empurra levemente para longe da parede ao soltar
velocity.x = -facing * 40.0
velocity.y = 60.0
Esse snap "duro", teleportando o global_position, é o mais simples e funciona bem em jogos de pixel art com movimento rápido. Se você quiser algo mais suave, dá para animar o deslocamento com um Tween ao longo de uns 0.15 segundos, movendo o corpo pela curva enquanto a animação de escalada toca. O importante é que, ao fim, o personagem esteja de pé sobre o topo, com is_hanging desligado e o movimento normal retomando na frente seguinte.
Vale um detalhe de robustez: antes de aplicar o snap, você pode conferir com um RayCast2D extra apontando para baixo, a partir do ponto de destino, se realmente há chão ali. Isso evita casos raros de o personagem subir para dentro de outro sólido. Para a maioria dos projetos pequenos, os dois raios de detecção já bastam e o snap direto resolve.
Como o ledge climb melhora o game feel de plataforma
O ledge climb não é enfeite. Ele muda a margem de erro do jogo inteiro. Sem ele, cada pulo é binário: você alcança a plataforma ou cai. Com ele, existe uma faixa de "quase" que vira sucesso, e essa faixa é onde mora a sensação de generosidade que faz o jogador continuar tentando em vez de largar o controle. É primo do coyote time e do jump buffer: todas são mecânicas de perdão que escondem a rigidez da física por trás de um controle que parece justo.
Ele também conversa diretamente com o resto do seu kit de movimento. Como no _start_hanging() você zera o velocity e, se quiser, o contador de pulos, o personagem sai da borda com o pulo e o wall jump recarregados. Isso abre encadeamentos: pular, enganchar, escalar e já emendar um novo pulo ou um dash no topo. Se o seu personagem tem dash de personagem 2D, o ledge climb dá um respiro no ar que reposiciona antes do próximo movimento, e o level design ganha verticalidade sem exigir precisão de pixel do jogador.
Sobre a base de tudo isso: o ledge climb é um bom termômetro de quando a arquitetura do seu movimento começa a pedir uma state machine de verdade. Enquanto for só um bool is_hanging, tudo bem. Quando virarem cinco ou seis estados conversando entre si, o if/else no _physics_process engasga. Aí é hora de refatorar para estados explícitos, e a escolha de linguagem entra na conversa também: se você está em dúvida entre produtividade e performance para esse tipo de lógica, o comparativo de GDScript ou C-Sharp no Godot ajuda a decidir sem achismo.
Fechando o loop
Recapitulando o caminho para escalar borda 2D no Godot 4: dois RayCast2D provam a geometria da borda (peito bate, cabeça livre), com force_raycast_update() para leitura confiável no frame certo; o estado hanging congela a gravidade zerando o velocity; o input de subir dispara o snap que reposiciona o corpo em cima da plataforma; e o de descer solta o personagem de volta ao ar. Tudo com CharacterBody2D, move_and_slide sem argumento, is_on_wall e GDScript tipado, do jeito que o Godot 4 real funciona.
Comece com os valores de exemplo, cole no seu projeto e ajuste CLIMB_UP, CLIMB_FORWARD e a posição dos raios até a subida parecer natural com o tamanho do seu personagem. É meia hora de trabalho que devolve horas de frustração ao jogador. E é assim, mecânica por mecânica, que um protótipo cru começa a parecer um jogo de verdade aqui no CursoGame.Dev.
Perguntas frequentes
Por que preciso de dois RayCast2D para o ledge climb?
Um raio na altura do peito precisa NAO bater na parede e outro na altura da cabeca precisa bater, ou o contrario dependendo da leitura. Essa diferenca de leitura entre os dois raios prova que existe uma borda agarravel logo acima, e nao uma parede cheia ou o vazio.
Como congelar a gravidade enquanto o personagem esta pendurado?
No estado hanging voce simplesmente nao soma GRAVITY ao velocity.y e zera velocity antes do move_and_slide. Sem forca para baixo, o CharacterBody2D fica travado na quina ate o jogador apertar o input de subir ou de largar.
O ledge climb funciona junto com wall jump e double jump?
Sim. O ideal e tratar hanging como mais um estado ao lado de idle, run, jump, wall slide e dash. Ao pendurar voce zera o contador de pulos, entao o personagem sai da borda com pulo e wall jump prontos de novo.


