Add project files.

This commit is contained in:
max
2025-12-18 01:04:21 +01:00
parent 532b1f4c53
commit c22452c66c
11 changed files with 1138 additions and 0 deletions

3
Car simulation.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="Car simulation/Car simulation.csproj" />
</Solution>

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Car_simulation</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SFML.Net" Version="2.6.1" />
</ItemGroup>
</Project>

221
Car simulation/Car.cs Normal file
View File

@@ -0,0 +1,221 @@
using static SFML.Window.Mouse;
namespace Car_simulation
{
public class Car
{
public Vector2 Position = new Vector2(0, 0);
public Vector2 Velocity = new Vector2(0, 0);
public float Speed => Velocity.Length;
public float Mass = 1500f; // kg
public int WheelCount = 4;
public int DrivenWheels = 2;
public float ThrottleInput = 0f;
public float BrakeInput = 0f;
public float ClutchInput = 1f; // 0 = engaged, 1 = disengaged
public bool ForceClutch = false;
public float SteeringInput = 0f;
// Aerodynamics
private const float AirDensity = 1.225f;
public float DragCoefficient = 0.1f;
public float FrontalArea = 2.2f; // m²
public float RollingResistanceCoefficient = 0.015f;
// Components
public Engine Engine;
public Drivetrain Drivetrain;
public WheelSystem WheelSystem;
private EngineSound _engineSound;
private bool _audioEnabled = true;
public Car()
{
Engine = new Engine();
WheelSystem = new WheelSystem();
Drivetrain = new Drivetrain(Engine, WheelSystem);
// Initial setup
WheelSystem.WheelCount = WheelCount;
WheelSystem.DrivenWheels = DrivenWheels;
InitializeAudio();
}
private void InitializeAudio()
{
try
{
_engineSound = new EngineSound();
_engineSound.SetEngineState(Engine.IdleRPM, 0f);
_engineSound.StartSound();
}
catch (Exception ex)
{
Console.WriteLine($"Audio initialization failed: {ex.Message}");
_audioEnabled = false;
}
}
public void Update(float deltaTime)
{
Engine.Throttle = ThrottleInput;
Drivetrain.ClutchEngagement = 1f - ClutchInput; // Convert: 0 input = 1 engagement
if (ForceClutch)
Drivetrain.ClutchEngagement = 0f;
float resistanceTorque = CalculateResistanceTorque();
WheelSystem.ResistanceTorque = resistanceTorque;
Drivetrain.Update(deltaTime);
WheelSystem.ApplyResistance(deltaTime);
float engineLoad = Drivetrain.CalculateEngineLoad(deltaTime);
Engine.Update(deltaTime, engineLoad);
UpdateVehicleMotion(deltaTime);
ApplyBraking(deltaTime);
if (_audioEnabled)
{
UpdateAudio();
}
}
private void UpdateAudio()
{
try
{
float throttle = Engine.GetActualThrottle();
_engineSound.SetEngineState(Engine.RPM, throttle);
}
catch (Exception ex)
{
Console.WriteLine($"Audio update error: {ex.Message}");
}
}
private void UpdateVehicleMotion(float deltaTime)
{
// Calculate net force
float tractiveForce = CalculateTractiveForce();
float resistanceForce = CalculateTotalResistanceForce();
float netForce = tractiveForce - resistanceForce;
// Calculate acceleration: a = F / m
float acceleration = netForce / Mass;
// Update velocity: v = v₀ + a·Δt
if (Velocity.Length > 0)
{
Vector2 direction = Velocity.Normalized();
float newSpeed = Velocity.Length + acceleration * deltaTime;
newSpeed = Math.Max(newSpeed, 0); // Don't go backwards without reverse gear
Velocity = direction * newSpeed;
}
else
{
// Starting from standstill
Velocity = new Vector2(acceleration * deltaTime, 0);
}
Position += Velocity * deltaTime;
// Sync wheel speed with actual vehicle speed (with slip allowance)
float currentWheelSpeed = Velocity.Length;
WheelSystem.SetSpeed(currentWheelSpeed);
}
private float CalculateTractiveForce()
{
// 1. Get the torque available at the wheels
float wheelTorque = Drivetrain.ClutchTorque * Drivetrain.Efficiency;
// 2. Convert torque to theoretical force: F = τ / r
float theoreticalForce = wheelTorque / WheelSystem.Radius;
// 3. Account for weight distribution and driven wheels
// Normal load on driven wheels = (DrivenWheels / WheelCount) * Weight
float drivenWheelNormalLoad = (DrivenWheels / (float)WheelCount) * Mass * 9.81f;
// 4. Calculate maximum tractive force based on friction (tire grip)
float frictionCoefficient = 1.2f; // Typical tire on dry asphalt
float maxTractiveForce = drivenWheelNormalLoad * frictionCoefficient;
// 5. Limit the force by what the tires can actually grip
// Also handle direction (forward/reverse)
if (theoreticalForce > 0)
{
return Math.Min(theoreticalForce, maxTractiveForce);
}
else
{
// For reverse or engine braking
return Math.Max(theoreticalForce, -maxTractiveForce);
}
}
private void ApplyBraking(float deltaTime)
{
if (BrakeInput <= 0) return;
float brakeTorque = BrakeInput * 500f; // 500 Nm max brake torque
WheelSystem.ApplyTorque(-brakeTorque, deltaTime);
}
public float CalculateTotalResistanceForce()
{
float dragForce = CalculateDragForce();
float rollingForce = CalculateRollingResistanceForce();
return dragForce + rollingForce;
}
private float CalculateDragForce()
{
// F_drag = 0.5 * ρ * Cd * A * v²
float speed = Speed;
return 0.5f * AirDensity * DragCoefficient * FrontalArea * speed * speed;
}
private float CalculateRollingResistanceForce()
{
// F_rolling = C_r * m * g
return RollingResistanceCoefficient * Mass * 9.81f;
}
// Convert resistance force to wheel torque
public float CalculateResistanceTorque()
{
float totalForce = CalculateTotalResistanceForce();
return totalForce * WheelSystem.Radius;
}
public void DisplayUpdate()
{
Console.SetCursorPosition(0, 0);
Console.WriteLine($"Engine Energy: {Engine.FlywheelEnergy,7:F0} J");
Console.WriteLine($"Engine Torque: {Engine.GetTorqueOutput(),7:F0} Nm");
Console.WriteLine($"Engine RPM: {Engine.RPM,7:F0}");
Console.WriteLine($"Wheel Energy: {WheelSystem.WheelEnergy,7:F0} J");
Console.WriteLine($"Wheel RPM: {WheelSystem.RPM,7:F0}");
Console.WriteLine($"Vehicle: {Speed * 3.6f,7:F1} km/h");
Console.WriteLine($"Throttle: {Engine.GetActualThrottle() * 100,6:F1}%");
Console.WriteLine($"Power: {Engine.CurrentPower / 1000,6:F1} kW");
Console.WriteLine($"Transmitted: {Drivetrain.TransmittedPower / 1000,6:F1} kW");
Console.WriteLine($"Brake: {BrakeInput * 100,6:F1}%");
Console.WriteLine($"Clutch: {ClutchInput * 100,6:F1}% disengaged");
Console.WriteLine($"Speed Diff: {Drivetrain.GetSpeedDifferenceRPM(),6:F0} RPM");
Console.WriteLine($"Clutch T: {Drivetrain.ClutchTorque,6:F0} Nm");
Console.WriteLine($"Resistance: {CalculateTotalResistanceForce(),6:F1} N");
Console.WriteLine($"Drag: {CalculateDragForce(),6:F1} N");
Console.WriteLine($"Rolling: {CalculateRollingResistanceForce(),6:F1} N");
Console.WriteLine($"Gear: {Drivetrain.GetCurrentGearName(),3} (Ratio: {Drivetrain.GearRatio:F2}:1)");
}
}
}

