Voltar para o Blog
Quest Log

Movimento de Personagem 2D de Plataforma na Prática: Aceleração, Fricção e Controle no Ar

Personagem 2D pixel art correndo e pulando entre plataformas flutuantes com trilhas de movimento

Tutorial prático de movimento plataforma 2D no Godot 4: aceleração, fricção, controle no ar e tuning de game feel com CharacterBody2D, com código completo.

Ferramenta:Godot

Movimento de Personagem 2D de Plataforma na Prática: Aceleração, Fricção e Controle no Ar

Movimento plataforma 2D é o tipo de coisa que parece resolvida em dez linhas de código e na verdade define se o seu jogo é gostoso ou genérico. O template padrão do Godot move o personagem, sim. Mas ele liga e desliga a velocidade como um interruptor, e interruptor não tem peso, não tem momentum, não tem personalidade. A diferença entre "anda" e "controla bem" mora em três sistemas: aceleração, fricção e controle no ar.

Esse tutorial monta um controlador de plataforma completo no Godot 4 com CharacterBody2D, camada por camada. Todo código é GDScript que roda como está. No final você vai ter um script único com tudo exposto no Inspector pra ajustar sem recompilar nada na cabeça.

O esqueleto: CharacterBody2D e o loop de física

A estrutura de cena é a de sempre: um CharacterBody2D com uma CollisionShape2D (cápsula, de preferência, porque desliza em quina sem travar) e um Sprite2D ou AnimatedSprite2D.

Player (CharacterBody2D)
├── CollisionShape2D
└── AnimatedSprite2D

E o ponto de partida, o controlador mínimo:

extends CharacterBody2D

const SPEED: float = 300.0
const JUMP_VELOCITY: float = -400.0

var gravity = 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 = Input.get_axis("move_left", "move_right")
    velocity.x = direction * SPEED

    move_and_slide()

Isso funciona. E é exatamente o que vamos desmontar, porque velocity.x = direction * SPEED é o interruptor: o personagem sai de 0 pra 300 pixels por segundo em um único frame e volta pra 0 no instante em que você solta o botão. Nenhum objeto com massa se comporta assim, e o cérebro do jogador percebe isso mesmo sem saber explicar.

Antes de seguir: tudo aqui vive em _physics_process, nunca em _process. O passo de física é fixo (60 vezes por segundo por padrão), então o movimento fica idêntico em qualquer máquina. Se a diferença entre os dois callbacks e o papel do delta ainda é nebulosa pra você, o guia de delta time e movimento por frame resolve isso de vez.

Aceleração e fricção: o coração do movimento plataforma 2D

A ideia é simples: em vez de setar a velocidade no alvo, você caminha até ela um pouco por frame. O move_toward() faz isso sem você escrever if pra cada direção:

@export var max_speed := 300.0
@export var acceleration := 2200.0  # pixels/s² indo na direção do input
@export var friction := 1800.0      # pixels/s² freando sem input

func apply_run(direction: float, delta: float) -> void:
    if direction != 0:
        velocity.x = move_toward(velocity.x, direction * max_speed, acceleration * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, friction * delta)

Multiplicar por delta transforma os valores em taxa por segundo, então com acceleration = 2200 o personagem leva uns 0,14 segundo pra atingir velocidade máxima. Curto o bastante pra responder, longo o bastante pra ter peso.

Usei @export de propósito: esses números são tuning, não lógica. Expostos no Inspector, você ajusta com o jogo rodando e sente o resultado na hora. É assim que se calibra game feel, na mão, não na teoria.

O caso da virada

Tem um terceiro estado escondido aí que muita gente ignora: o jogador está correndo pra direita e aperta esquerda. Se você só aplica a aceleração normal, o personagem demora pra inverter e parece pesado demais. Jogos de plataforma responsivos aplicam uma desaceleração extra na virada:

@export var turn_boost := 2.0  # multiplicador de aceleração ao inverter direção

