Como Fazer um Jogo de Corrida: Carro Arcade, Voltas e Oponentes na Godot

Como fazer um jogo de corrida na Godot 4: física de carro arcade top-down com GDScript tipado, checkpoints contra atalho, voltas e oponentes com IA.
Como Fazer um Jogo de Corrida: Carro Arcade, Voltas e Oponentes na Godot
Aprender como fazer um jogo de corrida é mais acessível do que parece, desde que você escolha a versão certa do gênero pra começar. Corrida não significa obrigatoriamente Gran Turismo: um jogo top-down, visto de cima, no estilo Micro Machines, tem tudo que define o gênero (carro que acelera e faz curva, pista com limites, voltas, cronômetro e oponentes) e cabe num escopo que uma pessoa sozinha consegue terminar.
Neste guia você monta exatamente esse protótipo na Godot 4, com GDScript tipado em todos os exemplos: a física de carro arcade com aceleração, atrito e esterço dependente da velocidade, a pista com zonas que punem quem sai do asfalto, o sistema de voltas com checkpoints que impede atalho, e oponentes que seguem a pista com Path2D. No final, uma lista honesta do que adicionar depois e do que deixar de fora.
Top-down 2D ou 3D: decida antes de abrir a engine
A primeira decisão de qualquer jogo de corrida é a perspectiva, e ela define quase todo o trabalho que vem depois.
O 3D é o que a maioria imagina primeiro: câmera atrás do carro, sensação de velocidade, pista com relevo. Também é onde a maioria trava. Você precisa de modelos de carro, câmera que segue suave, física de corpo rígido com quatro rodas, iluminação, cenário. Cada um desses itens é um estudo por si só, e o carro só fica gostoso de dirigir depois que todos funcionam juntos.
O top-down 2D entrega o mesmo gênero por uma fração do custo. A câmera é fixa ou segue o carro por cima, a arte é um sprite visto de cima, e a física inteira cabe num script de trinta linhas. Curva, freada, ultrapassagem, volta rápida: tudo que faz corrida ser corrida está lá. É por isso que este guia usa top-down 2D. Se mesmo assim você quiser o caminho tridimensional, faça primeiro este protótipo e depois migre os conceitos lendo como fazer seu primeiro jogo 3D na Godot, porque voltas, checkpoints e IA de oponente funcionam igual nos dois mundos.
A cena principal do protótipo fica assim:
Corrida (Node2D)
├── Pista (TileMapLayer)
├── CarroJogador (CharacterBody2D)
├── Grama (Area2D)
├── Checkpoints (Node2D)
│ ├── Checkpoint0 (Area2D)
│ ├── Checkpoint1 (Area2D)
│ └── Checkpoint2 (Area2D)
├── TrajetoIA (Path2D)
│ └── Oponente (PathFollow2D)
│ └── Sprite2D
└── CanvasLayer
├── LabelVoltas (Label)
└── LabelTempo (Label)
Como fazer um jogo de corrida: a física do carro arcade
O coração do jogo é o carro, e aqui vale uma regra de ouro: não simule um carro de verdade. Nada de torque, marcha ou peso por eixo. Física arcade é três ideias simples: uma velocidade escalar que sobe com aceleração e desce com atrito, um ângulo que gira quando o jogador esterça, e a regra de que o esterço depende da velocidade (carro parado não vira, e é isso que dá a sensação de dirigir em vez de deslizar).
Antes do código, crie quatro ações no Project > Project Settings > Input Map: acelerar, freio, esquerda e direita, mapeadas nas setas ou em WASD. O carro é um CharacterBody2D com Sprite2D e CollisionShape2D, com o bico do sprite apontando pra direita (o eixo local X):
class_name CarroJogador
extends CharacterBody2D
@export var aceleracao: float = 600.0
@export var atrito: float = 500.0
@export var velocidade_max: float = 450.0
@export var esterco: float = 2.6
var velocidade_atual: float = 0.0
var na_grama: bool = false
func _physics_process(delta: float) -> void:
var pedal: float = Input.get_axis("freio", "acelerar")
var virar: float = Input.get_axis("esquerda", "direita")
# Na grama o teto de velocidade cai pela metade.
var teto: float = velocidade_max * (0.5 if na_grama else 1.0)
if pedal != 0.0:
velocidade_atual += pedal * aceleracao * delta
else:
velocidade_atual = move_toward(velocidade_atual, 0.0, atrito * delta)
velocidade_atual = clampf(velocidade_atual, -teto * 0.5, teto)
# Esterco proporcional a velocidade: parado nao vira, de re inverte.
var fator: float = clampf(absf(velocidade_atual) / (velocidade_max * 0.5), 0.0, 1.0)
rotation += virar * esterco * fator * signf(velocidade_atual) * delta
velocity = transform.x * velocidade_atual
move_and_slide()
Três linhas fazem o carro parecer carro. A primeira é move_toward no atrito: soltar o pedal desacelera suave até zero, sem parada seca. A segunda é o fator do esterço: dividir a velocidade atual pela metade da máxima faz o carro ganhar capacidade de curva conforme acelera, chegando ao esterço pleno na metade da velocidade máxima. A terceira é o signf: quando a velocidade é negativa (ré), a direção do esterço inverte, exatamente como num carro de verdade dando marcha ré.
Por fim, velocity = transform.x * velocidade_atual aplica a velocidade na direção do bico do carro. É grip total: o carro sempre anda pra onde aponta, sem derrapagem. Pra um primeiro protótipo é o comportamento certo, e o drift entra depois como camada opcional. Se CharacterBody2D, velocity e move_and_slide ainda são novidade pra você, vale a leitura sobre física de jogos na Godot antes de seguir, porque tudo daqui em diante usa esses conceitos.
Pista e limites: TileMap, sprites e a grama que pune
Pra pista você tem dois caminhos igualmente válidos. O primeiro é um TileMapLayer com tiles de asfalto, grama e barreira, ideal pra iterar o traçado rápido. O segundo é uma imagem única da pista inteira num Sprite2D, ideal se você desenha a pista num editor de imagem. Nos dois casos, os limites físicos (muros, barreiras de pneu) são colisões: no TileMapLayer, pinte a física direto no TileSet; no sprite único, contorne a pista com StaticBody2D e alguns CollisionShape2D.
Mas muro sozinho deixa a pista sem punição intermediária: ou o jogador está no asfalto ou bateu. O que dá textura à corrida é a zona lenta fora do traçado, a grama. Ela é um Area2D cobrindo a área fora da pista, com um script mínimo:
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body is CarroJogador:
body.na_grama = true
func _on_body_exited(body: Node2D) -> void:
if body is CarroJogador:
body.na_grama = false
Lembra do teto no script do carro? É ele que faz o trabalho: entrou na grama, a velocidade máxima cai pela metade e o clampf segura o carro. O efeito na prática é que cortar por fora até funciona, mas custa tempo, e o jogador aprende sozinho a respeitar o traçado. Esse tipo de punição suave é muito melhor que parede invisível.
Voltas e checkpoints: como impedir atalho na pista
A ideia ingênua pra contar voltas é colocar uma linha de chegada (um Area2D) e somar uma volta a cada passagem. Ela quebra em segundos: o jogador cruza a linha, dá ré, cruza de novo, e pronto, três voltas em dez segundos. A solução clássica do gênero é a sequência de checkpoints: vários Area2D espalhados ao longo da pista, e a volta só conta se o jogador passou por todos, na ordem.
Coloque os checkpoints como filhos do node Checkpoints, na ordem do traçado, com o último em cima da linha de chegada. Um script único no node pai conecta todos e valida a sequência:
extends Node2D
@export var voltas_para_vencer: int = 3
var proximo_checkpoint: int = 0
var volta_atual: int = 0
var corrida_ativa: bool = true
func _ready() -> void:
for i: int in get_child_count():
var checkpoint: Area2D = get_child(i)
checkpoint.body_entered.connect(_on_checkpoint.bind(i))
func _on_checkpoint(body: Node2D, indice: int) -> void:
if not body is CarroJogador:
return
# So aceita o checkpoint esperado: fora de ordem, ignora.
if indice != proximo_checkpoint:
return
proximo_checkpoint += 1
if proximo_checkpoint >= get_child_count():
proximo_checkpoint = 0
volta_atual += 1
if volta_atual >= voltas_para_vencer:
corrida_ativa = false
O truque está no bind(i): cada checkpoint conecta o mesmo callback, mas carregando o próprio índice, então um script serve pra pista inteira. E a regra indice != proximo_checkpoint é o antídoto contra atalho: se o jogador cortar pelo meio do mapa e cair no checkpoint 2 sem ter passado pelo 1, nada acontece. A volta só fecha com a sequência completa.
O cronômetro fecha o pacote, e não precisa de node Timer nenhum: somar delta a cada frame é mais simples e mais preciso pra tempo de corrida. No mesmo script dos checkpoints:
@onready var label_tempo: Label = $"../CanvasLayer/LabelTempo"
var tempo_corrida: float = 0.0
func _process(delta: float) -> void:
if corrida_ativa:
tempo_corrida += delta
label_tempo.text = _formatar_tempo(tempo_corrida)
func _formatar_tempo(segundos: float) -> String:
var minutos: int = int(segundos / 60.0)
var restantes: int = int(segundos) % 60
var centesimos: int = int(segundos * 100.0) % 100
return "%02d:%02d.%02d" % [minutos, restantes, centesimos]
Quando corrida_ativa vira false na última volta, o cronômetro congela sozinho, e o valor de tempo_corrida é o resultado final, pronto pra tela de vitória.
Oponentes com IA: Path2D e PathFollow2D
Corrida sem oponente é teste de pista. E a boa notícia é que a primeira IA de corrida decente não precisa dirigir de verdade: ela pode seguir um trilho. Desenhe um Path2D acompanhando o traçado ideal da pista (clique em Add Point e contorne o circuito, fechando o loop no final). Cada oponente é um PathFollow2D filho desse path, com o sprite do carro dentro:
extends PathFollow2D
@export var velocidade_base: float = 380.0
@export var variacao: float = 40.0
var velocidade: float = 0.0
func _ready() -> void:
# Cada oponente sorteia um ritmo proprio.
velocidade = velocidade_base + randf_range(-variacao, variacao)
rotates = true
loop = true
func _physics_process(delta: float) -> void:
progress += velocidade * delta
O PathFollow2D cuida de tudo que seria difícil: progress avança o carro ao longo da curva, rotates gira o sprite pra acompanhar o traçado e loop recomeça o circuito sem costura. O randf_range no _ready faz cada oponente correr num ritmo diferente, então a corrida abre naturalmente. Duplique o PathFollow2D duas ou três vezes e ajuste o h_offset de cada um pra eles não ocuparem exatamente a mesma linha na pista.
Aqui entra um conceito que todo jogo de corrida usa e pouca gente nomeia: rubber banding. É o ajuste dinâmico da velocidade dos oponentes pra corrida ficar equilibrada: quem está muito na frente do jogador desacelera um pouco, quem ficou muito atrás acelera um pouco. Mario Kart faz isso descaradamente, e é parte do motivo de as corridas serem sempre disputadas. Na sua versão, basta comparar a posição do oponente com a do jogador de tempos em tempos e ajustar velocidade em até uns dez por cento pra cada lado. Só não exagere: rubber banding forte demais faz o jogador sentir que vencer ou perder não depende dele, e isso mata a graça.
Vale ser honesto sobre o limite dessa IA: ela não desvia, não erra e não disputa espaço, porque está num trilho. Pra um protótipo, é exatamente o que você quer, porque dá vida à pista com vinte linhas de código. A evolução (oponentes com CharacterBody2D dirigindo com a mesma física do jogador, mirando pontos do path) é um ótimo segundo projeto, não o primeiro.
O que adicionar depois: drift, turbo e ranking local
Com carro, pista, voltas, cronômetro e oponentes, você tem um jogo de corrida completo e jogável. A partir daqui, cada adição deve ser uma camada pequena sobre essa base:
- Drift: guarde a velocidade como
Vector2e interpole a direção dela pra direção do carro comlerp. Interpolação rápida é grip, interpolação lenta é derrapagem. Ligue a interpolação lenta num botão e você tem drift arcade. - Turbo: um multiplicador temporário em
velocidade_max, ativado por item ou por zonas de turbo na pista (outroArea2D, reaproveitando o padrão da grama). - Ranking de tempo local: salve os cinco melhores tempos num arquivo com
ConfigFilee mostre na tela de vitória. Bater o próprio recorde segura o jogador por horas. - Sons e game feel: ronco de motor com pitch acompanhando a velocidade, screech na curva, tremida de câmera na colisão.
E uma lista igualmente importante do que não fazer agora: seleção de dez carros, campeonato com temporada, multiplayer online, clima dinâmico. Cada um desses é um projeto inteiro, e nenhum deles melhora seu protótipo antes de a corrida básica estar gostosa. Termine a versão de uma pista e três voltas, coloque alguém pra jogar e ajuste os números exportados até a curva apertada dar friozinho na barriga.
Se ao longo do caminho você percebeu que está aprendendo Godot aos pedaços, catando um tutorial aqui e outro ali, considere seguir uma trilha estruturada do zero ao jogo publicado: veja o melhor curso de Godot pra encurtar esse caminho. O gênero corrida é generoso com quem começa pequeno: a física arcade deste guia é a mesma base que sustenta desde Micro Machines até jogo de kart moderno. A diferença entre eles e o seu protótipo é iteração, não segredo.
Perguntas frequentes
Qual engine usar para fazer um jogo de corrida?
Para um primeiro jogo de corrida, a Godot 4 é a escolha mais prática: é gratuita, leve, e o GDScript deixa a física arcade simples de escrever e ajustar. Unity e Unreal fazem sentido se a meta é corrida 3D realista com física de simulação, mas o custo de aprendizado é bem maior. Todos os exemplos deste guia rodam na Godot 4 sem nenhum plugin.
É melhor começar um jogo de corrida em 2D ou 3D?
Em 2D top-down, sem dúvida. A visão de cima elimina câmera 3D, modelagem, iluminação e suspensão, e mesmo assim entrega o gênero completo: aceleração, curvas, voltas, cronômetro e oponentes. O 3D multiplica o trabalho de arte e de física, e a maioria dos projetos 3D de iniciante morre antes de a primeira pista ficar jogável.
Como fazer o carro derrapar (drift) num jogo de corrida 2D?
Separando a direção do movimento da direção do bico do carro. Em vez de aplicar a velocidade direto no eixo do carro (transform.x), guarde a velocidade como Vector2 e interpole a direção dela pra direção do carro com lerp: quanto mais lenta a interpolação, mais o carro desliza de lado. É uma mudança pequena em relação ao código deste guia, e dá pra ativar só enquanto o jogador segura um botão de drift.
Como impedir que o jogador corte caminho na pista?
Com checkpoints em sequência. Espalhe Area2D numeradas ao longo da pista e só aceite cada passagem se ela for a próxima da ordem. A volta só conta quando o jogador cruza a linha de chegada tendo validado todos os checkpoints anteriores, então cortar pelo meio do mapa não adianta: o checkpoint pulado invalida a volta.
Quanto tempo leva para fazer um jogo de corrida?
Um protótipo top-down 2D com uma pista, três voltas, cronômetro e dois ou três oponentes sai em uma ou duas semanas de trabalho nas horas livres, seguindo a estrutura deste guia. Um jogo de corrida 3D com física decente, várias pistas e IA competente é projeto de meses, mesmo pra equipes pequenas. Comece pelo protótipo 2D e termine ele.


