major rework
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
using SFML.System;
|
using SFML.System;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Car_simulation
|
namespace Car_simulation.Audio
|
||||||
{
|
{
|
||||||
public class EngineSound : SoundStream
|
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 Engine Engine { get; private set; }
|
||||||
public WheelSystem WheelSystem { get; private set; }
|
public WheelSystem WheelSystem { get; private set; }
|
||||||
|
|
||||||
private int currentGear = 1;
|
private int _currentGear = 1;
|
||||||
public float[] GearRatios { get; set; } =
|
public float[] GearRatios { get; set; } = { 3.8f, 2.5f, 1.8f, 1.3f, 1.0f, 0.8f, 0.65f };
|
||||||
{
|
|
||||||
3.8f, // 1st
|
|
||||||
2.5f, // 2nd
|
|
||||||
1.8f, // 3rd
|
|
||||||
1.3f, // 4th
|
|
||||||
1.0f, // 5th
|
|
||||||
0.8f, // 6th
|
|
||||||
0.65f // 7th
|
|
||||||
};
|
|
||||||
public float FinalDriveRatio { get; set; } = 4.0f;
|
public float FinalDriveRatio { get; set; } = 4.0f;
|
||||||
public float Efficiency { get; set; } = 0.95f;
|
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 MaxClutchTorque { get; set; } = 400f;
|
||||||
public float ClutchStiffness { get; set; } = 50f;
|
public float ClutchStiffness { get; set; } = 50f;
|
||||||
|
|
||||||
// State
|
|
||||||
public float ClutchTorque { get; private set; }
|
public float ClutchTorque { get; private set; }
|
||||||
public float TransmittedPower { get; private set; }
|
public float TransmittedPower { get; private set; }
|
||||||
public float ClutchSlipRatio { get; private set; }
|
public float ClutchSlipRatio { get; private set; }
|
||||||
@@ -46,21 +36,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expected vs actual wheel speeds
|
|
||||||
float expectedWheelOmega = Engine.AngularVelocity / TotalRatio;
|
float expectedWheelOmega = Engine.AngularVelocity / TotalRatio;
|
||||||
float actualWheelOmega = WheelSystem.AngularVelocity;
|
float actualWheelOmega = WheelSystem.AngularVelocity;
|
||||||
float omegaDifference = actualWheelOmega - expectedWheelOmega;
|
float omegaDifference = actualWheelOmega - expectedWheelOmega;
|
||||||
|
|
||||||
// Calculate max torque clutch can transmit
|
|
||||||
float maxClutchTorque = MaxClutchTorque * ClutchEngagement;
|
float maxClutchTorque = MaxClutchTorque * ClutchEngagement;
|
||||||
|
|
||||||
// Simple spring model: torque tries to sync speeds
|
|
||||||
float desiredTorque = -omegaDifference * ClutchStiffness;
|
float desiredTorque = -omegaDifference * ClutchStiffness;
|
||||||
|
|
||||||
// Clamp to clutch capacity
|
|
||||||
desiredTorque = Math.Clamp(desiredTorque, -maxClutchTorque, maxClutchTorque);
|
desiredTorque = Math.Clamp(desiredTorque, -maxClutchTorque, maxClutchTorque);
|
||||||
|
|
||||||
// Also limit by engine capability when accelerating
|
|
||||||
if (desiredTorque > 0)
|
if (desiredTorque > 0)
|
||||||
{
|
{
|
||||||
float engineTorque = Engine.GetTorqueOutput() * Engine.GetActualThrottle();
|
float engineTorque = Engine.GetTorqueOutput() * Engine.GetActualThrottle();
|
||||||
@@ -70,53 +53,35 @@
|
|||||||
|
|
||||||
ClutchTorque = desiredTorque;
|
ClutchTorque = desiredTorque;
|
||||||
|
|
||||||
// Calculate energy transfer based on torque
|
|
||||||
float energyTransferred = 0f;
|
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;
|
energyTransferred = power * deltaTime;
|
||||||
|
|
||||||
// Wheels lose energy, engine gains (minus efficiency losses)
|
|
||||||
float wheelEnergyLoss = Math.Abs(energyTransferred);
|
float wheelEnergyLoss = Math.Abs(energyTransferred);
|
||||||
float engineEnergyGain = wheelEnergyLoss * Efficiency;
|
float engineEnergyGain = wheelEnergyLoss * Efficiency;
|
||||||
|
|
||||||
WheelSystem.TotalEnergy -= wheelEnergyLoss;
|
WheelSystem.TotalEnergy -= wheelEnergyLoss;
|
||||||
Engine.FlywheelEnergy += engineEnergyGain;
|
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;
|
||||||
float power = -ClutchTorque * Engine.AngularVelocity; // Negative torque, positive power
|
|
||||||
energyTransferred = power * deltaTime;
|
energyTransferred = power * deltaTime;
|
||||||
|
|
||||||
// Engine loses energy, wheels gain
|
|
||||||
float engineEnergyLoss = Math.Abs(energyTransferred);
|
float engineEnergyLoss = Math.Abs(energyTransferred);
|
||||||
float wheelEnergyGain = engineEnergyLoss * Efficiency;
|
float wheelEnergyGain = engineEnergyLoss * Efficiency;
|
||||||
|
|
||||||
Engine.FlywheelEnergy -= engineEnergyLoss;
|
Engine.FlywheelEnergy -= engineEnergyLoss;
|
||||||
WheelSystem.TotalEnergy += wheelEnergyGain;
|
WheelSystem.TotalEnergy += wheelEnergyGain;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// Nearly synchronized
|
|
||||||
energyTransferred = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate transmitted power
|
|
||||||
TransmittedPower = energyTransferred / deltaTime;
|
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)
|
if (maxClutchTorque > 0)
|
||||||
{
|
{
|
||||||
float torqueRatio = Math.Abs(ClutchTorque) / maxClutchTorque;
|
float torqueRatio = Math.Abs(ClutchTorque) / maxClutchTorque;
|
||||||
// If we're transmitting max torque, clutch is slipping
|
ClutchSlipRatio = torqueRatio;
|
||||||
// If we're transmitting less, clutch is gripping
|
|
||||||
ClutchSlipRatio = torqueRatio; // 0 = no slip, 1 = full slip
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -124,16 +89,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other methods...
|
|
||||||
public float GearRatio => GetCurrentGearRatio();
|
public float GearRatio => GetCurrentGearRatio();
|
||||||
public float TotalRatio => GearRatio * FinalDriveRatio;
|
public float TotalRatio => GearRatio * FinalDriveRatio;
|
||||||
|
|
||||||
private float GetCurrentGearRatio()
|
private float GetCurrentGearRatio()
|
||||||
{
|
{
|
||||||
if (currentGear == 0) return 0f;
|
if (_currentGear == 0) return 0f;
|
||||||
if (currentGear == -1) return -3.5f;
|
if (_currentGear == -1) return -3.5f;
|
||||||
if (currentGear > 0 && currentGear <= GearRatios.Length)
|
if (_currentGear > 0 && _currentGear <= GearRatios.Length)
|
||||||
return GearRatios[currentGear - 1];
|
return GearRatios[_currentGear - 1];
|
||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,20 +110,17 @@
|
|||||||
|
|
||||||
public string GetCurrentGearName()
|
public string GetCurrentGearName()
|
||||||
{
|
{
|
||||||
return currentGear switch
|
return _currentGear switch
|
||||||
{
|
{
|
||||||
-1 => "R",
|
-1 => "R",
|
||||||
0 => "N",
|
0 => "N",
|
||||||
_ => currentGear.ToString()
|
_ => _currentGear.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public float GetClutchSlipPercent()
|
public float GetClutchSlipPercent() => ClutchSlipRatio * 100f;
|
||||||
{
|
|
||||||
return ClutchSlipRatio * 100f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void GearUp() { if (currentGear < GearRatios.Length) currentGear++; }
|
public void GearUp() { if (_currentGear < GearRatios.Length) _currentGear++; }
|
||||||
public void GearDown() { if (currentGear > 1) 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;
|
namespace Car_simulation.Core.Physics
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Car_simulation
|
|
||||||
{
|
{
|
||||||
public class Util
|
|
||||||
{
|
|
||||||
public static float Lerp(float a, float b, float t)
|
|
||||||
{
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PhysicsUtil
|
public static class PhysicsUtil
|
||||||
{
|
{
|
||||||
public const float G = 9.81f;
|
public const float G = 9.81f;
|
||||||
public const float AirDensity = 1.225f;
|
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)
|
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 RPMToOmega(float rpm) => rpm * MathF.PI * 2f / 60f;
|
||||||
public static float OmegaToRPM(float omega) => omega * 60f / (2f * MathF.PI);
|
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)
|
public static float CalculateRotationalEnergy(float inertia, float omega)
|
||||||
{
|
{
|
||||||
return 0.5f * inertia * omega * omega;
|
return 0.5f * inertia * omega * omega;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate omega from energy: ω = sqrt(2E / I)
|
|
||||||
public static float CalculateOmegaFromEnergy(float energy, float inertia)
|
public static float CalculateOmegaFromEnergy(float energy, float inertia)
|
||||||
{
|
{
|
||||||
if (energy <= 0) return 0;
|
if (energy <= 0) return 0;
|
||||||
return MathF.Sqrt(2f * energy / inertia);
|
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,57 +1,91 @@
|
|||||||
using Car_simulation;
|
using Car_simulation.Core.Models;
|
||||||
using SFML.Window;
|
using Car_simulation.Input;
|
||||||
|
using Car_simulation.UI;
|
||||||
using SFML.Graphics;
|
using SFML.Graphics;
|
||||||
using SFML.System;
|
using SFML.System;
|
||||||
using System.Collections.Generic;
|
using SFML.Window;
|
||||||
using System;
|
|
||||||
|
|
||||||
internal class Program
|
namespace Car_simulation
|
||||||
{
|
{
|
||||||
Car car = new Car();
|
internal class Program
|
||||||
private bool _isRunning = true;
|
{
|
||||||
|
private Car _car = new Car();
|
||||||
private RenderWindow _window;
|
private RenderWindow _window;
|
||||||
private Font _font;
|
private Font _font;
|
||||||
private List<Text> _displayTexts = new List<Text>();
|
private DisplayManager _displayManager;
|
||||||
private RectangleShape _tachometerBackground;
|
private CarInputHandler _inputHandler;
|
||||||
private RectangleShape _tachometerNeedle;
|
|
||||||
private RectangleShape _speedometerBackground;
|
|
||||||
private RectangleShape _speedometerNeedle;
|
|
||||||
|
|
||||||
// Colors
|
// Timing
|
||||||
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 Clock _clock = new Clock();
|
||||||
private Time _timePerUpdate = Time.FromSeconds(1.0f / 60.0f); // 60 FPS physics
|
private Time _timePerUpdate = Time.FromSeconds(1.0f / 60.0f);
|
||||||
private Time _accumulatedTime = Time.Zero;
|
private Time _accumulatedTime = Time.Zero;
|
||||||
private float _totalTime = 0.0f;
|
private float _totalTime = 0.0f;
|
||||||
private long _updateCount = 0;
|
private long _updateCount = 0;
|
||||||
|
|
||||||
private Dictionary<Keyboard.Key, bool> _previousKeyStates = new Dictionary<Keyboard.Key, bool>();
|
static void Main(string[] args)
|
||||||
private Dictionary<Keyboard.Key, bool> _currentKeyStates = new Dictionary<Keyboard.Key, bool>();
|
|
||||||
|
|
||||||
private static void Main(string[] args)
|
|
||||||
{
|
{
|
||||||
Program program = new Program();
|
Program program = new Program();
|
||||||
program.Run();
|
program.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Run()
|
private void Run()
|
||||||
|
{
|
||||||
|
InitializeWindow();
|
||||||
|
InitializeUI();
|
||||||
|
InitializeInput();
|
||||||
|
|
||||||
|
while (_window.IsOpen)
|
||||||
|
{
|
||||||
|
_window.DispatchEvents();
|
||||||
|
|
||||||
|
Time elapsed = _clock.Restart();
|
||||||
|
_accumulatedTime += elapsed;
|
||||||
|
|
||||||
|
while (_accumulatedTime >= _timePerUpdate)
|
||||||
|
{
|
||||||
|
Update(_timePerUpdate.AsSeconds());
|
||||||
|
_accumulatedTime -= _timePerUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
_window.Close();
|
||||||
|
Console.WriteLine($"\nSimulation stopped after {_updateCount} updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeWindow()
|
||||||
{
|
{
|
||||||
_window = new RenderWindow(new VideoMode(1000, 800), "Car Simulation", Styles.Default);
|
_window = new RenderWindow(new VideoMode(1000, 800), "Car Simulation", Styles.Default);
|
||||||
_window.SetVisible(true);
|
_window.SetVisible(true);
|
||||||
_window.SetFramerateLimit(60);
|
_window.SetFramerateLimit(60);
|
||||||
_window.SetKeyRepeatEnabled(false);
|
_window.SetKeyRepeatEnabled(false);
|
||||||
|
|
||||||
_window.Closed += (sender, e) => _isRunning = false;
|
_window.Closed += (sender, e) => _window.Close();
|
||||||
|
_window.Resized += OnWindowResized;
|
||||||
_window.KeyPressed += OnKeyPressed;
|
_window.KeyPressed += OnKeyPressed;
|
||||||
_window.KeyReleased += OnKeyReleased;
|
}
|
||||||
|
|
||||||
// Load font
|
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
|
try
|
||||||
{
|
{
|
||||||
_font = new Font("arial.ttf");
|
_font = new Font("arial.ttf");
|
||||||
@@ -61,277 +95,63 @@ internal class Program
|
|||||||
_font = new Font("C:/Windows/Fonts/arial.ttf");
|
_font = new Font("C:/Windows/Fonts/arial.ttf");
|
||||||
}
|
}
|
||||||
|
|
||||||
InitializeDisplay();
|
_displayManager = new DisplayManager(_window, _font);
|
||||||
InitializeTrackedKeys();
|
}
|
||||||
|
|
||||||
_clock.Restart();
|
private void InitializeInput()
|
||||||
|
|
||||||
while (_isRunning && _window.IsOpen)
|
|
||||||
{
|
{
|
||||||
_window.DispatchEvents();
|
_inputHandler = new CarInputHandler();
|
||||||
|
_window.KeyPressed += (sender, e) => _inputHandler.OnKeyPressed(e);
|
||||||
|
_window.KeyReleased += (sender, e) => _inputHandler.OnKeyReleased(e);
|
||||||
|
}
|
||||||
|
|
||||||
Time elapsed = _clock.Restart();
|
private void Update(float deltaTime)
|
||||||
_accumulatedTime += elapsed;
|
|
||||||
|
|
||||||
while (_accumulatedTime >= _timePerUpdate)
|
|
||||||
{
|
{
|
||||||
ProcessInput(_timePerUpdate.AsSeconds());
|
_inputHandler.ProcessInput(deltaTime);
|
||||||
_totalTime += _timePerUpdate.AsSeconds();
|
_totalTime += deltaTime;
|
||||||
car.Update(_timePerUpdate.AsSeconds(), _totalTime);
|
|
||||||
_accumulatedTime -= _timePerUpdate;
|
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++;
|
_updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplay();
|
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();
|
_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