A ideia é um jogo de estacionamento, e estacionar é o que um carro faz de mais devagar: sair com a embreagem, andar a 5 km/h, virar o volante até o fim, dar ré. Um carro que só “anda para onde aponta” não serve para isso. Antes de pensar em vagas e cones, quis um carro com as peças que fazem um carro manual ser difícil de manobrar.
O projeto é em C++23 com raylib para a janela e o desenho, e Dear ImGui para um painel de depuração com tudo o que a física calcula. O esqueleto (CMake com presets, dependências por FetchContent, Clangd) saiu do cpp-gen, o gerador de projetos C++ que eu mantenho. Vou seguir a ordem dos commits, porque cada um resolveu uma limitação do anterior.
Do pedal à roda
O primeiro commit criou o carro como uma corrente de peças, cada uma numa classe: Engine, Clutch e Gearbox. O torque passa por elas na mesma ordem de um carro de verdade:
- Motor. O torque segue uma curva em sino com pico de 350 Nm a 4.500 rpm, nunca abaixo de 10% do pico, multiplicada pelo acelerador. Dele sai um atrito interno que cresce com a rotação, de 15 a 65 Nm. Com o pé fora do acelerador, o torque fica negativo, e é isso que segura o carro quando você tira o pé (o freio motor).
- Embreagem. O pedal define o quanto ela está acoplada, de 0 a 100%. A capacidade é de 600 Nm vezes o acoplamento: um torque maior do que isso passa só até o limite, e o resto é a embreagem patinando.
- Câmbio. Cinco marchas (3,5, 2,0, 1,4, 1,0 e 0,8), ré com −3,0 e diferencial de 3,4, que multiplica todas. Só troca com a embreagem apertada.
- Roda. O torque no eixo dividido pelo raio da roda (0,33 m) vira força. Essa força é limitada pela aderência do pneu traseiro (1,1 vezes o peso sobre o eixo); passou disso, a roda patina e só 70% chega ao chão.
Os pedais também não são botões. O acelerador sobe a 2 por segundo e desce a 4, o freio sobe a 3 e desce a 5, então segurar a seta por meio segundo é meio pedal. Isso parece detalhe, mas é o que deixa dosar a saída com o teclado.
A parte que mais me ensinou foi a rotação do motor. Ela não é um número independente: com a embreagem acoplada, o motor gira na velocidade que as rodas e a marcha impõem; com ela solta, gira livre, subindo com o acelerador e caindo 3.500 rpm por segundo sem ele. No meio do caminho, as duas coisas se misturam na proporção do acoplamento:
// Blend free-revving RPM with drivetrain-constrained RPM based on clutch engagementif (!is_neutral && clutch_engagement > 0.01F) m_current_rpm = Lerp(free_rpm, target_rpm, clutch_engagement);else m_current_rpm = free_rpm;O target_rpm sai da velocidade do carro: rotação da roda vezes a relação da marcha vezes o diferencial. Quando a roda patina, a embreagem conta como solta nessa conta, e o motor sobe de giro como num carro de verdade derrapando a traseira.
Sair sem deixar morrer
Um carro parado engatado tem rodas a 0 rpm, então a conta acima puxaria o motor para zero. Para não exigir um pé esquerdo perfeito no teclado, abaixo de 3 m/s (10,8 km/h) o carro dosa a embreagem sozinho: o acoplamento é a rotação acima da marcha lenta, (rpm − 1.000) / 1.000. O motor sobe, acopla um pouco, é puxado para baixo, e acha um equilíbrio.

Passando de 3 m/s, a embreagem acopla de vez e o motor fica preso às rodas. Daí o carro sai de verdade: em cerca de 6 segundos de 1ª ele chega a 67,8 km/h, a 6.800 rpm.


