add basic engine simulation + working gauge

This commit is contained in:
Aidan Haas 2025-06-13 15:34:38 -04:00
parent 17fbfb3426
commit 2ac3bf166b
3 changed files with 160 additions and 48 deletions

Binary file not shown.

145
engine.py
View File

@ -1,50 +1,127 @@
import math
class Engine:
rpm = 0
throttle = 0
load = 0
instant_torque = 0
gear = 0
flywheel_inertia = 0
idle_rpm = 750
max_rpm = 6500
def __init__(self,
idle_rpm=750,
max_rpm=7000,
redline_rpm=6500,
flywheel_inertia=0.025,
fuel_efficiency=0.3,
torque_curve=None):
# General stuff
self.rpm = idle_rpm
self.throttle = 0.0
self.load = 0.0
self.gear = None
self.ignition = True
self.revCut = False
fuel_rate = 0
afr = 0
coolant_temp = 0
oil_temp = 0
self.idle_rpm = idle_rpm
self.max_rpm = max_rpm
self.redline_rpm = redline_rpm
self.flywheel_inertia = flywheel_inertia
self.fuel_efficiency = fuel_efficiency
self.torque_curve = torque_curve or self.default_torque_curve
self.instant_torque = 0
self.wheel_speed = 0
self.engineFriction = 5
self.peak_torque = 163 # Nm -> 120 ft/lb
self.engine_brake_strength = 40 # Nm
self.engine_brake_torque = 0
self.starting = False
ignition_timing = 0
# Fluids
self.oil_temp = 0
self.oil_pressure = 0
self.oil_capacity = 0
ambient_temp = 0
altitude = 0
intake_pressure = 0
vehicle_mass = 0
drivetrain_loss = 0
self.coolant_temp = 0
self.coolant_pressure = 0
self.coolant_capacity = 0
oil_pressure = 0
turbo_boost = 0
kr = 0
exhaust_temp = 0
self.fuel_capacity = 0
def start(self):
rpm = 250
# Fuel
self.fuel_rate = 0
self.afr = 0
# Timing stuff
self.kr = 0
self.timing_advance = 0
self.ignition_timing = 0
# Exhaust stuff
self.exhaust_temp = 0
self.turbo_boost = 0
# Environment
self.ambient_temp = 0
self.altitude = 0
self.intake_pressure = 0
self.vehicle_mass = 0
self.drivetrain_loss = 0
# Just a parabolic curve for now
def default_torque_curve(self, rpm):
rpm_ratio = rpm / self.max_rpm
return max(0.0, -4 * (rpm_ratio - 0.5) ** 2 + 1)
def start(self, dt):
self.ignition = True
self.starting = True
def update(self, throttle, load, dt):
if not self.running:
return
self.throttle = throttle
self.load = load
# Apply simplified engine RPM dynamics
rpm_change = (throttle - load) * self.torque_curve(self.rpm) / self.flywheel_inertia
self.rpm += rpm_change * dt
self.rpm = max(self.idle_rpm, min(self.rpm, self.max_rpm))
if self.starting:
available_torque = self.torque_curve(self.rpm)
self.instant_torque = (available_torque * self.peak_torque) + 10
rpm_change = (self.instant_torque / self.flywheel_inertia)
self.rpm += rpm_change * dt
if self.rpm > 2000:
self.starting = False
throttle = 0
# Idle -- Eventually should have learning alg to find the % throttle needed
if (self.rpm < self.idle_rpm):
self.throttle = 0.1
# Rev limit
if (self.rpm > self.redline_rpm):
self.revCut = True
if self.revCut and self.rpm > self.redline_rpm - 100:
self.throttle = 0
else:
self.revCut = False
# Engine friction
if (self.rpm > 0):
friction_force = self.engineFriction
else:
friction_force = 0
# Engine braking torque (If throttle closed)
if self.throttle < 0.1 and self.rpm > self.idle_rpm + 200 or not self.ignition:
engine_brake_torque = self.engine_brake_strength * (self.rpm / self.max_rpm)
else:
engine_brake_torque = 0
# Calculate torque
if self.ignition:
available_torque = self.torque_curve(self.rpm)
else:
available_torque = 0
self.instant_torque = (self.throttle * available_torque * self.peak_torque) - engine_brake_torque - friction_force
rpm_change = (self.instant_torque / self.flywheel_inertia)
self.rpm += rpm_change * dt
self.torque = self.torque_curve(self.rpm) * throttle
self.fuel_rate = self.rpm * throttle * self.fuel_efficiency
self._update_temps(dt)
#self._update_temps(dt)

