diff --git a/Car simulation.slnx b/Car simulation.slnx new file mode 100644 index 0000000..11cb5cd --- /dev/null +++ b/Car simulation.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Car simulation/Car simulation.csproj b/Car simulation/Car simulation.csproj new file mode 100644 index 0000000..027dfcc --- /dev/null +++ b/Car simulation/Car simulation.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + Car_simulation + enable + enable + + + + + + + diff --git a/Car simulation/Car.cs b/Car simulation/Car.cs new file mode 100644 index 0000000..0ecd04e --- /dev/null +++ b/Car simulation/Car.cs @@ -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)"); + } + } +} \ No newline at end of file diff --git a/Car simulation/Drivetrain.cs b/Car simulation/Drivetrain.cs new file mode 100644 index 0000000..fc71698 --- /dev/null +++ b/Car simulation/Drivetrain.cs @@ -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() + }; + } + } +} \ No newline at end of file diff --git a/Car simulation/Engine.cs b/Car simulation/Engine.cs new file mode 100644 index 0000000..9308aee --- /dev/null +++ b/Car simulation/Engine.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/Car simulation/EngineSound.cs b/Car simulation/EngineSound.cs new file mode 100644 index 0000000..bd8a66b --- /dev/null +++ b/Car simulation/EngineSound.cs @@ -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); + } + } + } +} \ No newline at end of file diff --git a/Car simulation/IPhysicsObject.cs b/Car simulation/IPhysicsObject.cs new file mode 100644 index 0000000..19c459d --- /dev/null +++ b/Car simulation/IPhysicsObject.cs @@ -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); + } +} diff --git a/Car simulation/Program.cs b/Car simulation/Program.cs new file mode 100644 index 0000000..68a4770 --- /dev/null +++ b/Car simulation/Program.cs @@ -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 _previousKeyStates = new Dictionary(); + private Dictionary _currentKeyStates = new Dictionary(); + + 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(_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(); + } +} \ No newline at end of file diff --git a/Car simulation/Util.cs b/Car simulation/Util.cs new file mode 100644 index 0000000..32e8678 --- /dev/null +++ b/Car simulation/Util.cs @@ -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 + } +} diff --git a/Car simulation/Vector2.cs b/Car simulation/Vector2.cs new file mode 100644 index 0000000..6f6ad9d --- /dev/null +++ b/Car simulation/Vector2.cs @@ -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); +} \ No newline at end of file diff --git a/Car simulation/WheelSystem.cs b/Car simulation/WheelSystem.cs new file mode 100644 index 0000000..2f1d710 --- /dev/null +++ b/Car simulation/WheelSystem.cs @@ -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); + } + } +} \ No newline at end of file