View File

@@ -0,0 +1,206 @@
namespace Car_simulation
{
public class Drivetrain
{
// Connected components
public Engine Engine { get; private set; }
public WheelSystem WheelSystem { get; private set; }
private int currentGear = 1;
public float[] GearRatios { get; set; } =
{
3.8f, // 1st - Lower for better launch
2.5f, // 2nd
1.8f, // 3rd
1.3f, // 4th
1.0f, // 5th - Direct drive
0.8f, // 6th - Overdrive
0.65f // 7th - Double overdrive (optional)
};
public float FinalDriveRatio { get; set; } = 5.0f;
public float Efficiency { get; set; } = 0.95f;
public float ClutchEngagement { get; set; } = 0f; // 0 = disengaged, 1 = fully engaged
// Calculated
public float GearRatio => GetCurrentGearRatio();
public float TotalRatio => GearRatio * FinalDriveRatio;
// Clutch properties
public float ClutchStiffness { get; set; } = 500f; // Nm/(rad/s) - how strongly clutch pulls speeds together
public float MaxClutchTorque { get; set; } = 4500f; // Maximum torque clutch can transmit
// State
public float SpeedDifference { get; private set; } // rad/s
public float ClutchTorque { get; private set; }
public float TransmittedPower { get; private set; }
private float previousWheelOmega = 0f;
public Drivetrain(Engine engine, WheelSystem wheelSystem)
{
Engine = engine;
WheelSystem = wheelSystem;
previousWheelOmega = wheelSystem.AngularVelocity;
}
public void GearUp()
{
if (currentGear < GearRatios.Length)
currentGear++;
}
public void GearDown()
{
if (currentGear > 1)
currentGear--;
}
private float GetCurrentGearRatio()
{
if (currentGear == 0) return 0f; // Neutral
if (currentGear == -1) return -3.5f; // Reverse (example ratio)
if (currentGear > 0 && currentGear <= GearRatios.Length)
return GearRatios[currentGear - 1];
return 0f; // Invalid gear
}
public float CalculateSpeedDifference()
{
if (TotalRatio == 0) return 0f;
float engineOmega = Engine.AngularVelocity;
float wheelOmega = WheelSystem.AngularVelocity;
float expectedWheelOmega = engineOmega / TotalRatio;
SpeedDifference = wheelOmega - expectedWheelOmega;
return SpeedDifference;
}
public float CalculateClutchTorque()
{
if (ClutchEngagement <= 0.01f)
{
ClutchTorque = 0;
return 0f;
}
CalculateSpeedDifference();
float torque = -SpeedDifference * ClutchStiffness * ClutchEngagement;
torque = Math.Clamp(torque, -MaxClutchTorque, MaxClutchTorque);
float actualThrottle = Engine.GetActualThrottle();
float availableEngineTorque = Engine.GetTorqueOutput();
float maxTorqueAtClutch = maxEngineTorque * TotalRatio * Efficiency;
torque = maxTorqueAtClutch;
ClutchTorque = torque;
return torque;
}
public void ApplyDrivetrainWork(float deltaTime)
{
if (ClutchEngagement <= 0.01f || TotalRatio == 0)
{
ClutchTorque = 0;
TransmittedPower = 0;
return;
}
CalculateSpeedDifference();
float clutchTorque = CalculateClutchTorque();
bool engineDrivingWheels = clutchTorque > 0;
bool wheelsDrivingEngine = clutchTorque < 0;
if (engineDrivingWheels)
{
// Engine -> Wheels (normal driving)
ApplyEngineToWheels(clutchTorque, deltaTime);
}
else if (wheelsDrivingEngine)
{
// Wheels -> Engine (engine braking)
ApplyWheelsToEngine(clutchTorque, deltaTime);
}
TransmittedPower = clutchTorque * SpeedDifference;
}
private void ApplyEngineToWheels(float clutchTorque, float deltaTime)
{
// Existing logic for engine driving wheels
float netWheelTorque = clutchTorque * Efficiency - WheelSystem.ResistanceTorque;
float netEngineTorque = -clutchTorque / TotalRatio;
// Apply to both
Engine.ApplyTorque(netEngineTorque, deltaTime);
WheelSystem.ApplyTorque(netWheelTorque, deltaTime);
}
private void ApplyWheelsToEngine(float clutchTorque, float deltaTime)
{
// Wheels driving engine (engine braking)
// Negative clutchTorque means wheels are trying to spin engine faster
float wheelTorque = clutchTorque; // Negative value
float engineTorque = -clutchTorque / TotalRatio; // Positive resistance
// Apply resistance to wheels
WheelSystem.ApplyTorque(wheelTorque, deltaTime);
Engine.ApplyTorque(-engineTorque, deltaTime); // Negative = slowing
}
public float GetEquivalentInertiaAtEngine()
{
float wheelInertia = WheelSystem.GetTotalInertia();
return Engine.MomentOfInertia + (wheelInertia * TotalRatio * TotalRatio);
}
public float CalculateEngineLoad(float deltaTime)
{
if (ClutchEngagement <= 0.01f) return 0f;
float wheelResistanceTorque = WheelSystem.ResistanceTorque;
float engineLoadTorque = wheelResistanceTorque / (TotalRatio * Efficiency);
float inertiaLoad = CalculateInertiaLoad(deltaTime);
return engineLoadTorque + inertiaLoad;
}
private float CalculateInertiaLoad(float deltaTime)
{
float wheelAlpha = (WheelSystem.AngularVelocity - previousWheelOmega) / deltaTime;
previousWheelOmega = WheelSystem.AngularVelocity;
float inertiaTorque = wheelAlpha * WheelSystem.GetTotalInertia();
return inertiaTorque / (TotalRatio * TotalRatio * Efficiency);
}
public void Update(float deltaTime)
{
ApplyDrivetrainWork(deltaTime);
}
// Helper methods
public float GetSpeedDifferenceRPM()
{
return SpeedDifference * PhysicsUtil.RAD_PER_SEC_TO_RPM;
}
public string GetCurrentGearName()
{
return currentGear switch
{
-1 => "R",
0 => "N",
_ => currentGear.ToString()
};
}
}
}

