Jogo de Carro na Unity: Wheel Collider e Câmera
Um unity car controller com WheelCollider que dirige bem depende de quatro ajustes que os tutoriais rápidos pulam: massa realista no Rigidbody, centro de massa baixo, motor e freio aplicados por roda (não no carro inteiro) e a malha da roda sincronizada com GetWorldPose. Aqui você monta o carro completo em C# para o Unity 6.3 LTS, coloca uma câmera de perseguição com Cinemachine, ajusta atrito para um drift básico e vê quando vale a pena trocar tudo isso por um carro arcade.
O resultado é um carro que acelera, freia, faz curva sem capotar e derrapa quando você puxa o freio de mão, pronto para virar a base de um jogo de corrida.
WheelCollider ou carro arcade: escolher
Antes de montar, decida o tipo de carro, porque o caminho muda.
WheelCollider é um collider especial da física 3D da Unity que simula uma roda com suspensão e atrito de pneu. Você aplica torque no eixo (motorTorque), torque de freio (brakeTorque) e ângulo de direção (steerAngle), e a física cuida de aderência, derrapagem e peso. É a escolha para corrida com cara de simulação, rally e jogos de mundo aberto em que o carro precisa reagir ao terreno.
Carro arcade é um Rigidbody (ou nem isso) com velocidade aplicada direto e aderência lateral calculada no script: você cancela uma parte da velocidade de lado a cada frame. É o caminho para kart, corrida top-down e jogos em que o controle precisa ser previsível e o drift tem que ser fácil. Se o seu jogo é visto de cima, o carro top-down na Godot mostra essa abordagem, e a lógica é a mesma na Unity.
Regra prática: se o jogador vai reclamar que o carro é "duro" ou "flutua", você quer WheelCollider bem ajustado. Se vai reclamar que é "difícil de controlar", você quer arcade. Este post segue o WheelCollider.
Montar o carro: corpo, rodas e colliders
A hierarquia que evita metade dos bugs:
Car (Rigidbody, CarController)
├── Body (malha da carroceria + BoxCollider ou MeshCollider convex)
├── WheelColliders
│ ├── FL (WheelCollider)
│ ├── FR (WheelCollider)
│ ├── RL (WheelCollider)
│ └── RR (WheelCollider)
└── WheelMeshes
├── FL_Mesh (malha da roda)
├── FR_Mesh
├── RL_Mesh
└── RR_Mesh
Os WheelColliders ficam separados das malhas. O WheelCollider não gira, quem gira é a malha visual, e misturar os dois no mesmo objeto faz o collider rodar junto e a física enlouquecer.
Configuração de partida:
- Rigidbody do
Car: Mass 1200, Linear Damping 0,05, Angular Damping 0,5, Interpolate ligado. - WheelCollider: Radius igual ao raio da sua roda (0,35 para uma roda de 70 cm de diâmetro), Suspension Distance 0,2, Spring 35000, Damper 4500, Target Position 0,5.
- Posicione cada WheelCollider no centro da malha da roda correspondente.
- O collider do corpo não pode envolver as rodas: deixe ele acima delas, senão o corpo bate no chão antes da suspensão trabalhar.
Os valores de mola e amortecedor batem com uma massa na casa de mil quilos. É por isso que a massa vem primeiro.
Car controller na Unity: motor, freio e direção
O script lê o input pelo Input System: a ação Move do asset padrão do Unity 6 dá o eixo vertical (acelerar e ré) e o horizontal (direção). O freio de mão usa a ação Jump (barra de espaço ou botão sul do controle), que já existe no asset.
Arquivo CarController.cs:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))]
public class CarController : MonoBehaviour
{
[Header("Wheels")]
[SerializeField] private WheelCollider frontLeft;
[SerializeField] private WheelCollider frontRight;
[SerializeField] private WheelCollider rearLeft;
[SerializeField] private WheelCollider rearRight;
[SerializeField] private Transform frontLeftMesh;
[SerializeField] private Transform frontRightMesh;
[SerializeField] private Transform rearLeftMesh;
[SerializeField] private Transform rearRightMesh;
[Header("Driving")]
[SerializeField] private float motorTorque = 1500f;
[SerializeField] private float brakeTorque = 3000f;
[SerializeField] private float handbrakeTorque = 5000f;
[SerializeField] private float maxSteerAngle = 30f;
[SerializeField] private float minSteerAngleAtSpeed = 8f;
[SerializeField] private float speedForMinSteer = 40f;
[SerializeField] private float maxSpeed = 55f;
[Header("Stability")]
[SerializeField] private Vector3 centerOfMassOffset = new Vector3(0f, -0.4f, 0.1f);
[SerializeField] private float driftRearStiffness = 0.5f;
private Rigidbody rb;
private InputAction moveAction;
private InputAction handbrakeAction;
private WheelFrictionCurve rearSidewaysNormal;
public float SpeedMetersPerSecond => rb.linearVelocity.magnitude;
private void Awake()
{
rb = GetComponent<Rigidbody>();
rb.centerOfMass += centerOfMassOffset;
moveAction = InputSystem.actions.FindAction("Player/Move");
handbrakeAction = InputSystem.actions.FindAction("Player/Jump");
rearSidewaysNormal = rearLeft.sidewaysFriction;
frontLeft.ConfigureVehicleSubsteps(5f, 12, 15);
}
private void FixedUpdate()
{
Vector2 input = moveAction.ReadValue<Vector2>();
bool handbrake = handbrakeAction.IsPressed();
float forwardSpeed = Vector3.Dot(rb.linearVelocity, transform.forward);
ApplySteering(input.x);
ApplyDrive(input.y, forwardSpeed, handbrake);
ApplyDrift(handbrake);
}
private void Update()
{
SyncWheel(frontLeft, frontLeftMesh);
SyncWheel(frontRight, frontRightMesh);
SyncWheel(rearLeft, rearLeftMesh);
SyncWheel(rearRight, rearRightMesh);
}
private void ApplySteering(float steerInput)
{
float speed01 = Mathf.Clamp01(SpeedMetersPerSecond / speedForMinSteer);
float steerLimit = Mathf.Lerp(maxSteerAngle, minSteerAngleAtSpeed, speed01);
float angle = steerInput * steerLimit;
frontLeft.steerAngle = angle;
frontRight.steerAngle = angle;
}
private void ApplyDrive(float throttle, float forwardSpeed, bool handbrake)
{
bool braking = (throttle < -0.05f && forwardSpeed > 1f) || (throttle > 0.05f && forwardSpeed < -1f);
float torque = (braking || SpeedMetersPerSecond > maxSpeed) ? 0f : throttle * motorTorque;
rearLeft.motorTorque = torque;
rearRight.motorTorque = torque;
float brake = braking ? brakeTorque * Mathf.Abs(throttle) : 0f;
frontLeft.brakeTorque = brake;
frontRight.brakeTorque = brake;
rearLeft.brakeTorque = handbrake ? handbrakeTorque : brake;
rearRight.brakeTorque = handbrake ? handbrakeTorque : brake;
}
private void ApplyDrift(bool handbrake)
{
WheelFrictionCurve friction = rearSidewaysNormal;
if (handbrake)
{
friction.stiffness = rearSidewaysNormal.stiffness * driftRearStiffness;
}
rearLeft.sidewaysFriction = friction;
rearRight.sidewaysFriction = friction;
}
private static void SyncWheel(WheelCollider collider, Transform mesh)
{
collider.GetWorldPose(out Vector3 position, out Quaternion rotation);
mesh.SetPositionAndRotation(position, rotation);
}
}
Decisões que importam aqui:
Tração traseira, direção dianteira. O torque vai só nas rodas de trás e o ângulo só nas da frente, como num carro de verdade. Tração nas quatro deixa o carro mais estável e menos divertido; experimente as duas.
Freio separado de ré. Apertar para trás com o carro andando para frente freia; com o carro parado, dá ré. Sem essa checagem de forwardSpeed, o carro tenta dar ré a 100 km/h e capota.
Direção que diminui com a velocidade. A 0 km/h as rodas viram 30 graus; a 40 m/s, só 8. Sem isso, um toque no analógico em alta velocidade capota o carro. Jogos de corrida fazem isso o tempo todo, e o jogador só percebe quando falta.
ConfigureVehicleSubsteps aumenta a quantidade de passos de simulação das rodas em baixa e alta velocidade, o que reduz tremedeira. Basta chamar em uma das rodas, porque o ajuste vale para o veículo inteiro.
Centro de massa e capotamento
A Unity calcula o centro de massa a partir dos colliders. Numa carroceria com collider alto, ele fica na altura do banco, e o carro tomba na primeira curva. rb.centerOfMass += centerOfMassOffset desce o ponto 40 cm. Um carro de corrida real tem o centro de massa baixíssimo, e no jogo você pode exagerar: colocar até abaixo do assoalho é um truque comum para carro arcade que nunca capota.
Para ver onde ele está, desenhe uma esfera com Gizmos.DrawSphere(rb.worldCenterOfMass, 0.1f) num OnDrawGizmos. Ajuste olhando, não chutando.
Se ainda capota em curva fechada, uma barra estabilizadora resolve: em cada eixo, compare quanto cada roda está comprimida (com GetGroundHit) e aplique uma força para baixo na roda mais esticada. São 15 linhas e mudam a sensação do carro inteiro.
Com motor, freio, direção e centro de massa certos, o carro já é dirigível. E aqui começa a parte que ninguém mostra: transformar um carro bom num jogo, com pista, voltas, adversários, menu, som de motor que acompanha o giro e uma build que roda liso. São muitas peças que dependem umas das outras, e montar isso sozinho sem um roteiro é onde a maioria dos projetos de corrida para.
Rodas visuais com GetWorldPose
WheelCollider.GetWorldPose devolve a posição e a rotação da roda no mundo já considerando contato com o chão, curso da suspensão, ângulo de direção e o giro acumulado. É só copiar para a malha, como o SyncWheel faz. Rode em Update (ou LateUpdate) e não em FixedUpdate: com o Rigidbody interpolado, a malha acompanha a carroceria suave.
O problema clássico aparece aqui: a roda gira de lado, ou de cabeça para baixo, ou ao contrário. Isso acontece porque o modelo foi exportado com outro eixo de rotação. A correção sem mexer no modelo:
- Crie um objeto vazio
FL_Meshalinhado com o WheelCollider (rotação zero). - Coloque a malha da roda como filha dele e rotacione só a filha até ela ficar certa (90 graus em Y ou Z, normalmente).
- Aponte o script para o objeto vazio.
O vazio recebe a pose correta; a filha carrega a correção de eixo.
Câmera de perseguição
Instale o Cinemachine pelo Package Manager. No Cinemachine 3, que é a versão para Unity 6, o componente se chama CinemachineCamera. Para a câmera de perseguição:
- GameObject > Cinemachine > Cinemachine Camera.
- Tracking Target: o carro.
- Position Control: Follow, com Binding Mode em Lock To Target With World Up. A câmera fica atrás do carro e gira com ele, sem inclinar quando o carro inclina.
- Follow Offset: (0, 2,2, -6).
- Position Damping em torno de 0,4 e Angular Damping em Y de 1,5: a câmera atrasa um pouco na curva e mostra a lateral do carro, o que comunica velocidade.
- Rotation Control: Rotation Composer, mirando um ponto um pouco à frente do carro.
Para sensação de velocidade, aumente o Field of View da lente conforme a velocidade sobe, de 60 para 75 graus, lendo SpeedMetersPerSecond do CarController. O guia de Cinemachine em terceira pessoa detalha cada parâmetro de follow e damping.
Ajuste de fricção e drift básico
O atrito do pneu é uma curva WheelFrictionCurve com quatro números e uma rigidez (stiffness). Para começar, não mexa na forma da curva: mexa só no stiffness.
- forwardFriction.stiffness maior: acelera e freia com mais aderência.
- sidewaysFriction.stiffness maior: faz curva sem escorregar.
- Traseira com aderência lateral menor que a dianteira: o carro tende a sair de traseira (sobre-esterço), mais divertido e mais difícil.
O drift do script reduz a aderência lateral das rodas traseiras pela metade enquanto o freio de mão está apertado e trava as rodas de trás com handbrakeTorque. A traseira escorrega, o carro gira, e quando o jogador solta, a aderência volta.
Atenção a um detalhe de C#: WheelFrictionCurve é uma struct. Escrever rearLeft.sidewaysFriction.stiffness = 0.5f não compila, porque você estaria alterando uma cópia. O caminho é pegar a curva numa variável, alterar e atribuir de volta, como o ApplyDrift faz.
Erros comuns: carro pulando, rodas girando ao contrário
- Carro pulando ou tremendo parado. Massa do Rigidbody baixa demais para a mola. Use 1000 a 1500.
- Carro afundado ou flutuando. Raio do WheelCollider diferente da roda visual, ou o collider posicionado fora do centro da malha.
- Rodas girando ao contrário ou de lado. Eixo do modelo diferente do esperado. Use o objeto vazio intermediário.
- Capota em toda curva. Centro de massa alto e direção sem limite por velocidade.
- Carro não anda.
motorTorquebaixo para a massa, ou freio aplicado sem querer. Com 1200 kg, comece com 1500. - Carro "patina" eterno. Damping zero. Um pouco de Linear Damping e resistência no motor resolvem.
- Código antigo com
rb.velocityourb.drag. No Unity 6 sãolinearVelocityelinearDamping.
Próximo passo
Monte uma pista oval com um Plane e paredes, coloque três triggers de checkpoint e conte voltas, com tempo da melhor volta na tela. Depois troque a tração para as quatro rodas e compare a sensação. Para o lado de design da corrida (checkpoints contra atalho, adversários e ritmo de pista), leia como fazer um jogo de corrida, que usa a Godot mas trata decisões que valem em qualquer engine.
Perguntas frequentes
Por que meu carro com WheelCollider fica pulando?
Quase sempre é a massa do Rigidbody. A suspensão padrão do WheelCollider é pensada para um carro de mais de mil quilos; com a massa padrão de 1 kg, a mola joga o carro para cima. Coloque a massa entre 1000 e 1500, confira se o collider do corpo não encosta nas rodas e só então ajuste Spring e Damper.
WheelCollider ou carro arcade sem rodas físicas?
WheelCollider quando você quer suspensão, peso e derrapagem com cara de simulação. Carro arcade (um Rigidbody com força aplicada e aderência lateral calculada no script) quando quer controle previsível, drift fácil e menos ajuste, como em jogos de kart ou top-down. O arcade é mais fácil de deixar divertido; o WheelCollider é mais fácil de deixar crível.
Como fazer o carro não capotar na curva na Unity?
Baixe o centro de massa com Rigidbody.centerOfMass, colocando-o perto do assoalho ou até um pouco abaixo. Depois, reduza o ângulo de direção conforme a velocidade sobe. Uma barra estabilizadora simples, que transfere força entre as rodas do mesmo eixo, resolve o resto.
Como fazer jogo de corrida na Unity com voltas?
Coloque triggers de checkpoint em ordem ao longo da pista e guarde qual é o próximo esperado. A volta só conta quando o carro cruza a linha de chegada depois de passar por todos na ordem. Isso impede atalhos e dá a base para posição na corrida e tempo de volta.



