Voltar para o Blog
Quest Log

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

Carro de corrida visto de cima em uma pista 2D com curvas, zona de grama, checkpoints e carros oponentes

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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

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 Vector2 e interpole a direção dela pra direção do carro com lerp. 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 (outro Area2D, reaproveitando o padrão da grama).
  • Ranking de tempo local: salve os cinco melhores tempos num arquivo com ConfigFile e 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.