major rework

This commit is contained in:
max
2026-02-16 18:32:48 +01:00
parent bbd82da07e
commit 932734e5b4
24 changed files with 1706 additions and 893 deletions

View File

@@ -2,7 +2,7 @@
using SFML.System;
using System;
namespace Car_simulation
namespace Car_simulation.Audio
{
public class EngineSound : SoundStream
{

View File

@@ -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<string> GetDisplayData()
{
return new List<string>
{
$"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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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--; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Car_simulation.Core.Components
{
public interface ICarComponent
{
void Update(float deltaTime);
}
}

View File

@@ -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
}
}
}

View File

@@ -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<string> GetDisplayData()
{
return new List<string>
{
$"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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<float, float> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,83 @@
using SFML.Window;
namespace Car_simulation.Input
{
public class CarInputHandler
{
private Dictionary<Keyboard.Key, bool> _previousKeyStates = new();
private Dictionary<Keyboard.Key, bool> _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; }
}
}

View File

@@ -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<Text> _displayTexts = new List<Text>();
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<Keyboard.Key, bool> _previousKeyStates = new Dictionary<Keyboard.Key, bool>();
private Dictionary<Keyboard.Key, bool> _currentKeyStates = new Dictionary<Keyboard.Key, bool>();
private static void Main(string[] args)
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<Keyboard.Key>(_currentKeyStates.Keys);
foreach (var key in keys)
{
if (_previousKeyStates.ContainsKey(key))
_previousKeyStates[key] = _currentKeyStates[key];
}
}
}

View File

@@ -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<Text> _debugTexts;
// Performance tracking
private Queue<float> _frameTimes = new Queue<float>();
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<Text>();
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<float> 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);
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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<Text> _displayTexts = new List<Text>();
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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}