63
main.py
View File

@ -12,6 +12,12 @@ dt = 0
player_pos = pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)
# Text Setup
pygame.font.init()
font = pygame.font.SysFont(None, 64)
# Gauge Setup
rpm_pos = pygame.Vector2(screen.get_width() * 0.25, screen.get_height() / 2)
spd_pos = pygame.Vector2(screen.get_width() * 0.75, screen.get_height() / 2)
@ -20,7 +26,7 @@ gauge_radius = screen.get_height() / 5
# Angle for gauge
angle_rad = math.radians(145)
needle_color = "black"
needle_color = "white"
needle_width = 4
tip_radius = needle_width
@ -29,29 +35,50 @@ offset_vector = pygame.Vector2(
math.sin(angle_rad) * gauge_radius
)
e = engine()
e = Engine()
throttle = 0
def map_value_to_angle(value, min_val, max_val):
clamped = max(min_val, min(value, max_val)) # Can change this later
return math.radians(150 + 270 * (clamped - min_val) / (max_val - min_val))
while running:
mX, mY = pygame.mouse.get_pos()
# poll for events
# pygame.QUIT event means the user clicked X to close your window
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if (pygame.key.name(event.key) == 'v'):
if e.ignition:
e.ignition = False
else:
e.start(dt)
if event.type == pygame.QUIT:
running = False
# fill the screen with a color to wipe away anything from last frame
screen.fill("white")
# RPM
# Min = 0 at 45 degrees
# Max = 8 at 315 degrees
pygame.draw.circle(screen, "red", rpm_pos, gauge_radius)
rpm_tip = rpm_pos + offset_vector
pygame.draw.line(screen, needle_color, rpm_pos, rpm_tip, needle_width)
pygame.draw.circle(screen, needle_color, rpm_tip, tip_radius)
rpm_text = font.render(f"RPM: {int(e.rpm)}", True, (0, 0, 0))
rpm_rect = rpm_text.get_rect(center=(screen.get_width() // 2, screen.get_height() // 4))
screen.blit(rpm_text, rpm_rect)
# Trans Speed
# Min = 0 at 45 degrees
# Max = 160 at 315 degrees
rpm_text = font.render(f"Torque: {int(e.instant_torque)}", True, (0, 0, 0))
rpm_rect = rpm_text.get_rect(center=(screen.get_width() // 2, screen.get_height() // 4 +100))
screen.blit(rpm_text, rpm_rect)
rpm_angle = map_value_to_angle(e.rpm, 0, 8000)
# RPM Gauge
speed_angle = map_value_to_angle(e.wheel_speed, 0, 160)
rpm_vector = pygame.Vector2(math.cos(rpm_angle), math.sin(rpm_angle)) * gauge_radius
pygame.draw.circle(screen, "black", rpm_pos, gauge_radius)
pygame.draw.line(screen, needle_color, rpm_pos, rpm_pos + rpm_vector, needle_width)
# Trans Gauge
speed_vector = pygame.Vector2(math.cos(speed_angle), math.sin(speed_angle)) * gauge_radius
pygame.draw.circle(screen, "red", spd_pos, gauge_radius)
spd_tip = spd_pos + offset_vector
pygame.draw.line(screen, needle_color, spd_pos, spd_tip, needle_width)
@ -61,13 +88,20 @@ while running:
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
player_pos.y -= 300 * dt
e.rpm += 600 * dt
if keys[pygame.K_s]:
player_pos.y += 300 * dt
e.rpm -= 600 * dt
if keys[pygame.K_a]:
player_pos.x -= 300 * dt
if keys[pygame.K_d]:
player_pos.x += 300 * dt
if keys[pygame.K_r]:
e.rpm = e.idle_rpm
# Throttle (Using mouse pos)
throttle = 1 - (mY / screen.get_height())
#print(throttle)
# flip() the display to put your work on screen
pygame.display.flip()
@ -76,5 +110,6 @@ while running:
# dt is delta time in seconds since last frame, used for framerate-
# independent physics.
dt = clock.tick(60) / 1000
e.update(throttle, 0, dt)
pygame.quit()