major rework
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
using SFML.System;
|
||||
using System;
|
||||
|
||||
namespace Car_simulation
|
||||
namespace Car_simulation.Audio
|
||||
{
|
||||
public class EngineSound : SoundStream
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Car simulation/Core/Components/Aerodynamics.cs
Normal file
29
Car simulation/Core/Components/Aerodynamics.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Car simulation/Core/Components/BrakeSystem.cs
Normal file
30
Car simulation/Core/Components/BrakeSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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--; }
|
||||
}
|
||||
}
|
||||
163
Car simulation/Core/Components/Engine.cs
Normal file
163
Car simulation/Core/Components/Engine.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Car simulation/Core/Components/ICarComponent.cs
Normal file
7
Car simulation/Core/Components/ICarComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Car_simulation.Core.Components
|
||||
{
|
||||
public interface ICarComponent
|
||||
{
|
||||
void Update(float deltaTime);
|
||||
}
|
||||
}
|
||||
95
Car simulation/Core/Components/WheelSystem.cs
Normal file
95
Car simulation/Core/Components/WheelSystem.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
153
Car simulation/Core/Models/Car.cs
Normal file
153
Car simulation/Core/Models/Car.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Car simulation/Core/Models/CarState.cs
Normal file
14
Car simulation/Core/Models/CarState.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Car simulation/Core/Physics/ResistanceCalculator.cs
Normal file
25
Car simulation/Core/Physics/ResistanceCalculator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Car simulation/Core/Physics/Vector2.cs
Normal file
38
Car simulation/Core/Physics/Vector2.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
83
Car simulation/Inputs/CarInputHandler.cs
Normal file
83
Car simulation/Inputs/CarInputHandler.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
306
Car simulation/UI/DisplayManager.cs
Normal file
306
Car simulation/UI/DisplayManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
164
Car simulation/UI/Instruments/Gauge.cs
Normal file
164
Car simulation/UI/Instruments/Gauge.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Car simulation/UI/Instruments/InstrumentPanel.cs
Normal file
73
Car simulation/UI/Instruments/InstrumentPanel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
201
Car simulation/UI/Instruments/Speedometer.cs
Normal file
201
Car simulation/UI/Instruments/Speedometer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
161
Car simulation/UI/Instruments/Tachometer.cs
Normal file
161
Car simulation/UI/Instruments/Tachometer.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user