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

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.
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.
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:
- Calculamos onde estão a roda traseira e a dianteira a partir do centro do carro.
- A traseira avança na direção atual da velocity.
- A dianteira avança na direção da velocity rotacionada pelo ângulo do volante.
- 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.0desloca 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 commax_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(), usamosvelocity.lerp(...). A velocity persegue o heading, mas com atraso controlado pela tração. Abaixo deslip_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.
Como tunar o carro
Os valores sugeridos dão um hatch ágil de kart arcade. O comportamento muda assim:
| Parâmetro | Se aumentar | Se diminuir |
|---|---|---|
engine_power | Acelera mais rápido e sobe a velocidade final | Carro pesado, lento para reagir |
steering_angle | Curvas mais fechadas, quase nervoso | Dirigível de caminhão |
wheel_base | Curva mais aberta e estável (limusine) | Curva agressiva (kart) |
drag | Velocidade máxima cai | Carro de foguete, difícil de segurar |
friction | Para rápido ao soltar o acelerador | Desliza como no gelo |
slip_speed | Só derrapa em velocidade muito alta | Derrapa em qualquer esquina |
traction_fast | Drift curto, recupera rápido | Drift longo estilo drift japonês |
traction_slow | Gruda total em baixa velocidade | Escorregadio 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.
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.


