From 2ac3bf166b09b3a7dc3dbf672f0ea4e4dc6aed41 Mon Sep 17 00:00:00 2001 From: Aidan Haas Date: Fri, 13 Jun 2025 15:34:38 -0400 Subject: [PATCH] add basic engine simulation + working gauge --- __pycache__/engine.cpython-313.pyc | Bin 0 -> 4582 bytes engine.py | 145 ++++++++++++++++++++++------- main.py | 63 ++++++++++--- 3 files changed, 160 insertions(+), 48 deletions(-) create mode 100644 __pycache__/engine.cpython-313.pyc diff --git a/__pycache__/engine.cpython-313.pyc b/__pycache__/engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b4582f6fd3fe15cf4a4e2ae519c0862671da87f GIT binary patch literal 4582 zcmc&&PfQ!x8Gkmm8NgtY5MqptjZI*^g}^S^lxCqM4Wx9FB`YRrky>dq*n^p7Y?C)* znnX&wmsTW8L{{3U&`VPF(Cn${soUH3)btcar1I#s2TMIvT&gH()k~%H``(PlOhhWR zmp-ib-tYbSzVH3}zTfb&y}gw{>b!Mv<;NX_`~^Wh1)R#u6jbgLk%+zvgpw#p6VV?d zV$*wmdj$0reA&Prg87`?Wamylc{vIAKKUM@K9Nwr=%Y=2r12*TVpEKK-$w&tAVz3V z3_>0fLy$L%%~UA(vn?z%TPmm}Wv_{AY2K#>wP5SZccFHlEOu&cr|7#vG+!NZRJRG% z1ARD;C(#e6zG7G#pkTcZB!=92N^^BDj%kA1enx9?>mO-+R}o(cP5It(I`UycQlNFA zyFRi}fU2>vphGS~DTiHzD>~vL?CbKNqaNg1$Za|1 z>5F<$j|as(sMmwy9+dE)J`YNIP|Ab)Jt*x#84nuppyM7iSVskt73Tk1N0-l2ydQgb z^n1U+49_k*#G2@8kqO2MEgOcWuz*&U^DIzMWXc4!P*M%GTw)=rtevYE(2~~_31>7H zN& zSD|YP3wuz9TG9=xnA zTihA#eDN-9;gHIP!kXk|%KTR;>oC+Jm>|jdHMx{iSfsw^lHI|=%JmhwqQhRG);6O; zm&(#oxvU#3EEktxQ=KkB)(q9CE?@3@WQ7Nh|Am zwwVQVMO&u$z$m^eiqDDSE28*#D83RMokPk_?CsH&a#0zT)x2C9ohj!kMKD7jwRd?$ zSBp;d#Oe(uNN^ccLy}lzmY>{-dILHoeH5J7$K?58(iPtjtZ46gV58mYItE3H)fMGM zG|n5lv9GNbO~HU;eH&q`ckr1IwNhixgoKqm@l1$Y@wck+*+($)w(e-z>`Vc$s3b@u?!iSs}) z4;2g)3sAnEM%oA|`M=u;3_b`Aez9o;T)G$-0f#>V-a!nGz{Gk093a`yJjJ8m``4>i zuL`!z+VmcGCK!LRetuA%Lmt8IEs1j}t9mh^|z^vD~3I{oZ zb1*2Z^Wi$HK-N?j_FhNhj z6wj`pjeJ*9P=PRyAroqL@&-_Fmj3z6uoetJ6n!?-aV^ZBH*^%_n9qXTYp}4h0FJ&1 z{W=CKu-Lh*Sln&5ZNhtAPgv(R-GQ1nq%Wo#0Ubi={#nlKdbpKuLii;Mcr33aXoYip zJ-vqq1*sHRJhR{iaJvn2e(pXzci1795P&QLF4I>cv zb$^439BixTKm8dYGEfdqRoq)H21;;#2T6xkmnbwWIt`9Yh8+@-R=CY?lmMe&Puvca zfK-e0^LKoExVkuFy-(dfV~w3yep_T4>1E~y2Qy9OzJz5AL1ii`qUWlMvX3= zYK+DEg?Fgb%vS?+@IUF4K?02UAKnL-4ixgBy}A-W3!u+Ek$v`I4?oK*Aw1Fl|2w+k zbF6g<=xf&Qqu1d5TITm~S?UgKnoSi8KikIi+v&@DdFa`D0$KJDMVCO~w%J~iQv>er zDNYfftD4`Pm7tL_8md@;ila8!|HqH#kYsnOBUoDw=aBKV4&git6oc?m4`i%t>e#wQMZ>wgX zx})7R`)(pF>b1IK)$Xj>okbRR)`Va8+4WXS2yY5^gnJ)+ddUp2G@w}Xe=>>vfVLnyKcioS{tV3xrwjoCYx@jJ<{Bi zU%Z_Z&q+X85gVzlz2)KxV}0h5e4Q2$5aWVa&!b z!_UraNc|f`K6Mcum70eZ=P_pss_itzRlQ;kX literal 0 HcmV?d00001 diff --git a/engine.py b/engine.py index a2f8ddb..93b5607 100644 --- a/engine.py +++ b/engine.py @@ -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) diff --git a/main.py b/main.py index 789738d..c4c906f 100644 --- a/main.py +++ b/main.py @@ -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() \ No newline at end of file