143
Car simulation/Engine.cs Normal file
View File

@@ -0,0 +1,143 @@
namespace Car_simulation
{
public class Engine
{
// Energy state
public float FlywheelEnergy { get; set; } // Joules
// Values
public float RPM => GetRPM();
public float AngularVelocity => GetOmega();
public float CurrentPower { get; private set; }
// Physical properties
public float MomentOfInertia { get; set; } = 0.25f; // kg·m²
public float IdleRPM { get; set; } = 800f;
public float StallSpeed { get; set; } = 200f;
public float Throttle { get; set; } = 0f;
public bool IsRunning => RPM > StallSpeed;
// Torque characteristics
public Dictionary<float, float> TorqueCurve { get; set; } = new()
{
// RPM - Torque Nm
{ 0f, 0f },
{ 800f, 150f }, // Idle
{ 2000f, 200f }, // Peak torque
{ 4500f, 250f },
{ 7200f, 250f },
{ 9200f, 250f },
{ 10000f, 200f },
{ 11000f, 0f }
};
public Engine()
{
// Start with idle energy
FlywheelEnergy = GetEnergyFromRPM(IdleRPM);
}
// Calculations
public float CalculateFrictionEnergy(float deltaTime)
{
// Real friction torque data for 2.0L engine (Nm)
float frictionTorque;
if (RPM < 500) frictionTorque = 15f; // Static/breakaway
else if (RPM < 1000) frictionTorque = 14f;
else if (RPM < 2000) frictionTorque = 16f;
else if (RPM < 3000) frictionTorque = 18f;
else if (RPM < 4000) frictionTorque = 21f;
else if (RPM < 5000) frictionTorque = 25f;
else if (RPM < 6000) frictionTorque = 30f;
else if (RPM < 7000) frictionTorque = 36f;
else frictionTorque = 44f;
float frictionPower = frictionTorque * AngularVelocity;
return frictionPower * deltaTime;
}
private float CalculateCombustionEnergy(float deltaTime)
{
float torque = GetTorqueOutput() * GetActualThrottle();
return torque * AngularVelocity * deltaTime;
}
private float CalculateLoadEnergy(float deltaTime, float loadTorque)
{
return loadTorque * AngularVelocity * deltaTime;
}
// Get
public float GetActualThrottle()
{
float idleThrottle = Math.Max((IdleRPM - RPM) / 10, 0);
return Math.Clamp(Throttle + idleThrottle, 0, 1);
}
public float GetOmega()
{
if (FlywheelEnergy <= 0) return 0;
return MathF.Sqrt(2f * FlywheelEnergy / MomentOfInertia);
}
public float GetRPM()
{
return GetOmega() * PhysicsUtil.RAD_PER_SEC_TO_RPM;
}
// Set
public float GetEnergyFromRPM(float rpm)
{
float omega = rpm * PhysicsUtil.RPM_TO_RAD_PER_SEC;
return 0.5f * MomentOfInertia * omega * omega;
}
// torque curve
public float GetTorqueOutput()
{
if (RPM <= 0) return 0;
var points = TorqueCurve.OrderBy(p => p.Key).ToList();
if (RPM <= points.First().Key) return points.First().Value;
if (RPM >= points.Last().Key) return points.Last().Value;
for (int i = 0; i < points.Count - 1; i++)
{
if (RPM >= points[i].Key && RPM <= points[i + 1].Key)
{
float t = (RPM - points[i].Key) / (points[i + 1].Key - points[i].Key);
return PhysicsUtil.Lerp(points[i].Value, points[i + 1].Value, t);
}
}
return 0f;
}
public void ApplyTorque(float torque, float deltaTime)
{
if (torque == 0) return;
float work = torque * AngularVelocity * deltaTime;
FlywheelEnergy += work;
FlywheelEnergy = Math.Max(FlywheelEnergy, 0);
}
public void Update(float deltaTime, float loadTorque)
{
float combustionEnergy = CalculateCombustionEnergy(deltaTime);
float frictionEnergy = CalculateFrictionEnergy(deltaTime);
float loadEnergy = CalculateLoadEnergy(deltaTime, loadTorque);
float netEnergy = combustionEnergy - frictionEnergy - loadEnergy;
CurrentPower = netEnergy / deltaTime;
FlywheelEnergy += netEnergy;
FlywheelEnergy = Math.Max(FlywheelEnergy, 0);
}
}
}