Nas capturas, o carro passa de 134,6 km/h na 2ª e de 155,9 km/h na 3ª. Nessa versão a velocidade era um único número: o carro andava para onde apontava, e virar o volante só girava o desenho. Para estacionar, isso não basta.
Pneus que escorregam: Pacejka
O segundo commit trocou esse número por vetores. A velocidade passou a existir em dois referenciais: o do mundo, para mover o carro na tela, e o do próprio carro, com uma componente para a frente e outra de lado. O carro ganhou também uma velocidade de rotação (a guinada) e um momento de inércia.
Com uma velocidade de lado, cada eixo tem um ângulo de deriva: a diferença entre para onde o pneu aponta e para onde ele está indo de fato. É esse ângulo que gera a força lateral que faz o carro curvar, e a relação entre os dois não é linear. Até um certo ângulo, mais ângulo dá mais força; passando dele, o pneu satura e começa a escorregar. A fórmula mais usada para isso é a “fórmula mágica” de Pacejka, com quatro coeficientes de ajuste:
float Car::CalculatePacejka(float slip_angle, float normal_load){ float B = TIRE_B; // 1.5: rigidez float C = TIRE_C; // 1.9: forma da curva float D = normal_load * TIRE_D; // pico, proporcional ao peso sobre o pneu float E = TIRE_E; // 0.97: curvatura depois do pico return D * sinf(C * atanf((B * slip_angle) - (E * (B * slip_angle - atanf(B * slip_angle)))));}As forças laterais da frente e de trás entram na soma das forças e no torque de guinada (a força da frente vezes a distância até o centro, menos a de trás vezes a dela). Uma curva fechada em alta velocidade agora faz o carro escorregar de lado e perder velocidade, em vez de virar como num trilho.

O ângulo de deriva é calculado com atan2 da velocidade de lado sobre a velocidade para a frente, e perto do zero essa conta é instável: uma velocidade para a frente minúscula dá ângulos enormes. Por isso, abaixo de 3 m/s com o volante virado, a guinada vem de um modelo de bicicleta, pura geometria: o carro gira em volta de um raio que é a distância entre eixos dividida pela tangente do ângulo do volante. É a mesma faixa da embreagem automática, e é a faixa em que se estaciona.
O peso que muda de lugar
O terceiro commit trouxe a transferência de carga. Acelerando, o peso vai para trás; freando, vai para a frente. O quanto vai depende da massa, da aceleração, da altura do centro de gravidade (0,55 m) e da distância entre eixos. O peso de cada eixo passou a alimentar duas coisas: o pico do Pacejka daquele eixo e o limite de tração da roda traseira.
Duas decisões estão anotadas no próprio código. A conta usa a aceleração do quadro anterior, porque a deste quadro depende das forças que dependem do peso: seria uma dependência circular. E cada eixo fica com pelo menos 20% do peso, porque sem peso o pico do Pacejka vai a zero e a força lateral some de uma vez.
Separar antes de crescer
O quarto commit não mudou o comportamento: dividiu o Car.cpp, que estava com mais de 550 linhas, em três arquivos. Car.cpp ficou com o ciclo de vida, a entrada e os getters, CarPhysics.cpp com pedais, transmissão, forças e integração, e CarRendering.cpp com a geometria e o desenho.
Na parte de desenho está a geometria de Ackermann. Numa curva, a roda de dentro percorre um círculo menor que a de fora, então as duas não podem ter o mesmo ângulo, senão uma arrasta. Cada roda da frente recebe o ângulo que aponta para o mesmo centro de curva, e as linhas vermelhas mostram isso.

O que as capturas revelaram
Para tirar as capturas, dirigi o carro por um roteiro: um main só para isso, fora do repositório, que aperta as teclas em quadros definidos com os eventos de automação do próprio raylib e salva a tela com a telemetria do painel. Repetir o mesmo trajeto mostrou dois defeitos que eu não tinha visto jogando.
Apertar a embreagem abaixo de uns 15 km/h para o carro na hora. A 11,1 km/h, três quadros depois de apertar a embreagem, a velocidade é zero.


A causa veio do commit do Pacejka. O carro tinha uma regra de parada total para não ficar deslizando a 0,001 m/s para sempre: abaixo de 0,05 m/s e sem tração, a velocidade vira zero. Na refatoração, a regra passou a olhar a aceleração em vez da velocidade. Com a embreagem apertada, só sobram o arrasto e a resistência de rolagem, e até uns 4,1 m/s (15 km/h) eles desaceleram o carro menos que 0,05 m/s². A correção é voltar a comparar a velocidade.
Com o volante todo virado, o carro não sai do lugar. Acelerador no fundo, 1ª engatada, e ele fica em 0,7 km/h. O modelo de bicicleta cuida da guinada em baixa velocidade, mas a força do pneu dianteiro continua sendo calculada, com um ângulo de deriva perto do ângulo do volante (40°). Isso dá uns 5.700 N de força lateral, e a parte dela que aponta contra o movimento (uns 3.600 N) é mais do que o motor entrega com a embreagem automática em 5% (uns 1.000 N). Num simulador de estacionamento essa é justamente a manobra principal, então é o próximo commit.
Um último aviso para quem abrir o repositório: o commit mais recente troca o carro na tela por um quadrado que anda com as setas, e o desenho do carro e o painel ficaram comentados no main.cpp. As capturas deste post são do commit anterior, o último com o carro inteiro.
O que aprendi
- Modele a corrente inteira, não o resultado. Velocidade como saída do motor, da embreagem, do câmbio e do pneu faz aparecerem sozinhos comportamentos que eu não programei: o freio motor, o giro subindo na troca, a roda patinando.
- Saiba onde o modelo quebra e troque de modelo ali. O Pacejka é ótimo em movimento e instável perto de zero, então a baixa velocidade usa geometria.
- Uma refatoração que “não muda nada” muda. A regra de parada trocou velocidade por aceleração ao mudar de lugar, e só um trajeto repetido mostrou.
- Um painel com todos os números da física vale mais que o desenho bonito. Os dois defeitos apareceram lendo o painel, não olhando o carro.
O código está no Codeberg. Meus repositórios pessoais ficam lá, fora do GitHub, para não depender de uma única plataforma.