Voltar para o Blog
Quest Log

Godot: Carro Top-Down 2D Dirigível Passo a Passo (GDScript)

Carro 2D visto de cima derrapando em uma pista de asfalto dentro do editor da Godot

Tutorial de carro top-down 2D dirigível no Godot 4 com CharacterBody2D: direção estilo bicicleta, aceleração, atrito, drift e câmera, em GDScript tipado.

Ferramenta:Godot

Fazer um carro top-down no Godot que seja gostoso de dirigir não exige física de motor, câmbio nem simulação de pneu. Com um CharacterBody2D, meia dúzia de variáveis e um modelo de direção estilo bicicleta, você monta um controlador arcade que acelera, freia, faz curva de verdade e derrapa quando você exagera na velocidade. Neste tutorial vamos construir tudo em GDScript 4 tipado, bloco por bloco, e no final você terá valores prontos para tunar o comportamento do seu jogo.

O objetivo aqui é claro: arcade. Nada de torque, RPM ou transferência de peso realista. Queremos um carro que responde na hora, escorrega de um jeito controlável e roda estável em qualquer taxa de frames. Este post é o complemento técnico do nosso guia sobre como fazer um jogo de corrida, que cobre o gênero como um todo: pista, oponentes, progressão e escopo.

Montando a cena do carro

Crie uma cena nova com esta estrutura:

