← voltar ao blog
ACERTOjogos9min de leituramin

Um carro em C++ com motor, embreagem e câmbio: o começo de um simulador de estacionamento

Como montei a física de um carro em C++23 com raylib, commit por commit: a corrente que vai do pedal à roda, a embreagem que patina, os pneus pelo modelo de Pacejka, a transferência de carga e os dois defeitos que as capturas revelaram.

Matheus Nunes PereiraEngenheiro de Software

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:

src/Engine.cpp
// Blend free-revving RPM with drivetrain-constrained RPM based on clutch engagement
if (!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.

O simulador com o painel de depuração à esquerda e o carro preto sobre um quadrado branco; o painel mostra 1ª marcha, acelerador 100%, embreagem 6%, 1.064 rpm e 3,6 km/h
Saindo em 1ª com acelerador no fundo: a embreagem automática fica em 6% e o motor em 1.064 rpm. As linhas vermelhas mostram para onde apontam as rodas da frente.

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.

O carro sozinho sobre o fundo cinza, com o painel mostrando 1ª marcha, 6.808 rpm, embreagem 100%, torque no eixo de 2.694 Nm e 67,8 km/h
Ainda em 1ª, a 67,8 km/h e 6.808 rpm. O torque no eixo (2.694 Nm) é o do motor multiplicado por 3,5 e por 3,4.
O painel no meio da troca para a 2ª: embreagem 0%, torque no eixo 0, rotação de 7.399 rpm e 73,2 km/h
No meio da troca para a 2ª: embreagem apertada, nada chega ao eixo, e o motor, solto, sobe para 7.399 rpm com o pé ainda no acelerador.

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:

src/CarPhysics.cpp
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 carro inclinado em diagonal, com as linhas vermelhas das rodas da frente apontando para baixo; o painel mostra 84,1 km/h, ângulo de 63,6 graus e volante a 19 graus
Virando o volante até o fim a 146 km/h: pouco mais de um segundo depois, o carro já girou 63° e caiu para 84 km/h, com os pneus escorregando de lado.

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 carro com o volante a 40 graus para a direita; as duas linhas vermelhas saindo das rodas da frente divergem, a de dentro mais inclinada que a de fora
Volante a 40° em baixa velocidade: a roda de dentro vira mais que a de fora, e as duas linhas apontam para o mesmo centro de curva.

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.

AntesO painel a 11,1 km/h em 1ª, embreagem 100%
DepoisO mesmo painel três quadros depois, com a embreagem apertada: 0,0 km/h
A 11,1 km/h e, três quadros depois de apertar a embreagem, parado.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Neste post
Escrito por Matheus Nunes PereiraEngenheiro de Software. Sistemas distribuídos, telemetria em tempo real e software que conversa com hardware. Se tiver um problema assim, vamos conversar.Engenheiro de Software

Sistemas distribuídos, telemetria em tempo real e software que conversa com hardware. Se tiver um problema assim, vamos conversar.