func apply_run(direction: float, delta: float) -> void:
    if direction != 0:
        var accel := acceleration
        # Input aponta contra a velocidade atual? É uma virada.
        if signf(direction) != signf(velocity.x) and velocity.x != 0:
            accel *= turn_boost
        velocity.x = move_toward(velocity.x, direction * max_speed, accel * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, friction * delta)

São quatro linhas e o controle muda de figura. A virada rápida é um daqueles detalhes que ninguém nota quando existe e todo mundo sofre quando falta.

Fricção define o gênero

Repare na relação entre os dois valores. Fricção alta e aceleração alta dão um controle preciso, de puzzle platformer. Aceleração média com fricção baixa dá deslize, momentum, sensação de velocidade. Fase de gelo é só a mesma fórmula com fricção lá embaixo:

func get_friction() -> float:
    # Detecta o chão atual e devolve a fricção dele.
    if is_on_floor() and get_last_slide_collision():
        var collider = get_last_slide_collision().get_collider()
        if collider.is_in_group("gelo"):
            return 200.0
    return friction

Coloque as plataformas de gelo num grupo chamado gelo no editor e troque friction * delta por get_friction() * delta no freio. Pronto, mecânica de fase inteira saindo de uma função.

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

Controle no ar: onde os jogos bons se separam

Aqui está a decisão de design mais subestimada do gênero: o que acontece com o input enquanto o personagem está no ar?

Na vida real, ninguém muda de direção no meio de um salto. Em jogo de plataforma, quase todos os clássicos permitem, porque controle aéreo é o que deixa o jogador corrigir o pulo e se sentir no comando. A questão não é se você permite, é quanto.

A solução limpa é separar aceleração e fricção do chão das do ar:

@export_group("Ar")
@export var air_acceleration := 1400.0
@export var air_friction := 400.0

func apply_run(direction: float, delta: float) -> void:
    var accel := acceleration if is_on_floor() else air_acceleration
    var fric := friction if is_on_floor() else air_friction

    if direction != 0:
        if signf(direction) != signf(velocity.x) and velocity.x != 0:
            accel *= turn_boost
        velocity.x = move_toward(velocity.x, direction * max_speed, accel * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, fric * delta)

Dois detalhes de tuning que aprendi errando:

Aceleração no ar menor que no chão, mas não muito menor. Algo entre 50% e 80% do valor de chão funciona pra maioria dos jogos. Abaixo disso o personagem parece um tijolo arremessado.

Fricção no ar bem baixa. Se o jogador solta o direcional no meio do pulo, o esperado é que o corpo continue a trajetória, não que freie no ar como se houvesse atrito. Fricção aérea alta é um dos jeitos mais rápidos de deixar o pulo artificial.

Gravidade assimétrica e queda máxima

O pulo de arco perfeitamente simétrico, mesma velocidade subindo e descendo, parece flutuante. Quase todo plataforma bom desce mais rápido do que sobe. A implementação é uma gravidade multiplicada na descida:

@export var fall_gravity_multiplier := 1.6
@export var max_fall_speed := 700.0

func apply_gravity(delta: float) -> void:
    if is_on_floor():
        return
    var g := gravity
    if velocity.y > 0:  # descendo (Y cresce pra baixo no 2D)
        g *= fall_gravity_multiplier
    velocity.y = minf(velocity.y + g * delta, max_fall_speed)

O max_fall_speed resolve dois problemas de uma vez: queda longa não vira velocidade absurda que atravessa plataforma fina, e o jogador consegue mirar o pouso porque a velocidade terminal é previsível.

Pulo de altura variável

Pra fechar o controle aéreo, o corte de pulo: soltar o botão cedo encurta o salto. Com isso, um botão só produz desde um pulinho até o pulo cheio:

@export var jump_velocity := -420.0
@export var jump_cut := 0.45  # fração da velocidade que sobra ao soltar

func handle_jump() -> void:
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_velocity
    if Input.is_action_just_released("jump") and velocity.y < 0:
        velocity.y *= jump_cut

Se quiser ir além, coyote time e jump buffer entram aqui também. Eu cobri os dois em detalhe no tutorial de física do Godot, então não vou repetir, mas a versão curta: são dois temporizadores que perdoam erro de timing do jogador e valem cada linha.