Carro (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── Camera2D

Detalhes que importam:

  • Sprite2D: use um sprite de carro visto de cima apontando para a direita. No Godot, a rotação zero aponta para +X, e todo o código abaixo assume isso. Se o seu sprite aponta para cima, gire a textura ou ajuste o Sprite2D em 90 graus, mas deixe o nó raiz alinhado com +X.
  • CollisionShape2D: use uma CapsuleShape2D deitada (rotacione o CollisionShape2D em 90 graus, já que a cápsula nasce em pé). A cápsula é melhor que um retângulo porque as pontas arredondadas deslizam nas paredes da pista em vez de travar nas quinas.
  • Camera2D: filha do carro, com suavização ligada. Falamos dela mais abaixo.

Antes do código, configure o Input Map em Projeto > Configurações do Projeto > Mapa de Entrada com quatro ações: accelerate, brake, steer_left e steer_right. Associe W/S/A/D ou as setas, como preferir.

O esqueleto do controlador

Anexe um script ao CharacterBody2D. Começamos declarando todas as variáveis com tipagem estática. Cada @export aparece no Inspector, o que acelera muito o tuning:

extends CharacterBody2D

@export var wheel_base: float = 70.0        # distancia entre eixos, em pixels
@export var steering_angle: float = 15.0    # angulo maximo do volante, em graus
@export var engine_power: float = 900.0     # forca de aceleracao
@export var braking: float = -450.0         # forca de freio/re
@export var max_speed_reverse: float = 250.0
@export var friction: float = -55.0         # atrito com o chao
@export var drag: float = -0.06             # resistencia do ar
@export var slip_speed: float = 400.0       # acima disso, o carro derrapa
@export var traction_fast: float = 2.5      # tracao em alta velocidade
@export var traction_slow: float = 10.0     # tracao em baixa velocidade

var acceleration: Vector2 = Vector2.ZERO
var steer_direction: float = 0.0

O _physics_process é só um orquestrador. Ele zera a aceleração do frame, lê o input, aplica atrito, calcula a direção e move o corpo:

func _physics_process(delta: float) -> void:
    acceleration = Vector2.ZERO
    get_input()
    apply_friction(delta)
    calculate_steering(delta)
    velocity += acceleration * delta
    move_and_slide()

Repare que tudo é multiplicado por delta. É isso que garante que o carro ande na mesma velocidade a 30 ou a 144 FPS. Se esse conceito ainda não está sólido para você, leia nosso post sobre delta time e movimento por frame antes de continuar, porque todo o resto depende dele.

Input: acelerar, frear e virar

O input só faz duas coisas: define o ângulo do volante e empurra o carro para frente ou para trás ao longo do próprio eixo. transform.x é o vetor "frente do carro", então a aceleração sempre segue o nariz do veículo:

func get_input() -> void:
    var turn: float = Input.get_axis("steer_left", "steer_right")
    steer_direction = turn * deg_to_rad(steering_angle)
    if Input.is_action_pressed("accelerate"):
        acceleration = transform.x * engine_power
    if Input.is_action_pressed("brake"):
        acceleration = transform.x * braking

Input.get_axis devolve um valor entre -1 e 1, então steer_direction guarda o ângulo do volante em radianos, negativo para a esquerda e positivo para a direita. O freio usa força negativa: com o carro parado, segurar brake faz ele dar ré naturalmente.

Atrito e resistência do ar

Sem atrito, o carro deslizaria para sempre depois que você solta o acelerador. Usamos duas forças que apontam contra a velocidade:

  • friction: proporcional à velocidade. Domina em baixa velocidade e faz o carro parar de vez.
  • drag: proporcional à velocidade ao quadrado. Domina em alta velocidade e define, na prática, a velocidade máxima do carro (o ponto onde o drag anula o motor).
func apply_friction(delta: float) -> void:
    if acceleration == Vector2.ZERO and velocity.length() < 5.0:
        velocity = Vector2.ZERO
    var friction_force: Vector2 = velocity * friction * delta
    var drag_force: Vector2 = velocity * velocity.length() * drag * delta
    acceleration += friction_force + drag_force

A primeira condição é um detalhe importante: quando o carro está quase parado e sem input, zeramos a velocity na marra. Sem isso, o atrito faz o carro oscilar em micro movimentos e ele nunca para de verdade.

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

Direção estilo bicicleta: a curva de verdade

Aqui está o coração do tutorial. Girar o carro direto com rotation += algo faz ele girar no lugar como um tanque. O truque clássico dos jogos de corrida 2D é o modelo de bicicleta simplificado: fingimos que o carro tem só duas rodas, uma dianteira e uma traseira, separadas pela distância wheel_base.

A cada frame de física:

  1. Calculamos onde estão a roda traseira e a dianteira a partir do centro do carro.
  2. A traseira avança na direção atual da velocity.
  3. A dianteira avança na direção da velocity rotacionada pelo ângulo do volante.
  4. O novo heading do carro é o vetor que vai da traseira até a dianteira.

Como a dianteira anda "para o lado" e a traseira segue reto, o carro descreve um arco. E o melhor: o raio da curva depende da velocidade. Devagar, o carro vira fechado; rápido, a curva abre sozinha. É exatamente o comportamento que a mão do jogador espera.

func calculate_steering(delta: float) -> void:
    var rear_wheel: Vector2 = position - transform.x * wheel_base / 2.0
    var front_wheel: Vector2 = position + transform.x * wheel_base / 2.0
    rear_wheel += velocity * delta
    front_wheel += velocity.rotated(steer_direction) * delta
    var new_heading: Vector2 = rear_wheel.direction_to(front_wheel)

    var traction: float = traction_slow
    if velocity.length() > slip_speed:
        traction = traction_fast

    var dot: float = new_heading.dot(velocity.normalized())
    if dot > 0.0:
        velocity = velocity.lerp(new_heading * velocity.length(), traction * delta)
    elif dot < 0.0:
        velocity = -new_heading * min(velocity.length(), max_speed_reverse)

    rotation = new_heading.angle()

Vamos por partes:

  • As duas rodas: transform.x * wheel_base / 2.0 desloca metade do entre-eixos para frente e para trás do centro. Depois cada roda avança um frame de movimento, a dianteira com a velocity rotacionada pelo volante.
  • O dot product: new_heading.dot(velocity.normalized()) diz se estamos indo para frente (positivo) ou de ré (negativo). Na ré, alinhamos a velocity direto com o heading invertido e limitamos com max_speed_reverse, porque ninguém quer dar ré derrapando a 900 pixels por segundo.
  • O drift: em vez de forçar velocity = new_heading * velocity.length(), usamos velocity.lerp(...). A velocity persegue o heading, mas com atraso controlado pela tração. Abaixo de slip_speed, a tração é alta (10.0) e o carro gruda na direção que aponta. Acima, a tração cai para 2.5 e a inércia ganha: o carro aponta para dentro da curva enquanto ainda desliza para fora. Isso é a derrapagem, em uma linha de código.

Se quiser entender melhor por que interpolar velocity funciona e como o Godot trata forças e corpos, nosso guia de física de jogos na Godot cobre a base por trás disso.

Câmera que segue com suavização

Com o carro andando, a câmera dura vem em seguida. Como a Camera2D já é filha do carro, basta ligar a suavização no Inspector:

  • Position Smoothing > Enabled: ativado
  • Position Smoothing > Speed: comece com 5.0

Em alta velocidade, vale aumentar o zoom levemente para fora (por exemplo zoom = Vector2(0.8, 0.8)) para o jogador ver mais pista à frente. Se quiser drag margins, limites de mapa e o detalhe de por que a câmera pode tremer, temos um tutorial dedicado de câmera 2D que segue o jogador no Godot que se encaixa direto nesta cena.

A pista com colisão

O carro usa move_and_slide, então qualquer corpo físico segura ele. Duas opções:

  • StaticBody2D com CollisionPolygon2D: desenhe o contorno interno e externo da pista como dois polígonos. Rápido para prototipar.
  • TileMap com física: no TileSet, adicione uma camada de física e pinte os polígonos de colisão nos tiles de barreira (guard rails, muros, grama alta). Escala melhor para pistas grandes.

Nos dois casos, confira que a collision layer da pista está dentro da collision mask do carro. A cápsula do carro vai deslizar pelas paredes em vez de parar seco, o que perdoa o jogador que raspa na curva.

Voltas e checkpoints com Area2D

Para fechar, um sistema de volta simples: espalhe Area2D pela pista (uma na linha de chegada, duas ou três no meio do traçado) e conte a passagem em ordem. Script do checkpoint:

extends Area2D

signal reached(index: int)

@export var index: int = 0

func _on_body_entered(body: Node2D) -> void:
    if body is CharacterBody2D:
        reached.emit(index)

Conecte o sinal body_entered da própria Area2D a esse método. No nó da fase, um gerenciador valida a ordem para ninguém completar volta andando de ré por cima da linha de chegada:

extends Node2D

@export var total_checkpoints: int = 3
var next_checkpoint: int = 0
var laps: int = 0

func _on_checkpoint_reached(index: int) -> void:
    if index != next_checkpoint:
        return
    next_checkpoint += 1
    if next_checkpoint >= total_checkpoints:
        next_checkpoint = 0
        laps += 1
        print("Volta completa! Total: %d" % laps)

O checkpoint só conta se for o esperado. Passou no 2 sem passar no 1? Nada acontece. Simples e à prova de atalho.

Com o carro rodando e os checkpoints funcionando, um proximo passo divertido e gravar a sua volta e correr contra ela. Veja como fazer um sistema de replay com carro-fantasma na Godot.

Como tunar o carro

Os valores sugeridos dão um hatch ágil de kart arcade. O comportamento muda assim:

ParâmetroSe aumentarSe diminuir
engine_powerAcelera mais rápido e sobe a velocidade finalCarro pesado, lento para reagir
steering_angleCurvas mais fechadas, quase nervosoDirigível de caminhão
wheel_baseCurva mais aberta e estável (limusine)Curva agressiva (kart)
dragVelocidade máxima caiCarro de foguete, difícil de segurar
frictionPara rápido ao soltar o aceleradorDesliza como no gelo
slip_speedSó derrapa em velocidade muito altaDerrapa em qualquer esquina
traction_fastDrift curto, recupera rápidoDrift longo estilo drift japonês
traction_slowGruda total em baixa velocidadeEscorregadio até devagar

Duas receitas prontas: para um jogo de rally, baixe traction_fast para 1.5 e slip_speed para 300, e o carro vive de lado. Para corrida limpa estilo micro machines, suba traction_fast para 6.0 e o drift praticamente some.

Se você curte a visão de cima mas quer ação em vez de corrida, o mesmo tipo de câmera top-down serve pra um shooter: veja como fazer um jogo de tiro top-down no Godot, com mira no mouse e inimigos que perseguem o jogador.

Um aviso de escopo: resista à tentação de adicionar marchas, turbo e transferência de peso agora. Esse controlador de menos de 60 linhas sustenta jogo completo com IA de oponentes (basta alimentar steer_direction e a aceleração a partir de waypoints em vez do Input). Primeiro deixe a pista, o loop de voltas e o game feel redondos. Profundidade de simulação é polimento, não fundação.

Perguntas frequentes

Uso CharacterBody2D ou RigidBody2D para carro no Godot?

Para um carro arcade top-down, CharacterBody2D é a escolha mais previsível: você controla a velocidade direto no código e o carro responde exatamente como você programou. RigidBody2D entrega física realista, mas exige lutar contra o motor de física para deixar o controle gostoso. A maioria dos jogos de corrida 2D arcade usa corpo cinemático.

Como fazer o carro derrapar (drift) no Godot?

A derrapagem nasce da diferença entre a direção para onde o carro aponta (heading) e a direção para onde ele se move (velocity). Interpolando a velocity em direção ao heading com lerp e um fator de tração, você controla o quanto o carro escorrega. Tração baixa em alta velocidade produz o drift; tração alta gruda o carro na pista.

Como faço o carro fazer curva de verdade, e não só girar no lugar?

Use o modelo de bicicleta simplificado: simule uma roda dianteira e uma traseira separadas por wheel_base. A traseira avança na direção atual e a dianteira avança na direção rotacionada pelo volante. O novo heading é o vetor entre as duas. Isso faz o raio da curva depender da velocidade, como num carro real.

Serve para jogo de corrida com IA de oponentes?

Sim. Como o controlador só depende de dois valores por frame (aceleração e direção do volante), você pode alimentar esses valores tanto pelo Input do jogador quanto por um script de IA que segue waypoints pela pista. O mesmo carro serve para os dois casos sem mudar a física.

Por que meu carro atravessa as paredes da pista?

Confira três pontos: o movimento precisa usar move_and_slide (que respeita colisões), as paredes precisam ser StaticBody2D ou um TileMap com física configurada na camada, e as collision layers e masks do carro e da pista precisam se enxergar. Se você somar na position direto, a colisão é ignorada.