diff --git a/Car simulation/EngineSound.cs b/Car simulation/Audio/EngineSound.cs similarity index 99% rename from Car simulation/EngineSound.cs rename to Car simulation/Audio/EngineSound.cs index 55b5ecb..dd3adbf 100644 --- a/Car simulation/EngineSound.cs +++ b/Car simulation/Audio/EngineSound.cs @@ -2,7 +2,7 @@ using SFML.System; using System; -namespace Car_simulation +namespace Car_simulation.Audio { public class EngineSound : SoundStream { diff --git a/Car simulation/Car.cs b/Car simulation/Car.cs deleted file mode 100644 index ca4368a..0000000 --- a/Car simulation/Car.cs +++ /dev/null @@ -1,161 +0,0 @@ -using static SFML.Window.Mouse; - -namespace Car_simulation -{ - public class Car - { - public Vector2 Position = new Vector2(0, 0); - public Vector2 Velocity => new Vector2(WheelSystem.CarSpeed, 0); // Now directly from wheels - - public float Speed => WheelSystem.CarSpeed; - - public float Mass { get; set; } = 2000f; // 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.3f; - 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(); - - // Set car mass in wheel system (so it's included in energy calculations) - WheelSystem.CarMass = Mass; - WheelSystem.WheelCount = WheelCount; - WheelSystem.DrivenWheels = DrivenWheels; - - Drivetrain = new Drivetrain(Engine, WheelSystem); - - InitializeAudio(); - } - - public List GetDisplayData() - { - return new List - { - $"Engine Energy: {Engine.FlywheelEnergy,7:F0} J", - $"Engine Torque: {Engine.GetTorqueOutput(),7:F0} Nm", - $"Engine RPM: {Engine.RPM,7:F0}", - $"Total Energy: {WheelSystem.TotalEnergy,7:F0} J", - $" (Wheel Rot: {WheelSystem.GetRotationalEnergy(),7:F0} J)", - $" (Car Trans: {WheelSystem.GetTranslationalEnergy(),7:F0} J)", - $"Wheel RPM: {WheelSystem.RPM,7:F0}", - $"Vehicle: {Speed * 3.6f,7:F1} km/h", - $"Throttle: {Engine.GetActualThrottle() * 100,6:F1}%", - $"Power: {Engine.CurrentPower / 1000,6:F1} kW", - $"Transmitted: {Drivetrain.TransmittedPower / 1000,6:F1} kW", - $"Brake: {BrakeInput * 100,6:F1}%", - $"Speed Diff: {Drivetrain.GetSpeedDifferenceRPM(),6:F0} RPM", - $"Clutch: {ClutchInput * 100,6:F1}% disengaged", - $"Clutch T: {Drivetrain.ClutchTorque,6:F0} Nm", - $"Clutch Slip: {Drivetrain.GetClutchSlipPercent(),6:F1}%", - $"Resistance: {CalculateTotalResistanceForce(),6:F1} N", - $"Drag: {CalculateDragForce(),6:F1} N", - $"Rolling: {CalculateRollingResistanceForce(),6:F1} N", - $"Gear: {Drivetrain.GetCurrentGearName(),3} (Ratio: {Drivetrain.GearRatio:F2}:1)" - }; - } - - 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, float totalTime) - { - Engine.Throttle = ThrottleInput; - Drivetrain.ClutchEngagement = 1f - ClutchInput; - - if (ForceClutch) - Drivetrain.ClutchEngagement = 0f; - - // Update engine - Engine.Update(deltaTime, totalTime); - - // Update drivetrain (transfers energy between engine and wheels+car) - Drivetrain.Update(deltaTime); - - // Calculate and apply resistance - float resistanceForce = CalculateTotalResistanceForce(); - WheelSystem.ResistanceTorque = resistanceForce * WheelSystem.Radius; - WheelSystem.ApplyResistance(deltaTime); - - // Apply braking - ApplyBraking(deltaTime); - - // Update position based on velocity (which comes from WheelSystem) - Position += Velocity * 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 ApplyBraking(float deltaTime) - { - if (BrakeInput <= 0) return; - - float brakeTorque = BrakeInput * 3000f; - WheelSystem.ApplyTorque(-brakeTorque, deltaTime); - } - - public float CalculateTotalResistanceForce() - { - float dragForce = CalculateDragForce(); - float rollingForce = CalculateRollingResistanceForce(); - return dragForce + rollingForce; - } - - public float CalculateDragForce() - { - float speed = Speed; - return 0.5f * AirDensity * DragCoefficient * FrontalArea * speed * speed; - } - - public float CalculateRollingResistanceForce() - { - return RollingResistanceCoefficient * Mass * 9.81f; - } - } -} \ No newline at end of file diff --git a/Car simulation/Core/Components/Aerodynamics.cs b/Car simulation/Core/Components/Aerodynamics.cs new file mode 100644 index 0000000..235b320 --- /dev/null +++ b/Car simulation/Core/Components/Aerodynamics.cs @@ -0,0 +1,29 @@ +using Car_simulation.Core.Physics; + +namespace Car_simulation.Core.Components +{ + public class Aerodynamics + { + public float DragCoefficient { get; set; } = 0.3f; + public float FrontalArea { get; set; } = 2.2f; // m² + public float RollingResistanceCoefficient { get; set; } = 0.015f; + + private readonly ResistanceCalculator _resistanceCalculator = new ResistanceCalculator(); + + public float CalculateDragForce(float speed) + { + return _resistanceCalculator.CalculateDragForce(speed, DragCoefficient, FrontalArea); + } + + public float CalculateRollingResistanceForce(float mass) + { + return _resistanceCalculator.CalculateRollingResistanceForce(mass, RollingResistanceCoefficient); + } + + public float CalculateTotalResistanceForce(float speed, float mass) + { + return _resistanceCalculator.CalculateTotalResistanceForce( + speed, mass, DragCoefficient, FrontalArea, RollingResistanceCoefficient); + } + } +} \ No newline at end of file diff --git a/Car simulation/Core/Components/BrakeSystem.cs b/Car simulation/Core/Components/BrakeSystem.cs new file mode 100644 index 0000000..c8884ac --- /dev/null +++ b/Car simulation/Core/Components/BrakeSystem.cs @@ -0,0 +1,30 @@ +using Car_simulation.Core.Physics; + +namespace Car_simulation.Core.Components +{ + public class BrakeSystem : ICarComponent + { + public float BrakeInput { get; set; } = 0f; + public float MaxBrakeTorque { get; set; } = 3000f; + + private WheelSystem _wheelSystem; + + public BrakeSystem(WheelSystem wheelSystem) + { + _wheelSystem = wheelSystem; + } + + public void Update(float deltaTime) + { + if (BrakeInput <= 0) return; + + float brakeTorque = BrakeInput * MaxBrakeTorque; + _wheelSystem.ApplyTorque(-brakeTorque, deltaTime); + } + + public float GetBrakeTorque() + { + return BrakeInput * MaxBrakeTorque; + } + } +} \ No newline at end of file diff --git a/Car simulation/Drivetrain.cs b/Car simulation/Core/Components/Drivetrain.cs similarity index 58% rename from Car simulation/Drivetrain.cs rename to Car simulation/Core/Components/Drivetrain.cs index 4dd7d71..2ca0d6e 100644 --- a/Car simulation/Drivetrain.cs +++ b/Car simulation/Core/Components/Drivetrain.cs @@ -1,31 +1,21 @@ -namespace Car_simulation +using Car_simulation.Core.Physics; + +namespace Car_simulation.Core.Components { - public class Drivetrain + public class Drivetrain : ICarComponent { - // 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 - 2.5f, // 2nd - 1.8f, // 3rd - 1.3f, // 4th - 1.0f, // 5th - 0.8f, // 6th - 0.65f // 7th - }; + private int _currentGear = 1; + public float[] GearRatios { get; set; } = { 3.8f, 2.5f, 1.8f, 1.3f, 1.0f, 0.8f, 0.65f }; public float FinalDriveRatio { get; set; } = 4.0f; public float Efficiency { get; set; } = 0.95f; - public float ClutchEngagement { get; set; } = 0f; // 0 = disengaged, 1 = fully engaged + public float ClutchEngagement { get; set; } = 0f; - // Clutch properties public float MaxClutchTorque { get; set; } = 400f; public float ClutchStiffness { get; set; } = 50f; - // State public float ClutchTorque { get; private set; } public float TransmittedPower { get; private set; } public float ClutchSlipRatio { get; private set; } @@ -46,21 +36,14 @@ return; } - // Calculate expected vs actual wheel speeds float expectedWheelOmega = Engine.AngularVelocity / TotalRatio; float actualWheelOmega = WheelSystem.AngularVelocity; float omegaDifference = actualWheelOmega - expectedWheelOmega; - // Calculate max torque clutch can transmit float maxClutchTorque = MaxClutchTorque * ClutchEngagement; - - // Simple spring model: torque tries to sync speeds float desiredTorque = -omegaDifference * ClutchStiffness; - - // Clamp to clutch capacity desiredTorque = Math.Clamp(desiredTorque, -maxClutchTorque, maxClutchTorque); - // Also limit by engine capability when accelerating if (desiredTorque > 0) { float engineTorque = Engine.GetTorqueOutput() * Engine.GetActualThrottle(); @@ -70,53 +53,35 @@ ClutchTorque = desiredTorque; - // Calculate energy transfer based on torque float energyTransferred = 0f; - if (omegaDifference > 0.01f) // Wheels → Engine (engine braking) + if (omegaDifference > 0.01f) // Wheels → Engine { - // Power = torque × angular velocity (at slower side - engine) - float power = ClutchTorque * (Engine.AngularVelocity); + float power = ClutchTorque * Engine.AngularVelocity; energyTransferred = power * deltaTime; - - // Wheels lose energy, engine gains (minus efficiency losses) float wheelEnergyLoss = Math.Abs(energyTransferred); float engineEnergyGain = wheelEnergyLoss * Efficiency; WheelSystem.TotalEnergy -= wheelEnergyLoss; Engine.FlywheelEnergy += engineEnergyGain; } - else if (omegaDifference < -0.01f) // Engine → Wheels (acceleration) + else if (omegaDifference < -0.01f) // Engine → Wheels { - // Power = torque × angular velocity (at faster side - engine) - float power = -ClutchTorque * Engine.AngularVelocity; // Negative torque, positive power + float power = -ClutchTorque * Engine.AngularVelocity; energyTransferred = power * deltaTime; - - // Engine loses energy, wheels gain float engineEnergyLoss = Math.Abs(energyTransferred); float wheelEnergyGain = engineEnergyLoss * Efficiency; Engine.FlywheelEnergy -= engineEnergyLoss; WheelSystem.TotalEnergy += wheelEnergyGain; } - else - { - // Nearly synchronized - energyTransferred = 0; - } - // Calculate transmitted power TransmittedPower = energyTransferred / deltaTime; - // Calculate clutch slip CORRECTLY: - // Slip = 0 when torque < max torque (clutch can handle it) - // Slip = 1 when torque = max torque (clutch is slipping) if (maxClutchTorque > 0) { float torqueRatio = Math.Abs(ClutchTorque) / maxClutchTorque; - // If we're transmitting max torque, clutch is slipping - // If we're transmitting less, clutch is gripping - ClutchSlipRatio = torqueRatio; // 0 = no slip, 1 = full slip + ClutchSlipRatio = torqueRatio; } else { @@ -124,16 +89,15 @@ } } - // Other methods... public float GearRatio => GetCurrentGearRatio(); public float TotalRatio => GearRatio * FinalDriveRatio; private float GetCurrentGearRatio() { - if (currentGear == 0) return 0f; - if (currentGear == -1) return -3.5f; - if (currentGear > 0 && currentGear <= GearRatios.Length) - return GearRatios[currentGear - 1]; + if (_currentGear == 0) return 0f; + if (_currentGear == -1) return -3.5f; + if (_currentGear > 0 && _currentGear <= GearRatios.Length) + return GearRatios[_currentGear - 1]; return 0f; } @@ -146,20 +110,17 @@ public string GetCurrentGearName() { - return currentGear switch + return _currentGear switch { -1 => "R", 0 => "N", - _ => currentGear.ToString() + _ => _currentGear.ToString() }; } - public float GetClutchSlipPercent() - { - return ClutchSlipRatio * 100f; - } + public float GetClutchSlipPercent() => ClutchSlipRatio * 100f; - public void GearUp() { if (currentGear < GearRatios.Length) currentGear++; } - public void GearDown() { if (currentGear > 1) currentGear--; } + public void GearUp() { if (_currentGear < GearRatios.Length) _currentGear++; } + public void GearDown() { if (_currentGear > 1) _currentGear--; } } } \ No newline at end of file diff --git a/Car simulation/Core/Components/Engine.cs b/Car simulation/Core/Components/Engine.cs new file mode 100644 index 0000000..12c538b --- /dev/null +++ b/Car simulation/Core/Components/Engine.cs @@ -0,0 +1,163 @@ +using Car_simulation.Core.Physics; + +namespace Car_simulation.Core.Components +{ + public class Engine : ICarComponent + { + // Energy state + public float FlywheelEnergy { get; set; } + + // Physical properties + public float MomentOfInertia { get; set; } = 0.25f; + public float IdleRPM { get; set; } = 800f; + public float RevLimit { get; set; } = 7000; + public float StallSpeed { get; set; } = 200f; + public float Throttle { get; set; } = 0f; + public bool IsRunning => RPM > StallSpeed; + + private float _cutoffUntil = 0; + private bool _cutoff = false; + + // Torque curve + private TorqueCurve _torqueCurve; + + public Engine() + { + FlywheelEnergy = GetEnergyFromRPM(IdleRPM); + _torqueCurve = new TorqueCurve(); + InitializeDefaultCurve(); + } + + private void InitializeDefaultCurve() + { + _torqueCurve.AddPoint(0f, 0f); + _torqueCurve.AddPoint(800f, 95f); + _torqueCurve.AddPoint(1500f, 160f); + _torqueCurve.AddPoint(2500f, 200f); + _torqueCurve.AddPoint(4000f, 235f); + _torqueCurve.AddPoint(5000f, 230f); + _torqueCurve.AddPoint(6000f, 210f); + _torqueCurve.AddPoint(6800f, 185f); + _torqueCurve.AddPoint(7200f, 170f); + } + + public float RPM => GetRPM(); + public float AngularVelocity => GetOmega(); + public float CurrentPower { get; private set; } + + public void Update(float deltaTime) + { + // Engine updates are now handled through Car with totalTime parameter + } + + public void UpdateWithTime(float deltaTime, float totalTime) + { + UpdateRevLimiter(totalTime); + + float combustionEnergy = CalculateCombustionPower(deltaTime); + float frictionLoss = CalculateFrictionLoss(deltaTime); + float netEnergy = combustionEnergy - frictionLoss; + + CurrentPower = netEnergy / deltaTime; + FlywheelEnergy += netEnergy; + + // Stall protection + float stallEnergy = GetEnergyFromRPM(StallSpeed); + if (FlywheelEnergy < stallEnergy && Throttle > 0.1f) + { + FlywheelEnergy = stallEnergy * 1.2f; + } + + FlywheelEnergy = Math.Max(FlywheelEnergy, 0); + } + + private void UpdateRevLimiter(float totalTime) + { + if (RPM > RevLimit) + { + _cutoffUntil = totalTime + 0.01f; + } + _cutoff = (totalTime < _cutoffUntil); + } + + private float CalculateFrictionLoss(float deltaTime) + { + float frictionTorque = GetFrictionTorque(); + float frictionPower = frictionTorque * AngularVelocity; + return frictionPower * deltaTime; + } + + private float GetFrictionTorque() + { + return RPM switch + { + < 500 => 15f, + < 1000 => 14f, + < 2000 => 16f, + < 3000 => 18f, + < 4000 => 21f, + < 5000 => 25f, + < 6000 => 30f, + < 7000 => 36f, + _ => 44f + }; + } + + private float CalculateCombustionPower(float deltaTime) + { + float throttle = GetActualThrottle(); + if (_cutoff) throttle = 0; + float torque = GetTorqueOutput() * throttle; + return torque * AngularVelocity * deltaTime; + } + + public float GetActualThrottle() + { + if (RPM < IdleRPM && Throttle < 0.1f) + { + float idleThrottle = (IdleRPM - RPM) / 200f; + return Math.Clamp(idleThrottle, 0.1f, 0.3f); + } + return Throttle; + } + + public float GetOmega() => MathF.Sqrt(2f * FlywheelEnergy / MomentOfInertia); + public float GetRPM() => GetOmega() * PhysicsUtil.RAD_PER_SEC_TO_RPM; + + public float GetEnergyFromRPM(float rpm) + { + float omega = rpm * PhysicsUtil.RPM_TO_RAD_PER_SEC; + return 0.5f * MomentOfInertia * omega * omega; + } + + public float GetTorqueOutput() => _torqueCurve.GetTorqueAtRPM(RPM); + } + + public class TorqueCurve + { + private List<(float RPM, float Torque)> _points = new List<(float, float)>(); + + public void AddPoint(float rpm, float torque) => _points.Add((rpm, torque)); + + public float GetTorqueAtRPM(float rpm) + { + if (rpm <= 400) return 0; + if (_points.Count == 0) return 0; + + var orderedPoints = _points.OrderBy(p => p.RPM).ToList(); + if (rpm <= orderedPoints.First().RPM) return orderedPoints.First().Torque; + if (rpm >= orderedPoints.Last().RPM) return orderedPoints.Last().Torque; + + for (int i = 0; i < orderedPoints.Count - 1; i++) + { + if (rpm >= orderedPoints[i].RPM && rpm <= orderedPoints[i + 1].RPM) + { + float t = (rpm - orderedPoints[i].RPM) / (orderedPoints[i + 1].RPM - orderedPoints[i].RPM); + return PhysicsUtil.Lerp(orderedPoints[i].Torque, orderedPoints[i + 1].Torque, t); + } + } + + return 0f; + } + } +} \ No newline at end of file diff --git a/Car simulation/Core/Components/ICarComponent.cs b/Car simulation/Core/Components/ICarComponent.cs new file mode 100644 index 0000000..c46bc28 --- /dev/null +++ b/Car simulation/Core/Components/ICarComponent.cs @@ -0,0 +1,7 @@ +namespace Car_simulation.Core.Components +{ + public interface ICarComponent + { + void Update(float deltaTime); + } +} \ No newline at end of file diff --git a/Car simulation/Core/Components/WheelSystem.cs b/Car simulation/Core/Components/WheelSystem.cs new file mode 100644 index 0000000..3b030df --- /dev/null +++ b/Car simulation/Core/Components/WheelSystem.cs @@ -0,0 +1,95 @@ +using Car_simulation.Core.Physics; + +namespace Car_simulation.Core.Components +{ + public class WheelSystem : ICarComponent + { + // Physical properties + public float Radius { get; set; } = 0.3f; + public float WheelInertia { get; set; } = 2.0f; + public float CarMass { get; set; } = 1500f; + public int WheelCount { get; set; } = 4; + public int DrivenWheels { get; set; } = 2; + + // State + public float TotalEnergy { get; set; } = 0f; + public float AngularVelocity => GetOmega(); + public float RPM => GetRPM(); + public float CarSpeed => GetCarSpeed(); + + // Derived properties + public float GetTotalRotationalInertia() => WheelInertia * WheelCount; + public float GetEquivalentCarInertia() => CarMass * Radius * Radius; + public float GetTotalInertia() => GetTotalRotationalInertia() + GetEquivalentCarInertia(); + + // Calculations + public float GetOmega() + { + if (TotalEnergy <= 0 || GetTotalInertia() <= 0) return 0f; + return MathF.Sqrt(2f * TotalEnergy / GetTotalInertia()); + } + + public float GetRPM() => AngularVelocity * PhysicsUtil.RAD_PER_SEC_TO_RPM; + public float GetCarSpeed() => AngularVelocity * Radius; + + public float GetRotationalEnergy() + { + float omega = GetOmega(); + return 0.5f * GetTotalRotationalInertia() * omega * omega; + } + + public float GetTranslationalEnergy() + { + float speed = GetCarSpeed(); + return 0.5f * CarMass * speed * speed; + } + + public float GetEnergyFromSpeed(float speed) + { + float omega = speed / Radius; + float rotationalEnergy = 0.5f * GetTotalRotationalInertia() * omega * omega; + float translationalEnergy = 0.5f * CarMass * speed * speed; + return rotationalEnergy + translationalEnergy; + } + + public void SetSpeed(float speed) => TotalEnergy = GetEnergyFromSpeed(speed); + + public void ApplyWork(float work) + { + TotalEnergy += work; + TotalEnergy = Math.Max(TotalEnergy, 0); + } + + public void ApplyTorque(float torque, float deltaTime) + { + if (torque == 0) return; + float work = torque * AngularVelocity * deltaTime; + ApplyWork(work); + } + + public void ApplyResistance(float resistanceTorque, float deltaTime) + { + if (resistanceTorque <= 0 || AngularVelocity == 0) return; + + float omega = AngularVelocity; + if (MathF.Abs(omega) < 0.1f) 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; + TotalEnergy = Math.Max(energyNew, 0); + } + + public void Update(float deltaTime) + { + // WheelSystem updates are handled by Car through other components + } + } +} \ No newline at end of file diff --git a/Car simulation/Core/Models/Car.cs b/Car simulation/Core/Models/Car.cs new file mode 100644 index 0000000..c9ec9a1 --- /dev/null +++ b/Car simulation/Core/Models/Car.cs @@ -0,0 +1,153 @@ +using Car_simulation.Core.Components; +using Car_simulation.Core.Physics; +using Car_simulation.Audio; + +namespace Car_simulation.Core.Models +{ + public class Car + { + // Basic properties + public Vector2 Position = new Vector2(0, 0); + public float Mass { get; set; } = 2000f; + + // Inputs + public float ThrottleInput = 0f; + public float BrakeInput = 0f; + public float ClutchInput = 1f; + public bool ForceClutch = false; + public float SteeringInput = 0f; + + // Components + public Engine Engine { get; private set; } + public Drivetrain Drivetrain { get; private set; } + public WheelSystem WheelSystem { get; private set; } + public BrakeSystem BrakeSystem { get; private set; } + public Aerodynamics Aerodynamics { get; private set; } + + // Derived properties + public Vector2 Velocity => new Vector2(WheelSystem.CarSpeed, 0); + public float Speed => WheelSystem.CarSpeed; + + // Audio + private EngineSound _engineSound; + private bool _audioEnabled = true; + + public Car() + { + InitializeComponents(); + InitializeAudio(); + } + + private void InitializeComponents() + { + Engine = new Engine(); + WheelSystem = new WheelSystem(); + WheelSystem.CarMass = Mass; + WheelSystem.WheelCount = 4; + WheelSystem.DrivenWheels = 2; + + Drivetrain = new Drivetrain(Engine, WheelSystem); + BrakeSystem = new BrakeSystem(WheelSystem); + Aerodynamics = new Aerodynamics(); + } + + 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 List GetDisplayData() + { + return new List + { + $"Engine Energy: {Engine.FlywheelEnergy,7:F0} J", + $"Engine Torque: {Engine.GetTorqueOutput(),7:F0} Nm", + $"Engine RPM: {Engine.RPM,7:F0}", + $"Total Energy: {WheelSystem.TotalEnergy,7:F0} J", + $" (Wheel Rot: {WheelSystem.GetRotationalEnergy(),7:F0} J)", + $" (Car Trans: {WheelSystem.GetTranslationalEnergy(),7:F0} J)", + $"Wheel RPM: {WheelSystem.RPM,7:F0}", + $"Vehicle: {Speed * 3.6f,7:F1} km/h", + $"Throttle: {Engine.GetActualThrottle() * 100,6:F1}%", + $"Power: {Engine.CurrentPower / 1000,6:F1} kW", + $"Transmitted: {Drivetrain.TransmittedPower / 1000,6:F1} kW", + $"Brake: {BrakeInput * 100,6:F1}%", + $"Speed Diff: {Drivetrain.GetSpeedDifferenceRPM(),6:F0} RPM", + $"Clutch: {ClutchInput * 100,6:F1}% disengaged", + $"Clutch T: {Drivetrain.ClutchTorque,6:F0} Nm", + $"Clutch Slip: {Drivetrain.GetClutchSlipPercent(),6:F1}%", + $"Resistance: {CalculateTotalResistanceForce(),6:F1} N", + $"Drag: {CalculateDragForce(),6:F1} N", + $"Rolling: {CalculateRollingResistanceForce(),6:F1} N", + $"Gear: {Drivetrain.GetCurrentGearName(),3} (Ratio: {Drivetrain.GearRatio:F2}:1)" + }; + } + + public void Update(float deltaTime, float totalTime) + { + // Update inputs + Engine.Throttle = ThrottleInput; + Drivetrain.ClutchEngagement = 1f - ClutchInput; + BrakeSystem.BrakeInput = BrakeInput; + + if (ForceClutch) + Drivetrain.ClutchEngagement = 0f; + + // Update components + Engine.UpdateWithTime(deltaTime, totalTime); + Drivetrain.Update(deltaTime); + + // Apply resistance + float resistanceForce = CalculateTotalResistanceForce(); + WheelSystem.ApplyResistance(resistanceForce * WheelSystem.Radius, deltaTime); + + // Apply braking + BrakeSystem.Update(deltaTime); + + // Update position + Position += Velocity * deltaTime; + + // Update audio + 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}"); + } + } + + public float CalculateTotalResistanceForce() + { + return Aerodynamics.CalculateTotalResistanceForce(Speed, Mass); + } + + public float CalculateDragForce() + { + return Aerodynamics.CalculateDragForce(Speed); + } + + public float CalculateRollingResistanceForce() + { + return Aerodynamics.CalculateRollingResistanceForce(Mass); + } + } +} \ No newline at end of file diff --git a/Car simulation/Core/Models/CarState.cs b/Car simulation/Core/Models/CarState.cs new file mode 100644 index 0000000..ae91325 --- /dev/null +++ b/Car simulation/Core/Models/CarState.cs @@ -0,0 +1,14 @@ +namespace Car_simulation.Core.Models +{ + public class CarState + { + public float Speed { get; set; } + public float RPM { get; set; } + public float Throttle { get; set; } + public float Brake { get; set; } + public string Gear { get; set; } + public float ClutchSlip { get; set; } + public float EnginePower { get; set; } + public float TransmittedPower { get; set; } + } +} \ No newline at end of file diff --git a/Car simulation/Util.cs b/Car simulation/Core/Physics/PhysicsUtil.cs similarity index 69% rename from Car simulation/Util.cs rename to Car simulation/Core/Physics/PhysicsUtil.cs index 32e8678..81b35d6 100644 --- a/Car simulation/Util.cs +++ b/Car simulation/Core/Physics/PhysicsUtil.cs @@ -1,21 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Car_simulation +namespace Car_simulation.Core.Physics { - 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 const float RAD_PER_SEC_TO_RPM = 60f / (2f * MathF.PI); + public const float RPM_TO_RAD_PER_SEC = (2f * MathF.PI) / 60f; public static float Lerp(float a, float b, float t) { @@ -26,20 +16,15 @@ namespace Car_simulation 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 } -} +} \ No newline at end of file diff --git a/Car simulation/Core/Physics/ResistanceCalculator.cs b/Car simulation/Core/Physics/ResistanceCalculator.cs new file mode 100644 index 0000000..dd37199 --- /dev/null +++ b/Car simulation/Core/Physics/ResistanceCalculator.cs @@ -0,0 +1,25 @@ +using Car_simulation.Core.Components; + +namespace Car_simulation.Core.Physics +{ + public class ResistanceCalculator + { + public float CalculateDragForce(float speed, float dragCoefficient, float frontalArea) + { + return 0.5f * PhysicsUtil.AirDensity * dragCoefficient * frontalArea * speed * speed; + } + + public float CalculateRollingResistanceForce(float mass, float rollingResistanceCoefficient) + { + return rollingResistanceCoefficient * mass * PhysicsUtil.G; + } + + public float CalculateTotalResistanceForce(float speed, float mass, + float dragCoefficient, float frontalArea, float rollingResistanceCoefficient) + { + float dragForce = CalculateDragForce(speed, dragCoefficient, frontalArea); + float rollingForce = CalculateRollingResistanceForce(mass, rollingResistanceCoefficient); + return dragForce + rollingForce; + } + } +} \ No newline at end of file diff --git a/Car simulation/Core/Physics/Vector2.cs b/Car simulation/Core/Physics/Vector2.cs new file mode 100644 index 0000000..6a80ad9 --- /dev/null +++ b/Car simulation/Core/Physics/Vector2.cs @@ -0,0 +1,38 @@ +namespace Car_simulation.Core.Physics +{ + 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; + + public Vector2 Normalized() + { + float length = Length; + if (length > 0.0001f) + return new Vector2(X / length, Y / length); + return new Vector2(0, 0); + } + + public void Normalize() + { + float length = Length; + if (length > 0.0001f) + { + X /= length; + Y /= length; + } + } + + public static Vector2 Normalize(Vector2 v) => v.Normalized(); + + 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/Engine.cs b/Car simulation/Engine.cs deleted file mode 100644 index bf40f4f..0000000 --- a/Car simulation/Engine.cs +++ /dev/null @@ -1,155 +0,0 @@ -namespace Car_simulation -{ - public class Engine - { - // Energy state - public float FlywheelEnergy { get; set; } - - // Values - public float RPM => GetRPM(); - public float AngularVelocity => GetOmega(); - public float CurrentPower { get; private set; } - - // Physical properties - public float MomentOfInertia { get; set; } = 0.25f; - public float IdleRPM { get; set; } = 800f; - public float RevLimit { get; set; } = 7000; - public float StallSpeed { get; set; } = 200f; - public float Throttle { get; set; } = 0f; - public bool IsRunning => RPM > StallSpeed; - private float _cutoffUntil = 0; - private bool _cutoff = false; - - // Torque curve - public Dictionary TorqueCurve { get; set; } = new() - { - { 0f, 0f }, - { 800f, 95f }, - { 1500f, 160f }, - { 2500f, 200f }, - { 4000f, 235f }, // peak torque - { 5000f, 230f }, - { 6000f, 210f }, - { 6800f, 185f }, - { 7200f, 170f }, - }; - - public Engine() - { - FlywheelEnergy = GetEnergyFromRPM(IdleRPM); - } - - public void UpdateRevLimiter(float totalTime) - { - if (RPM > RevLimit) - { - _cutoffUntil = totalTime + 0.01f; - } - - _cutoff = (totalTime < _cutoffUntil); - } - - public float CalculateFrictionLoss(float deltaTime) - { - float frictionTorque; - - // Realistic friction based on RPM - if (RPM < 500) frictionTorque = 15f; - 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; - } - - public float CalculateCombustionPower(float deltaTime) - { - float throttle = GetActualThrottle(); - if (_cutoff) throttle = 0; - float torque = GetTorqueOutput() * throttle; - return torque * AngularVelocity * deltaTime; - } - - public float GetActualThrottle() - { - // Idle control: maintain idle speed when throttle is low - if (RPM < IdleRPM && Throttle < 0.1f) - { - float idleThrottle = (IdleRPM - RPM) / 200f; - return Math.Clamp(idleThrottle, 0.1f, 0.3f); - } - - return Throttle; - } - - 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; - } - - public float GetEnergyFromRPM(float rpm) - { - float omega = rpm * PhysicsUtil.RPM_TO_RAD_PER_SEC; - return 0.5f * MomentOfInertia * omega * omega; - } - - public float GetTorqueOutput() - { - if (RPM <= 400) 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 Update(float deltaTime, float totalTime) - { - UpdateRevLimiter(totalTime); - // Combustion adds energy (if throttle > 0) - float combustionEnergy = CalculateCombustionPower(deltaTime); - - // Friction always removes energy - float frictionLoss = CalculateFrictionLoss(deltaTime); - - // Net energy change from combustion and friction ONLY - // Note: Drivetrain energy transfer happens separately in Drivetrain.Update() - float netEnergy = combustionEnergy - frictionLoss; - CurrentPower = netEnergy / deltaTime; - - FlywheelEnergy += netEnergy; - - // Stall protection - keep engine running if it has throttle - float stallEnergy = GetEnergyFromRPM(StallSpeed); - if (FlywheelEnergy < stallEnergy && Throttle > 0.1f) - { - FlywheelEnergy = stallEnergy * 1.2f; - } - - FlywheelEnergy = Math.Max(FlywheelEnergy, 0); - } - } -} \ No newline at end of file diff --git a/Car simulation/IPhysicsObject.cs b/Car simulation/IPhysicsObject.cs deleted file mode 100644 index 19c459d..0000000 --- a/Car simulation/IPhysicsObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/Inputs/CarInputHandler.cs b/Car simulation/Inputs/CarInputHandler.cs new file mode 100644 index 0000000..5ec1ba2 --- /dev/null +++ b/Car simulation/Inputs/CarInputHandler.cs @@ -0,0 +1,83 @@ +using SFML.Window; + +namespace Car_simulation.Input +{ + public class CarInputHandler + { + private Dictionary _previousKeyStates = new(); + private Dictionary _currentKeyStates = new(); + + public CarInputState InputState { get; private set; } = new CarInputState(); + + public CarInputHandler() + { + InitializeTrackedKeys(); + } + + private void InitializeTrackedKeys() + { + var keys = new[] { + Keyboard.Key.W, Keyboard.Key.S, + Keyboard.Key.Down, Keyboard.Key.Up, Keyboard.Key.Space, + Keyboard.Key.Escape, + Keyboard.Key.Right, Keyboard.Key.Left + }; + + foreach (var key in keys) + { + _previousKeyStates[key] = false; + _currentKeyStates[key] = false; + } + } + + public void OnKeyPressed(KeyEventArgs e) + { + if (_currentKeyStates.ContainsKey(e.Code)) + _currentKeyStates[e.Code] = true; + } + + public void OnKeyReleased(KeyEventArgs e) + { + if (_currentKeyStates.ContainsKey(e.Code)) + _currentKeyStates[e.Code] = false; + } + + public void ProcessInput(float deltaTime) + { + // Update input state based on key states + InputState.ThrottleChange = _currentKeyStates[Keyboard.Key.W] ? deltaTime * 4f : -deltaTime * 8f; + InputState.BrakeChange = _currentKeyStates[Keyboard.Key.S] ? deltaTime * 4f : -deltaTime * 8f; + + InputState.ClutchUp = _currentKeyStates[Keyboard.Key.Up]; + InputState.ClutchDown = _currentKeyStates[Keyboard.Key.Down]; + + InputState.GearUp = _currentKeyStates[Keyboard.Key.Right] && !_previousKeyStates[Keyboard.Key.Right]; + InputState.GearDown = _currentKeyStates[Keyboard.Key.Left] && !_previousKeyStates[Keyboard.Key.Left]; + + InputState.ToggleForceClutch = _currentKeyStates[Keyboard.Key.Space] && !_previousKeyStates[Keyboard.Key.Space]; + InputState.Quit = _currentKeyStates[Keyboard.Key.Escape]; + + UpdatePreviousKeyStates(); + } + + private void UpdatePreviousKeyStates() + { + foreach (var key in _currentKeyStates.Keys.ToList()) + { + _previousKeyStates[key] = _currentKeyStates[key]; + } + } + } + + public class CarInputState + { + public float ThrottleChange { get; set; } + public float BrakeChange { get; set; } + public bool ClutchUp { get; set; } + public bool ClutchDown { get; set; } + public bool GearUp { get; set; } + public bool GearDown { get; set; } + public bool ToggleForceClutch { get; set; } + public bool Quit { get; set; } + } +} \ No newline at end of file diff --git a/Car simulation/Program.cs b/Car simulation/Program.cs index aed3067..70d9f71 100644 --- a/Car simulation/Program.cs +++ b/Car simulation/Program.cs @@ -1,337 +1,157 @@ -using Car_simulation; -using SFML.Window; +using Car_simulation.Core.Models; +using Car_simulation.Input; +using Car_simulation.UI; using SFML.Graphics; using SFML.System; -using System.Collections.Generic; -using System; +using SFML.Window; -internal class Program +namespace Car_simulation { - Car car = new Car(); - private bool _isRunning = true; - - private RenderWindow _window; - private Font _font; - private List _displayTexts = new List(); - private RectangleShape _tachometerBackground; - private RectangleShape _tachometerNeedle; - private RectangleShape _speedometerBackground; - private RectangleShape _speedometerNeedle; - - // Colors - private Color _backgroundColor = new Color(20, 20, 30); - private Color _textColor = new Color(220, 220, 220); - private Color _highlightColor = new Color(0, 150, 255); - private Color _warningColor = new Color(255, 100, 100); - - // 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 float _totalTime = 0.0f; - private long _updateCount = 0; - - private Dictionary _previousKeyStates = new Dictionary(); - private Dictionary _currentKeyStates = new Dictionary(); - - private static void Main(string[] args) + internal class Program { - Program program = new Program(); - program.Run(); - } + private Car _car = new Car(); + private RenderWindow _window; + private Font _font; + private DisplayManager _displayManager; + private CarInputHandler _inputHandler; - private void Run() - { - _window = new RenderWindow(new VideoMode(1000, 800), "Car Simulation", Styles.Default); - _window.SetVisible(true); - _window.SetFramerateLimit(60); - _window.SetKeyRepeatEnabled(false); + // Timing + private Clock _clock = new Clock(); + private Time _timePerUpdate = Time.FromSeconds(1.0f / 60.0f); + private Time _accumulatedTime = Time.Zero; + private float _totalTime = 0.0f; + private long _updateCount = 0; - _window.Closed += (sender, e) => _isRunning = false; - _window.KeyPressed += OnKeyPressed; - _window.KeyReleased += OnKeyReleased; - - // Load font - try + static void Main(string[] args) { - _font = new Font("arial.ttf"); - } - catch - { - _font = new Font("C:/Windows/Fonts/arial.ttf"); + Program program = new Program(); + program.Run(); } - InitializeDisplay(); - InitializeTrackedKeys(); - - _clock.Restart(); - - while (_isRunning && _window.IsOpen) + private void Run() { - _window.DispatchEvents(); + InitializeWindow(); + InitializeUI(); + InitializeInput(); - Time elapsed = _clock.Restart(); - _accumulatedTime += elapsed; - - while (_accumulatedTime >= _timePerUpdate) + while (_window.IsOpen) { - ProcessInput(_timePerUpdate.AsSeconds()); - _totalTime += _timePerUpdate.AsSeconds(); - car.Update(_timePerUpdate.AsSeconds(), _totalTime); - _accumulatedTime -= _timePerUpdate; - _updateCount++; + _window.DispatchEvents(); + + Time elapsed = _clock.Restart(); + _accumulatedTime += elapsed; + + while (_accumulatedTime >= _timePerUpdate) + { + Update(_timePerUpdate.AsSeconds()); + _accumulatedTime -= _timePerUpdate; + } + + Render(); } - UpdateDisplay(); + _window.Close(); + Console.WriteLine($"\nSimulation stopped after {_updateCount} updates"); + } + + private void InitializeWindow() + { + _window = new RenderWindow(new VideoMode(1000, 800), "Car Simulation", Styles.Default); + _window.SetVisible(true); + _window.SetFramerateLimit(60); + _window.SetKeyRepeatEnabled(false); + + _window.Closed += (sender, e) => _window.Close(); + _window.Resized += OnWindowResized; + _window.KeyPressed += OnKeyPressed; + } + + private void OnWindowResized(object sender, SizeEventArgs e) + { + _window.SetView(new View(new FloatRect(0, 0, e.Width, e.Height))); + _displayManager?.HandleResize(e.Width, e.Height); + } + + private void OnKeyPressed(object sender, KeyEventArgs e) + { + // Toggle debug info with F3 + if (e.Code == Keyboard.Key.F3) + { + _displayManager?.ToggleDebugInfo(); + } + + // Pass key event to input handler + _inputHandler?.OnKeyPressed(e); + } + + private void InitializeUI() + { + try + { + _font = new Font("arial.ttf"); + } + catch + { + _font = new Font("C:/Windows/Fonts/arial.ttf"); + } + + _displayManager = new DisplayManager(_window, _font); + } + + private void InitializeInput() + { + _inputHandler = new CarInputHandler(); + _window.KeyPressed += (sender, e) => _inputHandler.OnKeyPressed(e); + _window.KeyReleased += (sender, e) => _inputHandler.OnKeyReleased(e); + } + + private void Update(float deltaTime) + { + _inputHandler.ProcessInput(deltaTime); + _totalTime += deltaTime; + + ApplyInputToCar(deltaTime); + _car.Update(deltaTime, _totalTime); + + // Update display with current car state + _displayManager.Update(_car, deltaTime, _totalTime, _updateCount); + + if (_inputHandler.InputState.Quit) + _window.Close(); + + _updateCount++; + } + + private void ApplyInputToCar(float deltaTime) + { + var input = _inputHandler.InputState; + + _car.ThrottleInput += input.ThrottleChange; + _car.BrakeInput += input.BrakeChange; + + _car.ThrottleInput = Math.Clamp(_car.ThrottleInput, 0f, 1f); + _car.BrakeInput = Math.Clamp(_car.BrakeInput, 0f, 1f); + + if (input.ClutchUp) + _car.ClutchInput += deltaTime * 0.5f; + if (input.ClutchDown) + _car.ClutchInput -= deltaTime * 0.5f; + + _car.ClutchInput = Math.Clamp(_car.ClutchInput, 0f, 1f); + + if (input.ToggleForceClutch) + _car.ForceClutch = !_car.ForceClutch; + + if (input.GearUp) + _car.Drivetrain.GearUp(); + if (input.GearDown) + _car.Drivetrain.GearDown(); + } + + private void Render() + { + _displayManager.Draw(); _window.Display(); - UpdatePreviousKeyStates(); - } - - _window.Close(); - Console.WriteLine($"\nSimulation stopped after {_updateCount} updates"); - } - - private void InitializeDisplay() - { - // Initialize display texts - for (int i = 0; i < 30; i++) - { - Text text = new Text("", _font, 16); - text.FillColor = _textColor; - text.Position = new Vector2f(20, 20 + i * 24); - _displayTexts.Add(text); - } - - // Tachometer - _tachometerBackground = new RectangleShape(new Vector2f(200, 200)); - _tachometerBackground.Position = new Vector2f(700, 50); - _tachometerBackground.FillColor = new Color(40, 40, 50); - _tachometerBackground.OutlineThickness = 2; - _tachometerBackground.OutlineColor = Color.White; - - _tachometerNeedle = new RectangleShape(new Vector2f(80, 4)); - _tachometerNeedle.Position = new Vector2f(800, 150); - _tachometerNeedle.FillColor = Color.Red; - _tachometerNeedle.Origin = new Vector2f(70, 2); - - // Speedometer - _speedometerBackground = new RectangleShape(new Vector2f(200, 200)); - _speedometerBackground.Position = new Vector2f(700, 300); - _speedometerBackground.FillColor = new Color(40, 40, 50); - _speedometerBackground.OutlineThickness = 2; - _speedometerBackground.OutlineColor = Color.White; - - _speedometerNeedle = new RectangleShape(new Vector2f(80, 4)); - _speedometerNeedle.Position = new Vector2f(800, 400); - _speedometerNeedle.FillColor = Color.Green; - _speedometerNeedle.Origin = new Vector2f(70, 2); - } - - private void UpdateDisplay() - { - _window.Clear(_backgroundColor); - - UpdateDisplayTexts(); - - foreach (var text in _displayTexts) - { - if (!string.IsNullOrEmpty(text.DisplayedString)) - _window.Draw(text); - } - - DrawGauges(); - DrawKeyBindings(); - } - - private void UpdateDisplayTexts() - { - // Clear all text - for (int i = 0; i < _displayTexts.Count; i++) - { - _displayTexts[i].DisplayedString = ""; - _displayTexts[i].FillColor = _textColor; - } - - // Update text - using safe indexing - int line = 0; - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = "ENGINE"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" RPM: {car.Engine.RPM,7:F0}"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Torque: {car.Engine.GetTorqueOutput(),7:F0} Nm"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Throttle: {car.Engine.GetActualThrottle() * 100,6:F1}%"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Power: {car.Engine.CurrentPower / 1000,6:F1} kW"; - - if (line < _displayTexts.Count) - { - _displayTexts[line].DisplayedString = $" Status: {(car.Engine.IsRunning ? "RUNNING" : "STALLED")}"; - _displayTexts[line].FillColor = car.Engine.IsRunning ? _textColor : _warningColor; - line++; - } - - line++; // Blank line - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = "DRIVETRAIN"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Gear: {car.Drivetrain.GetCurrentGearName(),3} (Ratio: {car.Drivetrain.GearRatio:F2}:1)"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Clutch: {car.ClutchInput * 100,6:F1}% disengaged"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Clutch Torque: {car.Drivetrain.ClutchTorque,6:F0} Nm"; - - if (line < _displayTexts.Count) - { - _displayTexts[line].DisplayedString = $" Clutch Slip: {car.Drivetrain.GetClutchSlipPercent(),6:F1}%"; - _displayTexts[line].FillColor = car.Drivetrain.GetClutchSlipPercent() > 50 ? _warningColor : _textColor; - line++; - } - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Transmitted: {car.Drivetrain.TransmittedPower / 1000,6:F1} kW"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Speed Diff: {car.Drivetrain.GetSpeedDifferenceRPM(),6:F0} RPM"; - - line++; // Blank line - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = "VEHICLE"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Speed: {car.Speed * 3.6f,7:F1} km/h"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Wheel RPM: {car.WheelSystem.RPM,7:F0}"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Brake: {car.BrakeInput * 100,6:F1}%"; - - line++; // Blank line - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = "FORCES"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Total Resistance: {car.CalculateTotalResistanceForce(),6:F1} N"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Drag: {car.CalculateDragForce(),6:F1} N"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Rolling: {car.CalculateRollingResistanceForce(),6:F1} N"; - - line++; // Blank line - - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = "ENERGY"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Engine: {car.Engine.FlywheelEnergy,7:F0} J"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Total: {car.WheelSystem.TotalEnergy,7:F0} J"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Wheel Rotation: {car.WheelSystem.GetRotationalEnergy() / 1000,7:F0} KJ"; - if (line < _displayTexts.Count) _displayTexts[line++].DisplayedString = $" Car Translation: {car.WheelSystem.GetTranslationalEnergy() / 1000,7:F0} KJ"; - } - - private void DrawGauges() - { - _window.Draw(_tachometerBackground); - - float rpmRatio = Math.Clamp(car.Engine.RPM / 13000f, 0f, 1f); - float tachometerAngle = -90 + (270 * rpmRatio); - _tachometerNeedle.Rotation = tachometerAngle; - _window.Draw(_tachometerNeedle); - - Text tachLabel = new Text("RPM", _font, 20); - tachLabel.FillColor = Color.White; - tachLabel.Position = new Vector2f(770, 70); - _window.Draw(tachLabel); - - Text rpmText = new Text($"{car.Engine.RPM:F0}", _font, 24); - rpmText.FillColor = car.Engine.RPM > 7000 ? _warningColor : Color.White; - rpmText.Position = new Vector2f(765, 100); - _window.Draw(rpmText); - - _window.Draw(_speedometerBackground); - - float speedRatio = Math.Clamp(car.Speed * 3.6f / 200f, 0f, 1f); - float speedometerAngle = -90 + (270 * speedRatio); - _speedometerNeedle.Rotation = speedometerAngle; - _window.Draw(_speedometerNeedle); - - Text speedLabel = new Text("SPEED", _font, 20); - speedLabel.FillColor = Color.White; - speedLabel.Position = new Vector2f(770, 320); - _window.Draw(speedLabel); - - Text speedText = new Text($"{car.Speed * 3.6f:F1} km/h", _font, 24); - speedText.FillColor = Color.White; - speedText.Position = new Vector2f(750, 350); - _window.Draw(speedText); - - Text gearText = new Text($"GEAR {car.Drivetrain.GetCurrentGearName()}", _font, 28); - gearText.FillColor = _highlightColor; - gearText.Position = new Vector2f(750, 520); - _window.Draw(gearText); - } - - private void DrawKeyBindings() - { - Text controls = new Text("CONTROLS\n\nW/S: Throttle/Brake\nUp/Down: Clutch\nLeft/Right: Gear Up/Down\nSpace: Toggle Force Clutch\nESC: Quit", _font, 14); - controls.FillColor = new Color(180, 180, 180); - controls.Position = new Vector2f(250, 625); - _window.Draw(controls); - } - - private void InitializeTrackedKeys() - { - var keys = new[] { - Keyboard.Key.W, Keyboard.Key.S, - Keyboard.Key.Down, Keyboard.Key.Up, Keyboard.Key.Space, - Keyboard.Key.Escape, - Keyboard.Key.Right, Keyboard.Key.Left - }; - - foreach (var key in keys) - { - _previousKeyStates[key] = false; - _currentKeyStates[key] = false; - } - } - - private void OnKeyPressed(object sender, KeyEventArgs e) - { - if (_currentKeyStates.ContainsKey(e.Code)) - _currentKeyStates[e.Code] = true; - } - - private void OnKeyReleased(object sender, KeyEventArgs e) - { - if (_currentKeyStates.ContainsKey(e.Code)) - _currentKeyStates[e.Code] = false; - } - - private void ProcessInput(float deltaTime) - { - // Throttle/Brake - if (_currentKeyStates[Keyboard.Key.W]) - car.ThrottleInput += deltaTime * 4f; - else - car.ThrottleInput -= deltaTime * 8f; - - if (_currentKeyStates[Keyboard.Key.S]) - car.BrakeInput += deltaTime * 4f; - else - car.BrakeInput -= deltaTime * 8f; - - car.ThrottleInput = Math.Clamp(car.ThrottleInput, 0f, 1f); - car.BrakeInput = Math.Clamp(car.BrakeInput, 0f, 1f); - - // Clutch - if (_currentKeyStates[Keyboard.Key.Up]) - car.ClutchInput += deltaTime * 0.5f; - if(_currentKeyStates[Keyboard.Key.Down]) - car.ClutchInput -= deltaTime * 0.5f; - - car.ClutchInput = Math.Clamp(car.ClutchInput, 0f, 1f); - - // Toggle force clutch - if (_currentKeyStates[Keyboard.Key.Space] && !_previousKeyStates[Keyboard.Key.Space]) - car.ForceClutch = !car.ForceClutch; - - // Gear changes - if (_currentKeyStates[Keyboard.Key.Right] && !_previousKeyStates[Keyboard.Key.Right]) - car.Drivetrain.GearUp(); - if (_currentKeyStates[Keyboard.Key.Left] && !_previousKeyStates[Keyboard.Key.Left]) - car.Drivetrain.GearDown(); - - // Quit - if (_currentKeyStates[Keyboard.Key.Escape]) - _isRunning = false; - } - - private void UpdatePreviousKeyStates() - { - var keys = new List(_currentKeyStates.Keys); - foreach (var key in keys) - { - if (_previousKeyStates.ContainsKey(key)) - _previousKeyStates[key] = _currentKeyStates[key]; } } } \ No newline at end of file diff --git a/Car simulation/UI/DisplayManager.cs b/Car simulation/UI/DisplayManager.cs new file mode 100644 index 0000000..fa82da5 --- /dev/null +++ b/Car simulation/UI/DisplayManager.cs @@ -0,0 +1,306 @@ +using SFML.Graphics; +using SFML.System; +using Car_simulation.Core.Models; +using Car_simulation.UI.Instruments; + +namespace Car_simulation.UI +{ + public class DisplayManager + { + private InstrumentPanel _instrumentPanel; + private RenderWindow _window; + private Font _font; + + private Color _backgroundColor = new Color(20, 20, 30); + private Text _fpsText; + private Clock _fpsClock; + private int _frameCount = 0; + private float _fpsUpdateInterval = 0.5f; + private float _fpsAccumulator = 0f; + + // Debug information + private bool _showDebugInfo = false; + private RectangleShape _debugPanel; + private List _debugTexts; + + // Performance tracking + private Queue _frameTimes = new Queue(); + private const int FRAME_TIME_HISTORY = 60; + private Text _performanceText; + + public DisplayManager(RenderWindow window, Font font) + { + _window = window; + _font = font; + + Initialize(); + } + + private void Initialize() + { + _instrumentPanel = new InstrumentPanel(_font, new Vector2f(750, 50), new Vector2f(750, 275)); + + // FPS counter + _fpsText = new Text("FPS: 0", _font, 12); + _fpsText.FillColor = new Color(180, 180, 180); + _fpsText.Position = new Vector2f(10, _window.Size.Y - 25); + + _fpsClock = new Clock(); + + // Performance text + _performanceText = new Text("", _font, 12); + _performanceText.FillColor = new Color(180, 180, 220); + _performanceText.Position = new Vector2f(_window.Size.X - 200, _window.Size.Y - 25); + + // Debug panel + InitializeDebugPanel(); + } + + private void InitializeDebugPanel() + { + _debugPanel = new RectangleShape(new Vector2f(300, 200)); + _debugPanel.Position = new Vector2f(_window.Size.X - 320, 20); + _debugPanel.FillColor = new Color(0, 0, 0, 200); + _debugPanel.OutlineThickness = 1; + _debugPanel.OutlineColor = Color.Cyan; + + _debugTexts = new List(); + for (int i = 0; i < 10; i++) + { + Text text = new Text("", _font, 12); + text.FillColor = Color.Cyan; + text.Position = new Vector2f(_window.Size.X - 310, 30 + i * 18); + _debugTexts.Add(text); + } + } + + public void Update(Car car, float deltaTime, float totalTime, long updateCount) + { + UpdateFPS(deltaTime); + UpdatePerformanceInfo(deltaTime); + + _instrumentPanel.Update(car); + + if (_showDebugInfo) + { + UpdateDebugInfo(car, deltaTime, totalTime, updateCount); + } + } + + private void UpdateFPS(float deltaTime) + { + _frameCount++; + _fpsAccumulator += deltaTime; + + if (_fpsAccumulator >= _fpsUpdateInterval) + { + float fps = _frameCount / _fpsAccumulator; + _fpsText.DisplayedString = $"FPS: {fps:F1}"; + + _frameCount = 0; + _fpsAccumulator = 0f; + } + } + + private void UpdatePerformanceInfo(float deltaTime) + { + // Track frame times for smoothing + _frameTimes.Enqueue(deltaTime * 1000); // Convert to milliseconds + if (_frameTimes.Count > FRAME_TIME_HISTORY) + { + _frameTimes.Dequeue(); + } + + // Calculate average frame time + float avgFrameTime = 0; + foreach (var time in _frameTimes) + { + avgFrameTime += time; + } + avgFrameTime /= _frameTimes.Count; + + _performanceText.DisplayedString = $"Frame: {deltaTime * 1000:F1}ms (Avg: {avgFrameTime:F1}ms)"; + } + + private void UpdateDebugInfo(Car car, float deltaTime, float totalTime, long updateCount) + { + int line = 0; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Time: {totalTime:F2}s"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Updates: {updateCount}"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Delta: {deltaTime:F4}s"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Position: ({car.Position.X:F1}, {car.Position.Y:F1})"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Velocity: {car.Velocity.Length:F2} m/s"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Engine Omega: {car.Engine.AngularVelocity:F2} rad/s"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Wheel Omega: {car.WheelSystem.AngularVelocity:F2} rad/s"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Clutch Eng: {car.Drivetrain.ClutchEngagement:P0}"; + + if (line < _debugTexts.Count) + _debugTexts[line++].DisplayedString = $"Force Clutch: {car.ForceClutch}"; + } + + public void Draw() + { + _window.Clear(_backgroundColor); + + _instrumentPanel.Draw(_window); + + // Draw FPS counter + _window.Draw(_fpsText); + + // Draw performance info + _window.Draw(_performanceText); + + // Draw debug panel if enabled + if (_showDebugInfo) + { + _window.Draw(_debugPanel); + foreach (var text in _debugTexts) + { + if (!string.IsNullOrEmpty(text.DisplayedString)) + _window.Draw(text); + } + } + + // Draw version info + DrawVersionInfo(); + } + + private void DrawVersionInfo() + { + Text versionText = new Text("Car Simulation v1.0", _font, 10); + versionText.FillColor = new Color(100, 100, 100); + versionText.Position = new Vector2f(_window.Size.X - 120, _window.Size.Y - 15); + _window.Draw(versionText); + } + + public void ToggleDebugInfo() + { + _showDebugInfo = !_showDebugInfo; + } + + public bool ShowDebugInfo => _showDebugInfo; + + public void HandleResize(uint width, uint height) + { + // Update FPS text position + _fpsText.Position = new Vector2f(10, height - 25); + + // Update performance text position + _performanceText.Position = new Vector2f(width - 200, height - 25); + + // Update debug panel position + _debugPanel.Position = new Vector2f(width - 320, 20); + + // Update debug text positions + for (int i = 0; i < _debugTexts.Count; i++) + { + _debugTexts[i].Position = new Vector2f(width - 310, 30 + i * 18); + } + + // You might want to reposition the instrument panel too + // For now, the instrument panel is fixed, but you could add resize handling + } + + public void DrawCustomMessage(string message, Vector2f position, uint fontSize, Color color) + { + Text customText = new Text(message, _font, fontSize); + customText.FillColor = color; + customText.Position = position; + _window.Draw(customText); + } + + public void DrawProgressBar(Vector2f position, Vector2f size, float progress, Color fillColor, Color bgColor) + { + // Draw background + RectangleShape background = new RectangleShape(size); + background.Position = position; + background.FillColor = bgColor; + background.OutlineThickness = 1; + background.OutlineColor = Color.White; + _window.Draw(background); + + // Draw fill + if (progress > 0) + { + RectangleShape fill = new RectangleShape(new Vector2f(size.X * Math.Clamp(progress, 0, 1), size.Y)); + fill.Position = position; + fill.FillColor = fillColor; + _window.Draw(fill); + } + + // Draw percentage text + Text progressText = new Text($"{progress:P0}", _font, 12); + progressText.FillColor = Color.White; + progressText.Position = new Vector2f( + position.X + size.X / 2 - progressText.GetLocalBounds().Width / 2, + position.Y + size.Y / 2 - progressText.CharacterSize / 2 + ); + _window.Draw(progressText); + } + + public void DrawGraph(string title, List data, Vector2f position, Vector2f size, Color lineColor) + { + if (data.Count < 2) return; + + // Draw graph background + RectangleShape graphBackground = new RectangleShape(size); + graphBackground.Position = position; + graphBackground.FillColor = new Color(0, 0, 0, 128); + graphBackground.OutlineThickness = 1; + graphBackground.OutlineColor = Color.White; + _window.Draw(graphBackground); + + // Draw title + Text titleText = new Text(title, _font, 12); + titleText.FillColor = Color.White; + titleText.Position = new Vector2f(position.X + 5, position.Y + 5); + _window.Draw(titleText); + + // Find min and max values + float minValue = data.Min(); + float maxValue = data.Max(); + float valueRange = Math.Max(maxValue - minValue, 0.001f); + + // Draw data points as line + VertexArray line = new VertexArray(PrimitiveType.LineStrip); + + for (int i = 0; i < data.Count; i++) + { + float x = position.X + (i / (float)(data.Count - 1)) * size.X; + float normalizedValue = (data[i] - minValue) / valueRange; + float y = position.Y + size.Y - normalizedValue * size.Y; + + line.Append(new Vertex(new Vector2f(x, y), lineColor)); + } + + _window.Draw(line); + + // Draw min/max labels + Text minText = new Text($"{minValue:F0}", _font, 10); + minText.FillColor = Color.White; + minText.Position = new Vector2f(position.X + size.X - 30, position.Y + size.Y - 15); + _window.Draw(minText); + + Text maxText = new Text($"{maxValue:F0}", _font, 10); + maxText.FillColor = Color.White; + maxText.Position = new Vector2f(position.X + size.X - 30, position.Y + 5); + _window.Draw(maxText); + } + } +} \ No newline at end of file diff --git a/Car simulation/UI/Instruments/Gauge.cs b/Car simulation/UI/Instruments/Gauge.cs new file mode 100644 index 0000000..7f9cb2b --- /dev/null +++ b/Car simulation/UI/Instruments/Gauge.cs @@ -0,0 +1,164 @@ +using SFML.Graphics; +using SFML.System; + +namespace Car_simulation.UI.Instruments +{ + public class Gauge + { + private RectangleShape _background; + private RectangleShape _needle; + private Font _font; + private Text _label; + private Text _valueText; + private Color _normalColor = Color.White; + private Color _warningColor = new Color(255, 100, 100); + + public Vector2f Position { get; set; } + public float Size { get; set; } = 200f; + + private float _value = 0f; + private float _minValue; + private float _maxValue; + private float _valuePerMark; + private float _linesPerMark; + private string _displayText; + + public Gauge(Font font, Vector2f position, float size, float minValue, float maxValue, float valuePerMark, string _displayText, float linesPerMark) + { + _minValue = minValue; + _maxValue = maxValue; + _linesPerMark = linesPerMark + _valuePerMark = valuePerMark; + _font = font; + Position = position; + Size = size; + Initialize(); + } + + private void Initialize() + { + _background = new RectangleShape(new Vector2f(Size, Size)); + _background.Position = Position; + _background.FillColor = new Color(40, 40, 50); + _background.OutlineThickness = 2; + _background.OutlineColor = Color.White; + + float needleLength = Size * 0.4f; + _needle = new RectangleShape(new Vector2f(needleLength, 4)); + _needle.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size / 2 + ); + _needle.FillColor = Color.Red; + _needle.Origin = new Vector2f(needleLength * 0.875f, 2); + + _label = new Text(_displayText, _font, (uint)(Size * 0.1f)); + _label.FillColor = Color.White; + _label.Position = new Vector2f( + Position.X + Size / 2 - _label.GetLocalBounds().Width / 2, + Position.Y + Size * 0.80f + ); + + _valueText = new Text("0", _font, (uint)(Size * 0.12f)); + _valueText.FillColor = _normalColor; + _valueText.Position = new Vector2f( + Position.X + Size / 2 - 20, + Position.Y + Size + ); + } + + public void Update(float value) + { + _value = value; + + _minValue + float ratio = Math.Clamp(_value / _maxValue, 0f, 1f); + float needleAngle = -45 + (270 * ratio); + + _needle.Rotation = needleAngle; + _valueText.DisplayedString = $"{_value:F0}"; + + FloatRect bounds = _valueText.GetLocalBounds(); + _valueText.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + _valueText.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size * 0.35f + ); + } + + public void Draw(RenderWindow window) + { + window.Draw(_background); + + DrawTickMarks(window); + + window.Draw(_needle); + window.Draw(_label); + window.Draw(_valueText); + } + + private void DrawTickMarks(RenderWindow window) + { + float targetAngle = 270; + int marks = (int)MathF.Ceiling(_maxValue / 1000) * 2; + + for (int i = marks; i >= 0; i--) + { + float angle = 135 + (i * (targetAngle / marks)); // 270° divided into 10 segments + float startRadius = Size * 0.45f; + float endRadius = Size * 0.42f; + + if (i % 2 == 0) // Major tick + { + endRadius = Size * 0.38f; + + float rpmValue = (i * 1000); + Text label = new Text($"{rpmValue / 2000:F0}", _font, 12); + label.FillColor = Color.White; + + float labelRadius = Size * 0.32f; + float _radAngle = angle * (float)Math.PI / 180f; + Vector2f labelPos = new Vector2f( + Position.X + Size / 2 + labelRadius * (float)Math.Cos(_radAngle), + Position.Y + Size / 2 + labelRadius * (float)Math.Sin(_radAngle) + ); + + FloatRect bounds = label.GetLocalBounds(); + label.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + label.Position = labelPos; + window.Draw(label); + } + + float radAngle = angle * (float)Math.PI / 180f; + Vector2f startPos = new Vector2f( + Position.X + Size / 2 + startRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + startRadius * (float)Math.Sin(radAngle) + ); + Vector2f endPos = new Vector2f( + Position.X + Size / 2 + endRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + endRadius * (float)Math.Sin(radAngle) + ); + + Vertex[] line = new Vertex[2] + { + new Vertex(startPos, Color.White), + new Vertex(endPos, Color.White) + }; + + window.Draw(line, PrimitiveType.Lines); + } + } + + public void SetPosition(Vector2f position) + { + Position = position; + Initialize(); // Re-initialize with new position + } + + public void SetSize(float size) + { + Size = size; + Initialize(); // Re-initialize with new size + } + } +} \ No newline at end of file diff --git a/Car simulation/UI/Instruments/InstrumentPanel.cs b/Car simulation/UI/Instruments/InstrumentPanel.cs new file mode 100644 index 0000000..71b3676 --- /dev/null +++ b/Car simulation/UI/Instruments/InstrumentPanel.cs @@ -0,0 +1,73 @@ +using Car_simulation.Core.Models; +using SFML.Graphics; +using SFML.System; + +namespace Car_simulation.UI.Instruments +{ + public class InstrumentPanel + { + private List _displayTexts = new List(); + private Tachometer _tachometer; + private Speedometer _speedometer; + private Font _font; + + private Color _textColor = Color.White; + + public InstrumentPanel(Font font, Vector2f tachometerPosition, Vector2f speedometerPosition) + { + _font = font; + _tachometer = new Tachometer(font, tachometerPosition, 200, 7000); + _speedometer = new Speedometer(font, speedometerPosition, 200); + InitializeDisplayTexts(); + } + + private void InitializeDisplayTexts() + { + for (int i = 0; i < 30; i++) + { + Text text = new Text("", _font, 16); + text.FillColor = Color.White; + text.Position = new Vector2f(20, 20 + i * 24); + _displayTexts.Add(text); + } + } + + public void Update(Car_simulation.Core.Models.Car car) + { + UpdateTextDisplay(car); + _tachometer.Update(car.Engine.RPM); + _speedometer.Update(car.Speed); + } + + private void UpdateTextDisplay(Car car) + { + var displayData = car.GetDisplayData(); + + // Just put each line in a text element + for (int i = 0; i < _displayTexts.Count; i++) + { + if (i < displayData.Count) + { + _displayTexts[i].DisplayedString = displayData[i]; + _displayTexts[i].FillColor = _textColor; + } + else + { + _displayTexts[i].DisplayedString = ""; + } + } + } + + public void Draw(RenderWindow window) + { + foreach (var text in _displayTexts) + { + if (!string.IsNullOrEmpty(text.DisplayedString)) + window.Draw(text); + } + + _tachometer.Draw(window); + _speedometer.Draw(window); + } + } +} \ No newline at end of file diff --git a/Car simulation/UI/Instruments/Speedometer.cs b/Car simulation/UI/Instruments/Speedometer.cs new file mode 100644 index 0000000..b069d6f --- /dev/null +++ b/Car simulation/UI/Instruments/Speedometer.cs @@ -0,0 +1,201 @@ +using SFML.Graphics; +using SFML.System; + +namespace Car_simulation.UI.Instruments +{ + public class Speedometer + { + private RectangleShape _background; + private RectangleShape _needle; + private Font _font; + private Text _label; + private Text _speedText; + private Text _gearText; + private Color _normalColor = Color.White; + private Color _highlightColor = new Color(0, 150, 255); + + public Vector2f Position { get; set; } + public float Size { get; set; } = 200f; + + private float _currentSpeed = 0f; + private string _currentGear = "N"; + private const float MAX_SPEED = 200f; // km/h + + public Speedometer(Font font) + { + _font = font; + Initialize(); + } + + public Speedometer(Font font, Vector2f position, float size) + { + _font = font; + Position = position; + Size = size; + Initialize(); + } + + private void Initialize() + { + // Background + _background = new RectangleShape(new Vector2f(Size, Size)); + _background.Position = Position; + _background.FillColor = new Color(40, 40, 50); + _background.OutlineThickness = 2; + _background.OutlineColor = Color.White; + + // Needle + float needleLength = Size * 0.4f; + _needle = new RectangleShape(new Vector2f(needleLength, 4)); + _needle.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size / 2 + ); + _needle.FillColor = Color.Green; + _needle.Origin = new Vector2f(needleLength * 0.875f, 2); + + // Labels + _label = new Text("SPEED", _font, (uint)(Size * 0.1f)); + _label.FillColor = Color.White; + _label.Position = new Vector2f( + Position.X + Size / 2 - _label.GetLocalBounds().Width / 2, + Position.Y + Size * 0.05f + ); + + _speedText = new Text("0 km/h", _font, (uint)(Size * 0.1f)); + _speedText.FillColor = _normalColor; + _speedText.Position = new Vector2f( + Position.X + Size / 2 - 40, + Position.Y + Size * 0.25f + ); + + _gearText = new Text($"GEAR {_currentGear}", _font, (uint)(Size * 0.14f)); + _gearText.FillColor = _highlightColor; + _gearText.Position = new Vector2f( + Position.X + Size * 0.5f, + Position.Y + Size * 1.1f + ); + } + + public void Update(float speed) + { + _currentSpeed = speed; + float speedKmh = speed * 3.6f; + + // Update needle angle (-90° to +180° rotation) + float speedRatio = Math.Clamp(speedKmh / MAX_SPEED, 0f, 1f); + float needleAngle = -90 + (270 * speedRatio); + _needle.Rotation = needleAngle; + + // Update speed text + _speedText.DisplayedString = $"{speedKmh:F1} km/h"; + + // Center speed text + FloatRect bounds = _speedText.GetLocalBounds(); + _speedText.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + _speedText.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size * 0.35f + ); + } + + public void UpdateGear(string gear) + { + _currentGear = gear; + _gearText.DisplayedString = $"GEAR {gear}"; + + // Center gear text + FloatRect bounds = _gearText.GetLocalBounds(); + _gearText.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + _gearText.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size * 1.1f + ); + } + + public void Draw(RenderWindow window) + { + window.Draw(_background); + + // Draw tick marks + DrawTickMarks(window); + + window.Draw(_needle); + window.Draw(_label); + window.Draw(_speedText); + window.Draw(_gearText); + } + + private void DrawTickMarks(RenderWindow window) + { + for (int i = 0; i <= 10; i++) + { + float angle = -90 + (i * 27); // 270° divided into 10 segments + float startRadius = Size * 0.45f; + float endRadius = Size * 0.42f; + + if (i % 2 == 0) // Major tick + { + endRadius = Size * 0.38f; + + // Add number label for major ticks + float speedValue = (i * MAX_SPEED / 10f); + Text label = new Text($"{speedValue:F0}", _font, 12); + label.FillColor = Color.White; + + float labelRadius = Size * 0.32f; + float _radAngle = angle * (float)Math.PI / 180f; + Vector2f labelPos = new Vector2f( + Position.X + Size / 2 + labelRadius * (float)Math.Cos(_radAngle), + Position.Y + Size / 2 + labelRadius * (float)Math.Sin(_radAngle) + ); + + FloatRect bounds = label.GetLocalBounds(); + label.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + label.Position = labelPos; + window.Draw(label); + } + + float radAngle = angle * (float)Math.PI / 180f; + Vector2f startPos = new Vector2f( + Position.X + Size / 2 + startRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + startRadius * (float)Math.Sin(radAngle) + ); + Vector2f endPos = new Vector2f( + Position.X + Size / 2 + endRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + endRadius * (float)Math.Sin(radAngle) + ); + + Vertex[] line = new Vertex[2] + { + new Vertex(startPos, Color.White), + new Vertex(endPos, Color.White) + }; + + window.Draw(line, PrimitiveType.Lines); + } + } + + public void SetPosition(Vector2f position) + { + Position = position; + Initialize(); // Re-initialize with new position + } + + public void SetSize(float size) + { + Size = size; + Initialize(); // Re-initialize with new size + } + + public void SetGearColor(Color color) + { + _gearText.FillColor = color; + } + + public void SetNeedleColor(Color color) + { + _needle.FillColor = color; + } + } +} \ No newline at end of file diff --git a/Car simulation/UI/Instruments/Tachometer.cs b/Car simulation/UI/Instruments/Tachometer.cs new file mode 100644 index 0000000..4653de8 --- /dev/null +++ b/Car simulation/UI/Instruments/Tachometer.cs @@ -0,0 +1,161 @@ +using SFML.Graphics; +using SFML.System; + +namespace Car_simulation.UI.Instruments +{ + public class Tachometer + { + private RectangleShape _background; + private RectangleShape _needle; + private Font _font; + private Text _label; + private Text _rpmText; + private Color _normalColor = Color.White; + private Color _warningColor = new Color(255, 100, 100); + + public Vector2f Position { get; set; } + public float Size { get; set; } = 200f; + + private float _currentRPM = 0f; + private float _maxRpm = 7000f; + + public Tachometer(Font font, Vector2f position, float size, float maxRpm) + { + _maxRpm = maxRpm; + _font = font; + Position = position; + Size = size; + Initialize(); + } + + private void Initialize() + { + // Background + _background = new RectangleShape(new Vector2f(Size, Size)); + _background.Position = Position; + _background.FillColor = new Color(40, 40, 50); + _background.OutlineThickness = 2; + _background.OutlineColor = Color.White; + + // Needle + float needleLength = Size * 0.4f; + _needle = new RectangleShape(new Vector2f(needleLength, 4)); + _needle.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size / 2 + ); + _needle.FillColor = Color.Red; + _needle.Origin = new Vector2f(needleLength * 0.875f, 2); // Offset origin to create pivot point + + // Labels + _label = new Text("RPM", _font, (uint)(Size * 0.1f)); + _label.FillColor = Color.White; + _label.Position = new Vector2f( + Position.X + Size / 2 - _label.GetLocalBounds().Width / 2, + Position.Y + Size * 0.80f + ); + + _rpmText = new Text("0", _font, (uint)(Size * 0.12f)); + _rpmText.FillColor = _normalColor; + _rpmText.Position = new Vector2f( + Position.X + Size / 2 - 20, + Position.Y + Size + ); + } + + public void Update(float rpm) + { + _currentRPM = rpm; + + float rpmRatio = Math.Clamp(rpm / _maxRpm, 0f, 1f); + float needleAngle = -45 + (270 * rpmRatio); + _needle.Rotation = needleAngle; + + // Update RPM text + _rpmText.DisplayedString = $"{rpm:F0}"; + + // Center RPM text + FloatRect bounds = _rpmText.GetLocalBounds(); + _rpmText.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + _rpmText.Position = new Vector2f( + Position.X + Size / 2, + Position.Y + Size * 0.35f + ); + } + + public void Draw(RenderWindow window) + { + window.Draw(_background); + + DrawTickMarks(window); + + window.Draw(_needle); + window.Draw(_label); + window.Draw(_rpmText); + } + + private void DrawTickMarks(RenderWindow window) + { + float targetAngle = 270; + int marks = (int)MathF.Ceiling(_maxRpm / 1000) * 2; + + for (int i = marks; i >= 0; i--) + { + float angle = 135 + (i * (targetAngle / marks)); // 270° divided into 10 segments + float startRadius = Size * 0.45f; + float endRadius = Size * 0.42f; + + if (i % 2 == 0) // Major tick + { + endRadius = Size * 0.38f; + + float rpmValue = (i * 1000); + Text label = new Text($"{rpmValue / 2000:F0}", _font, 12); + label.FillColor = Color.White; + + float labelRadius = Size * 0.32f; + float _radAngle = angle * (float)Math.PI / 180f; + Vector2f labelPos = new Vector2f( + Position.X + Size / 2 + labelRadius * (float)Math.Cos(_radAngle), + Position.Y + Size / 2 + labelRadius * (float)Math.Sin(_radAngle) + ); + + FloatRect bounds = label.GetLocalBounds(); + label.Origin = new Vector2f(bounds.Width / 2, bounds.Height / 2); + label.Position = labelPos; + window.Draw(label); + } + + float radAngle = angle * (float)Math.PI / 180f; + Vector2f startPos = new Vector2f( + Position.X + Size / 2 + startRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + startRadius * (float)Math.Sin(radAngle) + ); + Vector2f endPos = new Vector2f( + Position.X + Size / 2 + endRadius * (float)Math.Cos(radAngle), + Position.Y + Size / 2 + endRadius * (float)Math.Sin(radAngle) + ); + + Vertex[] line = new Vertex[2] + { + new Vertex(startPos, Color.White), + new Vertex(endPos, Color.White) + }; + + window.Draw(line, PrimitiveType.Lines); + } + } + + public void SetPosition(Vector2f position) + { + Position = position; + Initialize(); // Re-initialize with new position + } + + public void SetSize(float size) + { + Size = size; + Initialize(); // Re-initialize with new size + } + } +} \ No newline at end of file diff --git a/Car simulation/Vector2.cs b/Car simulation/Vector2.cs deleted file mode 100644 index 6f6ad9d..0000000 --- a/Car simulation/Vector2.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index c3393c1..0000000 --- a/Car simulation/WheelSystem.cs +++ /dev/null @@ -1,126 +0,0 @@ -namespace Car_simulation -{ - public class WheelSystem - { - // Physical properties - public float Radius { get; set; } = 0.3f; // meters - public float WheelInertia { get; set; } = 2.0f; // kg·m² per wheel - public float CarMass { get; set; } = 1500f; // kg - Car mass integrated into wheel system - - public int WheelCount { get; set; } = 4; - public int DrivenWheels { get; set; } = 2; // 2WD - - // State - public float TotalEnergy { get; set; } = 0f; // Joules (rotational + translational) - public float AngularVelocity => GetOmega(); - public float RPM => GetRPM(); - public float CarSpeed => GetCarSpeed(); // Now returns actual car speed - - public float ResistanceTorque { get; set; } = 0f; - - // Calculations - public float GetTotalRotationalInertia() - { - return WheelInertia * WheelCount; - } - - public float GetEquivalentCarInertia() - { - // Convert car mass to equivalent rotational inertia at wheels - // I = m * r² (from v = ω * r, so KE_translational = 0.5 * m * v² = 0.5 * m * (ωr)² = 0.5 * m * r² * ω²) - return CarMass * Radius * Radius; - } - - public float GetTotalInertia() - { - // Total inertia = rotational inertia of wheels + equivalent inertia of car mass - return GetTotalRotationalInertia() + GetEquivalentCarInertia(); - } - - public float GetOmega() - { - if (TotalEnergy <= 0 || GetTotalInertia() <= 0) return 0f; - return MathF.Sqrt(2f * TotalEnergy / GetTotalInertia()); - } - - public float GetRPM() - { - return AngularVelocity * PhysicsUtil.RAD_PER_SEC_TO_RPM; - } - - public float GetCarSpeed() - { - // v = ω * r (no slip assumed for base calculation) - return AngularVelocity * Radius; - } - - public float GetRotationalEnergy() - { - // Just the energy from wheel rotation - float omega = GetOmega(); - return 0.5f * GetTotalRotationalInertia() * omega * omega; - } - - public float GetTranslationalEnergy() - { - // Just the energy from car motion - float speed = GetCarSpeed(); - return 0.5f * CarMass * speed * speed; - } - - public float GetEnergyFromSpeed(float speed) - { - // Calculate total energy for given car speed - // Total energy = rotational energy of wheels + translational energy of car - float omega = speed / Radius; - float rotationalEnergy = 0.5f * GetTotalRotationalInertia() * omega * omega; - float translationalEnergy = 0.5f * CarMass * speed * speed; - return rotationalEnergy + translationalEnergy; - } - - public void SetSpeed(float speed) - { - TotalEnergy = GetEnergyFromSpeed(speed); - } - - // Apply work to the entire system (wheels + car) - public void ApplyWork(float work) - { - TotalEnergy += work; - TotalEnergy = Math.Max(TotalEnergy, 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) - { - // Static friction - 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; - TotalEnergy = Math.Max(energyNew, 0); - } - } -} \ No newline at end of file