O script completo

Juntando tudo num único controlador:

extends CharacterBody2D

@export_group("Chão")
@export var max_speed := 300.0
@export var acceleration := 2200.0
@export var friction := 1800.0
@export var turn_boost := 2.0

@export_group("Ar")
@export var air_acceleration := 1400.0
@export var air_friction := 400.0
@export var fall_gravity_multiplier := 1.6
@export var max_fall_speed := 700.0

@export_group("Pulo")
@export var jump_velocity := -420.0
@export var jump_cut := 0.45

var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta: float) -> void:
    apply_gravity(delta)
    handle_jump()
    apply_run(Input.get_axis("move_left", "move_right"), delta)
    move_and_slide()

func apply_gravity(delta: float) -> void:
    if is_on_floor():
        return
    var g := gravity
    if velocity.y > 0:
        g *= fall_gravity_multiplier
    velocity.y = minf(velocity.y + g * delta, max_fall_speed)

func handle_jump() -> void:
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_velocity
    if Input.is_action_just_released("jump") and velocity.y < 0:
        velocity.y *= jump_cut

func apply_run(direction: float, delta: float) -> void:
    var accel := acceleration if is_on_floor() else air_acceleration
    var fric := friction if is_on_floor() else air_friction

    if direction != 0:
        if signf(direction) != signf(velocity.x) and velocity.x != 0:
            accel *= turn_boost
        velocity.x = move_toward(velocity.x, direction * max_speed, accel * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, fric * delta)

Lembre de criar as ações move_left, move_right e jump em Project Settings > Input Map antes de rodar, senão o Godot reclama de ação inexistente.

Como ajustar os valores na prática

Os números que deixei são um ponto de partida honesto, não verdade absoluta. O processo de tuning que eu uso:

  1. Monte uma fase de teste feia. Plataformas em alturas e distâncias variadas, um corredor longo pra sentir a corrida, uma escadinha pra testar pulos curtos. Sem arte, só colisão.
  2. Ajuste um parâmetro por vez com o jogo rodando. Como tudo é @export, muda no Inspector e sente na hora.
  3. Ordem que funciona pra mim: velocidade máxima primeiro (escala geral do jogo), depois pulo e gravidade (o arco), depois aceleração e fricção (a textura do controle), e o ar por último.
  4. Grave a si mesmo jogando. Sério. Vendo a gravação você nota hesitação e correção de trajetória que não percebe jogando.

Um erro comum nessa fase é perseguir os valores de outro jogo. Celeste, Hollow Knight e Mario têm tunings completamente diferentes entre si, e os três controlam bem. O que eles têm em comum não são os números, é a coerência: cada um escolheu uma personalidade de movimento e calibrou tudo a favor dela.

Fechando

Movimento de plataforma bom não é um algoritmo secreto, é um punhado de decisões pequenas tomadas com intenção: acelerar em vez de teleportar a velocidade, frear diferente de acelerar, virar mais rápido do que correr, cair mais rápido do que subir, e dar ao jogador controle no ar suficiente pra ele se sentir dono do pulo.

O script desse tutorial cobre tudo isso em menos de 60 linhas. Copie, cole num CharacterBody2D e gaste uma hora só mexendo nos valores do Inspector. Essa uma hora de tuning vai te ensinar mais sobre game feel do que qualquer leitura, incluindo essa.

Com a base do movimento pronta, dá pra ir empilhando comportamentos: fazer o personagem virar o sprite na direção do movimento pra ele olhar pro lado certo, tratar escadas em plataforma 2D pra subir e descer entre níveis sem depender só do pulo, montar uma plataforma móvel que carrega o personagem sem ele escorregar, e implementar o agachar (crouch) em plataforma 2D pra passar por passagens baixas.

E se você ainda não montou o jogo em volta desse controlador, vale ver como fazer um jogo de plataforma no Godot do zero, com cenário, coletáveis e fim de fase, pra esse movimento ganhar um nível de verdade pra rodar.

Com o personagem se movendo, o próximo passo é fazer a câmera 2D seguir o jogador no Godot.