Hi,
I'm currently working on simulating realistic vehicle physics, with a particular focus on vehicle acceleration. I’ve been using the thesis “Development of a Car Physics Engine for Games” (https://nccastaff.bournemouth.ac.uk/jmacey/MastersProject/MSc12/Srisuchat/Thesis.pdf) as a resource, which has been quite helpful in implementing acceleration mechanics. However, I've encountered some confusion and challenges regarding the calculation and handling of RPM changes and their effects on acceleration.
The main questions I have are:
- RPM Handling: How should RPM increase and decrease be calculated in a vehicle physics simulation? What factors should be considered to make the acceleration feel realistic?
- Torque vs. BHP Curves: Is it more effective to define a torque curve as suggested in the thesis, or should I derive torque from a BHP curve instead? I understand that both approaches have merit, but I'm concerned about their realism in comparison to real-world vehicles.
- Simulation vs. Approximation: In racing simulations, do developers typically use a full vehicle dynamics simulation, or do they rely on approximation methods (like torque curves)
My code is very minimal, opens an SDL2 window so I could handle input events. You can press W to accelerate the vehicle which will print: RPM, Torque, Speed and Position.
#include <iostream>
#include <cmath>
#include "SDL2/SDL.h"
#include <cmath>
#include <limits.h>
#include <algorithm>
#include <fstream>
const int SCREEN_WIDTH = 600;
const int SCREEN_HEIGHT = 400;
bool initSDL(SDL_Window** window, SDL_Renderer** renderer) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cout << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
return false;
}
*window = SDL_CreateWindow("2D Car Simulation",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH,
SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
if (!*window) {
std::cout << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
SDL_Quit();
return false;
}
*renderer = SDL_CreateRenderer(*window, -1, SDL_RENDERER_ACCELERATED);
if (!*renderer) {
std::cout << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
SDL_DestroyWindow(*window); // Clean up the window before returning
SDL_Quit(); // Clean up SDL before returning
return false;
}
SDL_SetRenderDrawColor(*renderer, 0, 0, 0, 255); // Use 255 for alpha to make it fully opaque
return true;
}
// Function to clean up SDL resources
void closeSDL(SDL_Window* window, SDL_Renderer* renderer) {
// Destroy the renderer if it was created
if (renderer) {
SDL_DestroyRenderer(renderer);
}
// Destroy the window
if (window) {
SDL_DestroyWindow(window);
}
// Quit SDL subsystems
SDL_Quit();
}
class Vec3 {
public:
double x, y, z;
Vec3(double x = 0.0, double y = 0.0, double z = 0.0) : x(x), y(y), z(z) {}
Vec3 operator+(const Vec3& v) const {
return Vec3(x + v.x, y + v.y, z + v.z);
}
Vec3 operator-(const Vec3& v) const {
return Vec3(x - v.x, y - v.y, z - v.z);
}
Vec3 operator*(double scalar) const {
return Vec3(x * scalar, y * scalar, z * scalar);
}
friend Vec3 operator*(double scalar, const Vec3& vec) {
return Vec3(vec.x * scalar, vec.y * scalar, vec.z * scalar);
}
Vec3 operator/(double scalar) const {
if (scalar != 0.0)
return Vec3(x / scalar, y / scalar, z / scalar);
return Vec3(0, 0, 0); // Handle divide by zero case.
}
Vec3 operator/(const Vec3& vec) const {
return Vec3((vec.x != 0) ? x / vec.x : 0,
(vec.y != 0) ? y / vec.y : 0,
(vec.z != 0) ? z / vec.z : 0);
}
friend Vec3 operator/(double scalar, const Vec3& vec) {
return Vec3(scalar / vec.x, scalar / vec.y, scalar / vec.z);
}
double magnitude() const {
return std::sqrt(x * x + y * y + z * z);
}
Vec3 normalize() const {
double mag = magnitude();
return mag > 0 ? *this * (1.0 / mag) : Vec3();
}
// Output the vector for display
void print() const {
std::cout << "(" << x << ", " << y << ", " << z << ")";
}
};
class Car
{
public:
Vec3 position;
Vec3 velocity;
int currentGear;
double bhp;
int gears;
std::vector<double> gearRatios;
double rpm;
double enginePowerWatts;
Vec3 engineAngularVelocity;
Vec3 engineTorque;
const float airDensity = 1.225f;
const double dragCoefficient = 0.3; // 5.0f
const float frontalArea = 2.2f;
const float rollingResistanceCoefficient = 0.015; // 30.0f
const float gravity = 9.81f;
const float carMass = 1650.0f;
const float C_braking = 5000.0f;
double wheelRadius = 0.5; // (m)
double speed_MPH;
double throttle;
bool braking = false;
Vec3 wheelAngularVelocity;
double currentBHP;
std::ofstream dataFile;
public:
Car(double bhp, int numGears, std::vector<double>& gearRatios) : bhp{bhp}, gears{numGears}, gearRatios{gearRatios}, currentGear(0), rpm(600), throttle(0.0), dataFile("output.txt")
{
position = {0.0f, 0.0f, 0.0f};
velocity = {0.0f, 0.0f, 0.0f};
}
// Vec3 calculateDragForce(Vec3& velocity, double speed) {
// double C_drag = dragCoefficient * velocity * velocity.magnitude();
// // C_drag = C_drag * speed * speed;
// return {0.0, 0.0, C_drag};
// }
~Car()
{
if(dataFile.is_open())
{
dataFile.close();
}
}
void applyBraking(double deltaTime)
{
Vec3 direction = velocity.normalize();
Vec3 brakingForce = direction * (-C_braking);
// Update velocity based on braking force
Vec3 accelerationDueToBraking = brakingForce * (1.0 / carMass); // F = ma, a = F/m
// std::cout << "Braking acceleration: " << accelerationDueToBraking.x << ", " << accelerationDueToBraking.y << ", " << accelerationDueToBraking.z << std::endl;
velocity = velocity + accelerationDueToBraking * deltaTime;
// Prevent the velocity from going negative
if (velocity.magnitude() < 0.1) {
velocity = Vec3(0, 0, 0); // Car has come to a stop
}
// Update position based on the new velocity
position = position + velocity * deltaTime;
}
Vec3 calculateDragForce(const Vec3& velocity) {
double speed = velocity.magnitude(); // |V|
double drag = 0.5f * airDensity * -dragCoefficient * frontalArea * speed * speed;
//Vec3 dragForce = -dragCoefficient * velocity * speed;
return {0.0, 0.0, drag};
}
Vec3 calculateRollingResistanceForce(const Vec3& velocity) {
return -rollingResistanceCoefficient * velocity; // Calculating rolling resistance based on mass
}
Vec3 calculateTractionForce(const Vec3& wheelTorque) {
return {0.0, 0.0, wheelTorque.x / wheelRadius};
}
// using this now but torque no longer has a drop off at max RPM
double getBHP(double rpm)
{
double idle = 600.0, low = 2000.0, mid = 4000.0, high = 6000.0, max = 7000.0;
double idle_bhp = 1;
double low_bhp = 100;
double mid_bhp = 240;
double high_bhp = 380;
double max_bhp = 480;
double bhp;
if(rpm <= idle)
{
bhp = idle_bhp;
}
else if(rpm > idle && rpm <= low)
{
bhp = idle_bhp + (low_bhp - idle_bhp) * ((rpm - idle) / (low - idle));
}
else if(rpm > mid && rpm <= high)
{
bhp = mid_bhp + (high_bhp - mid_bhp) * ((rpm - mid) / (high - mid));
}
else if(rpm > high && rpm <= max)
{
bhp = high_bhp + (max_bhp - high_bhp) * ((rpm - high) / (max - high));
}
else
{
bhp = max_bhp;
}
return bhp;
}
double getTorque(double rpm, double throttle) {
double rpm_idle = 600.0;
double torque_idle = 40;
double rpm_low = 1000;
double torque_low = 200;
double rpm_mid = 1500;
double torque_mid = 330;
double rpm_high = 5000;
double torque_high = 450;
double rpm_max = 7000;
double torque_max = 80;
double torque;
// piecewise linear approximation to find the torque for the current rpm
if (rpm <= rpm_idle) {
torque = torque_idle;
} else if (rpm > rpm_idle && rpm <= rpm_low) {
torque = torque_idle + (torque_low - torque_idle) * ((rpm - rpm_idle) / (rpm_low - rpm_idle));
} else if (rpm > rpm_low && rpm <= rpm_mid) {
torque = torque_low + (torque_mid - torque_low) * ((rpm - rpm_low) / (rpm_mid - rpm_low));
} else if (rpm > rpm_mid && rpm <= rpm_high) {
torque = torque_mid + (torque_high - torque_mid) * ((rpm - rpm_mid) / (rpm_high - rpm_mid));
} else if (rpm > rpm_high && rpm <= rpm_max) {
torque = torque_high + (torque_max - torque_high) * ((rpm - rpm_high) / (rpm_max - rpm_high));
} else {
torque = torque_max;
}
return torque * throttle;
}
void update(double deltaTime)
{
// convert engine bhp to watts
enginePowerWatts = getBHP(rpm) * 745.7; // 99923.8
// calc the engines angular velocity
double angularVelo = (2 * M_PI * rpm) / 60.0; // 104.719
Vec3 engineAngularVelocity = {angularVelo, 0.0, 0.0};
// use engine angular vel to get engine torque : T_e = P / W_e
//engineTorque = enginePowerWatts / engineAngularVelocity;
//engineTorque = Vec3(enginePowerWatts / engineAngularVelocity);
// Completely ignoring using BHP to calculate torque. Just using the rpm and throttle
// to find the torque at current RPM (paper suggests using some kind of straight line graph)
engineTorque = {getTorque(rpm, throttle), 0.0, 0.0};
// std::cout << "RPM: " << rpm << ", Watt: " << enginePowerWatts << ", Angular Velo rad/s: " << angularVelo << ", Torque: " << engineTorque.x << ", " << engineTorque.y << ", " << engineTorque.z << std::endl;
double speed = velocity.magnitude();
Vec3 F_drag = calculateDragForce(velocity);
Vec3 F_rr = calculateRollingResistanceForce(velocity);
Vec3 F_long = {0.0, 0.0, 0.0};
// calculate the torque at the wheels
// T_w = T_e * g_k * G : T_w = wheel torque, T_e = engine torque, g_k = gear ratio, G = final drive ratio
// calculate final drive ratio -> this is how much the differential rotates to get 1 turn of the wheel
double outputRPM = rpm / gearRatios[currentGear];
double inputRPM = outputRPM * gearRatios[currentGear];
double finalDriveRatio = inputRPM / outputRPM;
//std::cout << "Current gear: " << currentGear + 1 << ", gear ratio: " << gearRatios[currentGear] << ", Final drive ratio: " << finalDriveRatio << std::endl;
// Wheel torque
Vec3 wheelTorque = engineTorque * gearRatios[currentGear] * finalDriveRatio;
//std::cout << "Wheel torque: " << wheelTorque.x << ", " << wheelTorque.y << ", " << wheelTorque.z << std::endl;
// wheel angular velocity = W_w = 2 * PI * omega_e / (60 * g_k * G) in rad/s
double wheelAngularVeloc = (2 * M_PI * rpm) / (60.0 * gearRatios[currentGear] * finalDriveRatio);
wheelAngularVelocity = {0.0, 0.0, wheelAngularVeloc};
// linear velocity -> speed of vehicle in m/s
// calc translational velocity
Vec3 V = wheelAngularVelocity * wheelRadius;
Vec3 F_traction = calculateTractionForce(wheelTorque);
// net force on the car
F_long = F_traction + (F_drag + F_rr);
Vec3 acceleration = {F_long.x / carMass, F_long.y / carMass, F_long.z / carMass};
// Debug statements to check force values
//std::cout << "F_drag: " << F_drag.x << ", " << F_drag.y << ", " << F_drag.z << std::endl;
//std::cout << "F_rr: " << F_rr.x << ", " << F_rr.y << ", " << F_rr.z << std::endl;
//std::cout << "F_traction: " << F_traction.x << ", " << F_traction.y << ", " << F_traction.z << std::endl;
//std::cout << "F_long: " << F_long.x << ", " << F_long.y << ", " << F_long.z << std::endl;
if(braking)
{
applyBraking(deltaTime);
} else {
velocity = velocity + acceleration * deltaTime;
}
position = position + velocity * deltaTime;
double throttleEffect = throttle * 1000.0;
if (throttle > 0.0) {
rpm += throttleEffect * deltaTime;
} else {
// Decrease RPM when throttle is off
double engineBrakingEffect = 500.0;
rpm -= engineBrakingEffect * deltaTime;
}
// Ensure RPM stays within limits
if (rpm >= 7000.0) {
rpm = 7000.0;
// shift gear and reset rpm to 1000 maybe?
// currentGear = currentGear + 1 > gearRatios.size() - 1 ? currentGear : currentGear + 1;
// rpm = 1000.0;
} else if (rpm < 600.0) { // idle rpm
rpm = 600.0;
}
// Calculate speed in m/s
double speedMps = velocity.magnitude(); // speed of the car in m/s
// Convert speed to mph
speed_MPH = speedMps * 2.23694;
std::cout << "T: " << throttle << ", RPM: " << rpm << ", Torque: " << engineTorque.x << ", " << engineTorque.y << ", " << engineTorque.z << ", Speed: " << speed_MPH << ", Position: " << position.x << ", " << position.y << ", " << position.z << std::endl;
// dataFile << rpm << ", " << speed_MPH << ", " << F_drag.z << ", " << F_rr.z << ", " << engineTorque.x << ", " << (float)getBHP(rpm) << std::endl;
}
};
int main() {
SDL_Window* window = nullptr;
SDL_Renderer* renderer = nullptr;
// Initialize SDL
if (!initSDL(&window, &renderer)) {
return -1;
}
// Set clear color for rendering
SDL_SetRenderDrawColor(renderer, 128, 179, 255, 255); // Light blue color (R, G, B, A)
bool quit = false;
SDL_Event e;
Uint32 lastTime = SDL_GetTicks();
int gears = 5;
std::vector<double> gearRatios = {3.1, 1.9, 1.3, 1.0, 0.8};
constexpr double bhp = 450;
Car car(bhp, gears, gearRatios);
while (!quit) {
Uint32 currentTime = SDL_GetTicks();
float dt = (currentTime - lastTime) / 1000.0f;
lastTime = currentTime;
// Handle events
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
quit = true;
}
}
// Check for throttle and braking input
const Uint8* currentKeyStates = SDL_GetKeyboardState(NULL);
car.throttle = currentKeyStates[SDL_SCANCODE_W] ? 1.0 : 0.0;
car.braking = currentKeyStates[SDL_SCANCODE_S] ? true : false;
// Update car state
car.update(dt);
// Clear the screen
SDL_SetRenderDrawColor(renderer, 128, 179, 255, 255); // Reset to clear color
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
// Clean up resources
closeSDL(window, renderer);
return 0;
}