View File

@@ -0,0 +1,149 @@
using SFML.Audio;
using SFML.System;
using System;
namespace Car_simulation
{
public class EngineSound : SoundStream
{
// Audio properties - smaller buffer for less latency
private const uint SAMPLE_RATE = 44100;
private const ushort CHANNEL_COUNT = 2; // Stereo
private const float BUFFER_DURATION = 0.01f; // 10ms instead of 50ms!
// Engine sound properties - NO SMOOTHING for instant response
private volatile float _currentRPM = 800f; // volatile for thread safety
private volatile float _currentThrottle = 0f;
private float _volume = 0.3f;
private bool _isPlaying = false;
// Harmonic series - DIRECT RPM TO FREQUENCY
private float[] _harmonicRatios = { 1f, 2f, 4f, 6f };
private float[] _harmonicAmplitudes = { 1f, 0.3f, 0.1f, 0.05f };
private float[] _harmonicPhases = new float[4];
// Engine configuration - for direct RPM calculation
public int CylinderCount { get; set; } = 4;
public float FiringFrequencyMultiplier => CylinderCount / 2f; // 4-stroke engines
// For RPM to frequency mapping
private float _rpmToHzFactor;
private Random _random = new Random();
public EngineSound()
{
Initialize(CHANNEL_COUNT, SAMPLE_RATE);
// Calculate direct conversion factor
// RPM to Hz: (RPM / 60) × (Cylinders / 2) for 4-stroke
_rpmToHzFactor = (1f / 60f) * (CylinderCount / 2f);
// Initialize phases
for (int i = 0; i < _harmonicPhases.Length; i++)
{
_harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI);
}
Console.WriteLine($"EngineSound initialized: {BUFFER_DURATION * 1000:F0}ms buffer, {CylinderCount} cylinders");
}
// CALL THIS FROM YOUR PHYSICS THREAD - INSTANT UPDATE
public void SetEngineState(float rpm, float throttle)
{
// NO LOCK, NO SMOOTHING - DIRECT ASSIGNMENT
_currentRPM = rpm;
_currentThrottle = throttle;
// Volume based on throttle (instant)
_volume = 0.1f + 0.4f * throttle;
}
public void StartSound()
{
if (!_isPlaying)
{
Play();
_isPlaying = true;
}
}
public void StopSound()
{
if (_isPlaying)
{
Stop();
_isPlaying = false;
}
}
protected override bool OnGetData(out short[] samples)
{
// SMALLER BUFFER: 10ms instead of 50ms
int sampleCount = (int)(SAMPLE_RATE * BUFFER_DURATION) * 2; // *2 for stereo
samples = new short[sampleCount];
// Get current values ONCE per buffer (not per sample)
float rpm = _currentRPM;
float throttle = _currentThrottle;
float volume = _volume;
// DIRECT RPM TO FREQUENCY - NO SMOOTHING
float baseFrequency = rpm * _rpmToHzFactor; // (RPM/60) × (cylinders/2)
// Pre-calculate harmonic frequencies
float[] harmonicFrequencies = new float[_harmonicRatios.Length];
float[] phaseIncrements = new float[_harmonicRatios.Length];
for (int h = 0; h < _harmonicRatios.Length; h++)
{
harmonicFrequencies[h] = baseFrequency * _harmonicRatios[h];
phaseIncrements[h] = harmonicFrequencies[h] * 2f * MathF.PI / SAMPLE_RATE;
}
// Calculate roughness factor
float roughness = 0.02f * throttle;
// Generate sound
for (int i = 0; i < sampleCount; i += 2)
{
float sampleValue = 0f;
// Sum all harmonics
for (int h = 0; h < _harmonicRatios.Length; h++)
{
sampleValue += MathF.Sin(_harmonicPhases[h]) * _harmonicAmplitudes[h];
_harmonicPhases[h] += phaseIncrements[h];
if (_harmonicPhases[h] > 2f * MathF.PI)
_harmonicPhases[h] -= 2f * MathF.PI;
}
// Add roughness
sampleValue += (float)(_random.NextDouble() * 2 - 1) * roughness;
// Apply volume
sampleValue *= volume;
// Clamp and convert
sampleValue = Math.Clamp(sampleValue, -1f, 1f);
short sample = (short)(sampleValue * 32767);
// Stereo
samples[i] = sample;
samples[i + 1] = sample;
}
return true;
}
protected override void OnSeek(Time timeOffset)
{
// Reset phases
for (int i = 0; i < _harmonicPhases.Length; i++)
{
_harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI);
}
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
namespace Car_simulation
{
public interface IPhysicsObject
{
Vector2 Position { get; }
float GetResistanceForce(Vector2 carPosition, float carSpeed);
float GetTractionCoefficient(Vector2 carPosition);
}
}

211
Car simulation/Program.cs Normal file
View File

@@ -0,0 +1,211 @@
using Car_simulation;
using SFML.Window;
using SFML.Graphics;
using SFML.System;
using System.Diagnostics;
internal class Program
{
Car car = new Car();
private bool _isRunning = true;
private RenderWindow _window;
// Timing for physics
private Clock _clock = new Clock();
private Time _timePerUpdate = Time.FromSeconds(1.0f / 60.0f); // 60 FPS physics
private Time _accumulatedTime = Time.Zero;
private long _updateCount = 0;
private Dictionary<Keyboard.Key, bool> _previousKeyStates = new Dictionary<Keyboard.Key, bool>();
private Dictionary<Keyboard.Key, bool> _currentKeyStates = new Dictionary<Keyboard.Key, bool>();
private static void Main(string[] args)
{
Program program = new Program();
program.Run();
}
private void Run()
{
_window = new RenderWindow(new VideoMode(800, 600), "Car Simulation", Styles.Default);
_window.SetVisible(true);
_window.SetFramerateLimit(60);
_window.SetKeyRepeatEnabled(false);
_window.Closed += (sender, e) => _isRunning = false;
_window.KeyPressed += OnKeyPressed;
_window.KeyReleased += OnKeyReleased;
InitializeTrackedKeys();
_clock.Restart();
while (_isRunning && _window.IsOpen)
{
_window.DispatchEvents();
Time elapsed = _clock.Restart();
_accumulatedTime += elapsed;
while (_accumulatedTime >= _timePerUpdate)
{
ProcessInput(_timePerUpdate.AsSeconds());
car.Update(_timePerUpdate.AsSeconds());
_accumulatedTime -= _timePerUpdate;
_updateCount++;
if (_accumulatedTime >= Time.FromSeconds(0.2f))
{
_accumulatedTime = _timePerUpdate;
}
}
UpdateDisplay();
UpdatePreviousKeyStates();
}
_window.Close();
Console.WriteLine($"\nSimulation stopped after {_updateCount} updates");
}
private void InitializeTrackedKeys()
{
// Initialize all keys we care about
var keysToTrack = new Keyboard.Key[]
{
Keyboard.Key.W,
Keyboard.Key.Up,
Keyboard.Key.Down,
Keyboard.Key.B,
Keyboard.Key.Space,
Keyboard.Key.Left,
Keyboard.Key.Right,
Keyboard.Key.Escape
};
foreach (var key in keysToTrack)
{
_currentKeyStates[key] = false;
_previousKeyStates[key] = false;
}
}
private void OnKeyPressed(object sender, KeyEventArgs e)
{
var key = e.Code;
// Update current state
if (_currentKeyStates.ContainsKey(key))
{
_currentKeyStates[key] = true;
}
}
private void OnKeyReleased(object sender, KeyEventArgs e)
{
var key = e.Code;
// Update current state
if (_currentKeyStates.ContainsKey(key))
{
_currentKeyStates[key] = false;
}
}
private void ProcessInput(float deltaTime)
{
// quit
if (IsKeyDown(Keyboard.Key.Escape))
{
_isRunning = false;
return;
}
// force clutch
car.ForceClutch = (IsKeyDown(Keyboard.Key.Space));
// throttle
if (IsKeyDown(Keyboard.Key.W))
{
car.ThrottleInput = Math.Min(car.ThrottleInput + 2f * deltaTime, 1.0f);
}
else
{
car.ThrottleInput = Math.Max(car.ThrottleInput - 10f * deltaTime, 0f);
}
// brake
if (IsKeyDown(Keyboard.Key.B))
{
car.BrakeInput = Math.Min(car.BrakeInput + 0.5f * deltaTime, 1.0f);
}
else
{
car.BrakeInput = Math.Max(car.BrakeInput - 1f * deltaTime, 0f);
}
// clutch
if (IsKeyDown(Keyboard.Key.Up))
{
car.ClutchInput = Math.Min(car.ClutchInput + 1f * deltaTime, 1.0f);
}
else if (IsKeyDown(Keyboard.Key.Down))
{
car.ClutchInput = Math.Max(car.ClutchInput - 1f * deltaTime, 0f);
}
// clutch
if (IsKeyDown(Keyboard.Key.Up))
{
car.ClutchInput = Math.Min(car.ClutchInput + 1f * deltaTime, 1.0f);
}
else if (IsKeyDown(Keyboard.Key.Down))
{
car.ClutchInput = Math.Max(car.ClutchInput - 1f * deltaTime, 0f);
}
// gear
if (WasKeyPressed(Keyboard.Key.Left))
{
car.Drivetrain.GearDown();
}
else if (WasKeyPressed(Keyboard.Key.Right))
{
car.Drivetrain.GearUp();
}
}
private void UpdatePreviousKeyStates()
{
var keys = new List<Keyboard.Key>(_currentKeyStates.Keys);
foreach (var key in keys)
{
_previousKeyStates[key] = _currentKeyStates[key];
}
}
private bool IsKeyDown(Keyboard.Key key)
{
return _currentKeyStates.ContainsKey(key) && _currentKeyStates[key];
}
private bool WasKeyPressed(Keyboard.Key key)
{
return IsKeyDown(key) &&
(!_previousKeyStates.ContainsKey(key) || !_previousKeyStates[key]);
}
private void UpdateDisplay()
{
_window.Clear(Color.Black);
// Render car or simulation visualization here
// For example, if car has Draw() method:
// car.Draw(_window);
car.DisplayUpdate(); // If this updates console display
_window.Display();
}
}

45
Car simulation/Util.cs Normal file
View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Car_simulation
{
public class Util
{
public static float Lerp(float a, float b, float t)
{
return a + (b - a) * t;
}
}
public static class PhysicsUtil
{
public const float G = 9.81f;
public const float AirDensity = 1.225f;
public static float Lerp(float a, float b, float t)
{
t = Math.Clamp(t, 0f, 1f);
return a + (b - a) * t;
}
public static float RPMToOmega(float rpm) => rpm * MathF.PI * 2f / 60f;
public static float OmegaToRPM(float omega) => omega * 60f / (2f * MathF.PI);
// Calculate kinetic energy: 0.5 * I * ω²
public static float CalculateRotationalEnergy(float inertia, float omega)
{
return 0.5f * inertia * omega * omega;
}
// Calculate omega from energy: ω = sqrt(2E / I)
public static float CalculateOmegaFromEnergy(float energy, float inertia)
{
if (energy <= 0) return 0;
return MathF.Sqrt(2f * energy / inertia);
}
public const float RAD_PER_SEC_TO_RPM = 60f / (2f * MathF.PI); // ≈ 9.549
public const float RPM_TO_RAD_PER_SEC = (2f * MathF.PI) / 60f; // ≈ 0.1047
}
}

39
Car simulation/Vector2.cs Normal file
View File

@@ -0,0 +1,39 @@
public struct Vector2
{
public float X, Y;
public Vector2(float x, float y) { X = x; Y = y; }
public float Length => MathF.Sqrt(X * X + Y * Y);
public float LengthSquared => X * X + Y * Y;
// Returns a normalized copy
public Vector2 Normalized()
{
float length = Length;
if (length > 0.0001f)
return new Vector2(X / length, Y / length);
return new Vector2(0, 0);
}
// Normalizes in place
public void Normalize()
{
float length = Length;
if (length > 0.0001f)
{
X /= length;
Y /= length;
}
}
// Static normalize
public static Vector2 Normalize(Vector2 v) => v.Normalized();
// Operators
public static Vector2 operator *(Vector2 v, float s) => new Vector2(v.X * s, v.Y * s);
public static Vector2 operator *(float s, Vector2 v) => new Vector2(v.X * s, v.Y * s);
public static Vector2 operator /(Vector2 v, float s) => new Vector2(v.X / s, v.Y / s);
public static Vector2 operator +(Vector2 a, Vector2 b) => new Vector2(a.X + b.X, a.Y + b.Y);
public static Vector2 operator -(Vector2 a, Vector2 b) => new Vector2(a.X - b.X, a.Y - b.Y);
}

View File

@@ -0,0 +1,92 @@
namespace Car_simulation
{
public class WheelSystem
{
// Physical properties
public float Radius { get; set; } = 0.3f; // meters
public float Inertia { get; set; } = 2.0f; // kg·m² per wheel
public int WheelCount { get; set; } = 4;
public int DrivenWheels { get; set; } = 2; // 2WD
// State
public float WheelEnergy { get; set; } = 0f; // Joules
public float AngularVelocity => GetOmega();
public float RPM => GetRPM();
public float Speed => GetSpeed();
public float ResistanceTorque { get; set; } = 0f;
// Calculations
public float GetTotalInertia()
{
return Inertia * WheelCount;
}
public float GetOmega()
{
if (WheelEnergy <= 0 || GetTotalInertia() <= 0) return 0f;
return MathF.Sqrt(2f * WheelEnergy / GetTotalInertia());
}
public float GetRPM()
{
return AngularVelocity * PhysicsUtil.RAD_PER_SEC_TO_RPM;
}
public float GetSpeed()
{
return AngularVelocity * Radius;
}
public float GetEnergyFromSpeed(float speed)
{
float omega = speed / Radius;
return 0.5f * GetTotalInertia() * omega * omega;
}
public void SetSpeed(float speed)
{
WheelEnergy = GetEnergyFromSpeed(speed);
}
// Apply work to the wheels
public void ApplyWork(float work)
{
WheelEnergy += work;
WheelEnergy = Math.Max(WheelEnergy, 0);
}
public void ApplyTorque(float torque, float deltaTime)
{
if (torque == 0) return;
float work = torque * AngularVelocity * deltaTime;
ApplyWork(work);
}
public void ApplyResistance(float deltaTime)
{
if (ResistanceTorque <= 0 || AngularVelocity == 0) return;
float omega = AngularVelocity;
if (MathF.Abs(omega) < 0.1f)
{
// Check if we have enough torque to overcome static friction
// For now, just return without applying resistance to allow startup
return;
}
float resistanceSign = -MathF.Sign(omega);
float alpha = (resistanceSign * ResistanceTorque) / GetTotalInertia();
float omegaNew = omega + alpha * deltaTime;
if (MathF.Sign(omegaNew) != MathF.Sign(omega))
{
omegaNew = 0;
}
float energyNew = 0.5f * GetTotalInertia() * omegaNew * omegaNew;
WheelEnergy = Math.Max(energyNew, 0);
}
}
}