Add project files.
This commit is contained in:
3
Car simulation.slnx
Normal file
3
Car simulation.slnx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Car simulation/Car simulation.csproj" />
|
||||||
|
</Solution>
|
||||||
15
Car simulation/Car simulation.csproj
Normal file
15
Car simulation/Car simulation.csproj
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RootNamespace>Car_simulation</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SFML.Net" Version="2.6.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
221
Car simulation/Car.cs
Normal file
221
Car simulation/Car.cs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
using static SFML.Window.Mouse;
|
||||||
|
|
||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class Car
|
||||||
|
{
|
||||||
|
public Vector2 Position = new Vector2(0, 0);
|
||||||
|
public Vector2 Velocity = new Vector2(0, 0);
|
||||||
|
public float Speed => Velocity.Length;
|
||||||
|
|
||||||
|
public float Mass = 1500f; // kg
|
||||||
|
public int WheelCount = 4;
|
||||||
|
public int DrivenWheels = 2;
|
||||||
|
|
||||||
|
public float ThrottleInput = 0f;
|
||||||
|
public float BrakeInput = 0f;
|
||||||
|
public float ClutchInput = 1f; // 0 = engaged, 1 = disengaged
|
||||||
|
public bool ForceClutch = false;
|
||||||
|
public float SteeringInput = 0f;
|
||||||
|
|
||||||
|
// Aerodynamics
|
||||||
|
private const float AirDensity = 1.225f;
|
||||||
|
public float DragCoefficient = 0.1f;
|
||||||
|
public float FrontalArea = 2.2f; // m²
|
||||||
|
public float RollingResistanceCoefficient = 0.015f;
|
||||||
|
|
||||||
|
// Components
|
||||||
|
public Engine Engine;
|
||||||
|
public Drivetrain Drivetrain;
|
||||||
|
public WheelSystem WheelSystem;
|
||||||
|
|
||||||
|
private EngineSound _engineSound;
|
||||||
|
private bool _audioEnabled = true;
|
||||||
|
|
||||||
|
public Car()
|
||||||
|
{
|
||||||
|
Engine = new Engine();
|
||||||
|
WheelSystem = new WheelSystem();
|
||||||
|
Drivetrain = new Drivetrain(Engine, WheelSystem);
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
WheelSystem.WheelCount = WheelCount;
|
||||||
|
WheelSystem.DrivenWheels = DrivenWheels;
|
||||||
|
|
||||||
|
InitializeAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeAudio()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_engineSound = new EngineSound();
|
||||||
|
_engineSound.SetEngineState(Engine.IdleRPM, 0f);
|
||||||
|
_engineSound.StartSound();
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Audio initialization failed: {ex.Message}");
|
||||||
|
_audioEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(float deltaTime)
|
||||||
|
{
|
||||||
|
Engine.Throttle = ThrottleInput;
|
||||||
|
Drivetrain.ClutchEngagement = 1f - ClutchInput; // Convert: 0 input = 1 engagement
|
||||||
|
|
||||||
|
if (ForceClutch)
|
||||||
|
Drivetrain.ClutchEngagement = 0f;
|
||||||
|
|
||||||
|
float resistanceTorque = CalculateResistanceTorque();
|
||||||
|
WheelSystem.ResistanceTorque = resistanceTorque;
|
||||||
|
|
||||||
|
Drivetrain.Update(deltaTime);
|
||||||
|
WheelSystem.ApplyResistance(deltaTime);
|
||||||
|
|
||||||
|
float engineLoad = Drivetrain.CalculateEngineLoad(deltaTime);
|
||||||
|
Engine.Update(deltaTime, engineLoad);
|
||||||
|
|
||||||
|
UpdateVehicleMotion(deltaTime);
|
||||||
|
ApplyBraking(deltaTime);
|
||||||
|
|
||||||
|
if (_audioEnabled)
|
||||||
|
{
|
||||||
|
UpdateAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAudio()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float throttle = Engine.GetActualThrottle();
|
||||||
|
_engineSound.SetEngineState(Engine.RPM, throttle);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Audio update error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVehicleMotion(float deltaTime)
|
||||||
|
{
|
||||||
|
// Calculate net force
|
||||||
|
float tractiveForce = CalculateTractiveForce();
|
||||||
|
float resistanceForce = CalculateTotalResistanceForce();
|
||||||
|
float netForce = tractiveForce - resistanceForce;
|
||||||
|
|
||||||
|
// Calculate acceleration: a = F / m
|
||||||
|
float acceleration = netForce / Mass;
|
||||||
|
|
||||||
|
// Update velocity: v = v₀ + a·Δt
|
||||||
|
if (Velocity.Length > 0)
|
||||||
|
{
|
||||||
|
Vector2 direction = Velocity.Normalized();
|
||||||
|
float newSpeed = Velocity.Length + acceleration * deltaTime;
|
||||||
|
newSpeed = Math.Max(newSpeed, 0); // Don't go backwards without reverse gear
|
||||||
|
Velocity = direction * newSpeed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Starting from standstill
|
||||||
|
Velocity = new Vector2(acceleration * deltaTime, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Position += Velocity * deltaTime;
|
||||||
|
|
||||||
|
// Sync wheel speed with actual vehicle speed (with slip allowance)
|
||||||
|
float currentWheelSpeed = Velocity.Length;
|
||||||
|
WheelSystem.SetSpeed(currentWheelSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateTractiveForce()
|
||||||
|
{
|
||||||
|
// 1. Get the torque available at the wheels
|
||||||
|
float wheelTorque = Drivetrain.ClutchTorque * Drivetrain.Efficiency;
|
||||||
|
|
||||||
|
// 2. Convert torque to theoretical force: F = τ / r
|
||||||
|
float theoreticalForce = wheelTorque / WheelSystem.Radius;
|
||||||
|
|
||||||
|
// 3. Account for weight distribution and driven wheels
|
||||||
|
// Normal load on driven wheels = (DrivenWheels / WheelCount) * Weight
|
||||||
|
float drivenWheelNormalLoad = (DrivenWheels / (float)WheelCount) * Mass * 9.81f;
|
||||||
|
|
||||||
|
// 4. Calculate maximum tractive force based on friction (tire grip)
|
||||||
|
float frictionCoefficient = 1.2f; // Typical tire on dry asphalt
|
||||||
|
float maxTractiveForce = drivenWheelNormalLoad * frictionCoefficient;
|
||||||
|
|
||||||
|
// 5. Limit the force by what the tires can actually grip
|
||||||
|
// Also handle direction (forward/reverse)
|
||||||
|
if (theoreticalForce > 0)
|
||||||
|
{
|
||||||
|
return Math.Min(theoreticalForce, maxTractiveForce);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For reverse or engine braking
|
||||||
|
return Math.Max(theoreticalForce, -maxTractiveForce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBraking(float deltaTime)
|
||||||
|
{
|
||||||
|
if (BrakeInput <= 0) return;
|
||||||
|
|
||||||
|
float brakeTorque = BrakeInput * 500f; // 500 Nm max brake torque
|
||||||
|
|
||||||
|
WheelSystem.ApplyTorque(-brakeTorque, deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float CalculateTotalResistanceForce()
|
||||||
|
{
|
||||||
|
float dragForce = CalculateDragForce();
|
||||||
|
float rollingForce = CalculateRollingResistanceForce();
|
||||||
|
return dragForce + rollingForce;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateDragForce()
|
||||||
|
{
|
||||||
|
// F_drag = 0.5 * ρ * Cd * A * v²
|
||||||
|
float speed = Speed;
|
||||||
|
return 0.5f * AirDensity * DragCoefficient * FrontalArea * speed * speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateRollingResistanceForce()
|
||||||
|
{
|
||||||
|
// F_rolling = C_r * m * g
|
||||||
|
return RollingResistanceCoefficient * Mass * 9.81f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert resistance force to wheel torque
|
||||||
|
public float CalculateResistanceTorque()
|
||||||
|
{
|
||||||
|
float totalForce = CalculateTotalResistanceForce();
|
||||||
|
return totalForce * WheelSystem.Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisplayUpdate()
|
||||||
|
{
|
||||||
|
Console.SetCursorPosition(0, 0);
|
||||||
|
Console.WriteLine($"Engine Energy: {Engine.FlywheelEnergy,7:F0} J");
|
||||||
|
Console.WriteLine($"Engine Torque: {Engine.GetTorqueOutput(),7:F0} Nm");
|
||||||
|
Console.WriteLine($"Engine RPM: {Engine.RPM,7:F0}");
|
||||||
|
Console.WriteLine($"Wheel Energy: {WheelSystem.WheelEnergy,7:F0} J");
|
||||||
|
Console.WriteLine($"Wheel RPM: {WheelSystem.RPM,7:F0}");
|
||||||
|
Console.WriteLine($"Vehicle: {Speed * 3.6f,7:F1} km/h");
|
||||||
|
Console.WriteLine($"Throttle: {Engine.GetActualThrottle() * 100,6:F1}%");
|
||||||
|
Console.WriteLine($"Power: {Engine.CurrentPower / 1000,6:F1} kW");
|
||||||
|
Console.WriteLine($"Transmitted: {Drivetrain.TransmittedPower / 1000,6:F1} kW");
|
||||||
|
Console.WriteLine($"Brake: {BrakeInput * 100,6:F1}%");
|
||||||
|
Console.WriteLine($"Clutch: {ClutchInput * 100,6:F1}% disengaged");
|
||||||
|
Console.WriteLine($"Speed Diff: {Drivetrain.GetSpeedDifferenceRPM(),6:F0} RPM");
|
||||||
|
Console.WriteLine($"Clutch T: {Drivetrain.ClutchTorque,6:F0} Nm");
|
||||||
|
Console.WriteLine($"Resistance: {CalculateTotalResistanceForce(),6:F1} N");
|
||||||
|
Console.WriteLine($"Drag: {CalculateDragForce(),6:F1} N");
|
||||||
|
Console.WriteLine($"Rolling: {CalculateRollingResistanceForce(),6:F1} N");
|
||||||
|
Console.WriteLine($"Gear: {Drivetrain.GetCurrentGearName(),3} (Ratio: {Drivetrain.GearRatio:F2}:1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
Car simulation/Drivetrain.cs
Normal file
206
Car simulation/Drivetrain.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class Drivetrain
|
||||||
|
{
|
||||||
|
// Connected components
|
||||||
|
public Engine Engine { get; private set; }
|
||||||
|
public WheelSystem WheelSystem { get; private set; }
|
||||||
|
|
||||||
|
private int currentGear = 1;
|
||||||
|
public float[] GearRatios { get; set; } =
|
||||||
|
{
|
||||||
|
3.8f, // 1st - Lower for better launch
|
||||||
|
2.5f, // 2nd
|
||||||
|
1.8f, // 3rd
|
||||||
|
1.3f, // 4th
|
||||||
|
1.0f, // 5th - Direct drive
|
||||||
|
0.8f, // 6th - Overdrive
|
||||||
|
0.65f // 7th - Double overdrive (optional)
|
||||||
|
};
|
||||||
|
public float FinalDriveRatio { get; set; } = 5.0f;
|
||||||
|
public float Efficiency { get; set; } = 0.95f;
|
||||||
|
public float ClutchEngagement { get; set; } = 0f; // 0 = disengaged, 1 = fully engaged
|
||||||
|
|
||||||
|
// Calculated
|
||||||
|
public float GearRatio => GetCurrentGearRatio();
|
||||||
|
public float TotalRatio => GearRatio * FinalDriveRatio;
|
||||||
|
|
||||||
|
// Clutch properties
|
||||||
|
public float ClutchStiffness { get; set; } = 500f; // Nm/(rad/s) - how strongly clutch pulls speeds together
|
||||||
|
public float MaxClutchTorque { get; set; } = 4500f; // Maximum torque clutch can transmit
|
||||||
|
|
||||||
|
// State
|
||||||
|
public float SpeedDifference { get; private set; } // rad/s
|
||||||
|
public float ClutchTorque { get; private set; }
|
||||||
|
public float TransmittedPower { get; private set; }
|
||||||
|
|
||||||
|
private float previousWheelOmega = 0f;
|
||||||
|
|
||||||
|
public Drivetrain(Engine engine, WheelSystem wheelSystem)
|
||||||
|
{
|
||||||
|
Engine = engine;
|
||||||
|
WheelSystem = wheelSystem;
|
||||||
|
previousWheelOmega = wheelSystem.AngularVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GearUp()
|
||||||
|
{
|
||||||
|
if (currentGear < GearRatios.Length)
|
||||||
|
currentGear++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GearDown()
|
||||||
|
{
|
||||||
|
if (currentGear > 1)
|
||||||
|
currentGear--;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetCurrentGearRatio()
|
||||||
|
{
|
||||||
|
if (currentGear == 0) return 0f; // Neutral
|
||||||
|
if (currentGear == -1) return -3.5f; // Reverse (example ratio)
|
||||||
|
if (currentGear > 0 && currentGear <= GearRatios.Length)
|
||||||
|
return GearRatios[currentGear - 1];
|
||||||
|
return 0f; // Invalid gear
|
||||||
|
}
|
||||||
|
|
||||||
|
public float CalculateSpeedDifference()
|
||||||
|
{
|
||||||
|
if (TotalRatio == 0) return 0f;
|
||||||
|
|
||||||
|
float engineOmega = Engine.AngularVelocity;
|
||||||
|
float wheelOmega = WheelSystem.AngularVelocity;
|
||||||
|
float expectedWheelOmega = engineOmega / TotalRatio;
|
||||||
|
|
||||||
|
SpeedDifference = wheelOmega - expectedWheelOmega;
|
||||||
|
return SpeedDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float CalculateClutchTorque()
|
||||||
|
{
|
||||||
|
if (ClutchEngagement <= 0.01f)
|
||||||
|
{
|
||||||
|
ClutchTorque = 0;
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalculateSpeedDifference();
|
||||||
|
|
||||||
|
float torque = -SpeedDifference * ClutchStiffness * ClutchEngagement;
|
||||||
|
torque = Math.Clamp(torque, -MaxClutchTorque, MaxClutchTorque);
|
||||||
|
|
||||||
|
float actualThrottle = Engine.GetActualThrottle();
|
||||||
|
float availableEngineTorque = Engine.GetTorqueOutput();
|
||||||
|
|
||||||
|
float maxTorqueAtClutch = maxEngineTorque * TotalRatio * Efficiency;
|
||||||
|
|
||||||
|
torque = maxTorqueAtClutch;
|
||||||
|
|
||||||
|
|
||||||
|
ClutchTorque = torque;
|
||||||
|
return torque;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyDrivetrainWork(float deltaTime)
|
||||||
|
{
|
||||||
|
if (ClutchEngagement <= 0.01f || TotalRatio == 0)
|
||||||
|
{
|
||||||
|
ClutchTorque = 0;
|
||||||
|
TransmittedPower = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalculateSpeedDifference();
|
||||||
|
float clutchTorque = CalculateClutchTorque();
|
||||||
|
|
||||||
|
bool engineDrivingWheels = clutchTorque > 0;
|
||||||
|
bool wheelsDrivingEngine = clutchTorque < 0;
|
||||||
|
|
||||||
|
if (engineDrivingWheels)
|
||||||
|
{
|
||||||
|
// Engine -> Wheels (normal driving)
|
||||||
|
ApplyEngineToWheels(clutchTorque, deltaTime);
|
||||||
|
}
|
||||||
|
else if (wheelsDrivingEngine)
|
||||||
|
{
|
||||||
|
// Wheels -> Engine (engine braking)
|
||||||
|
ApplyWheelsToEngine(clutchTorque, deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransmittedPower = clutchTorque * SpeedDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyEngineToWheels(float clutchTorque, float deltaTime)
|
||||||
|
{
|
||||||
|
// Existing logic for engine driving wheels
|
||||||
|
float netWheelTorque = clutchTorque * Efficiency - WheelSystem.ResistanceTorque;
|
||||||
|
float netEngineTorque = -clutchTorque / TotalRatio;
|
||||||
|
|
||||||
|
// Apply to both
|
||||||
|
Engine.ApplyTorque(netEngineTorque, deltaTime);
|
||||||
|
WheelSystem.ApplyTorque(netWheelTorque, deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyWheelsToEngine(float clutchTorque, float deltaTime)
|
||||||
|
{
|
||||||
|
// Wheels driving engine (engine braking)
|
||||||
|
// Negative clutchTorque means wheels are trying to spin engine faster
|
||||||
|
|
||||||
|
float wheelTorque = clutchTorque; // Negative value
|
||||||
|
float engineTorque = -clutchTorque / TotalRatio; // Positive resistance
|
||||||
|
|
||||||
|
// Apply resistance to wheels
|
||||||
|
WheelSystem.ApplyTorque(wheelTorque, deltaTime);
|
||||||
|
|
||||||
|
Engine.ApplyTorque(-engineTorque, deltaTime); // Negative = slowing
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetEquivalentInertiaAtEngine()
|
||||||
|
{
|
||||||
|
float wheelInertia = WheelSystem.GetTotalInertia();
|
||||||
|
return Engine.MomentOfInertia + (wheelInertia * TotalRatio * TotalRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float CalculateEngineLoad(float deltaTime)
|
||||||
|
{
|
||||||
|
if (ClutchEngagement <= 0.01f) return 0f;
|
||||||
|
|
||||||
|
float wheelResistanceTorque = WheelSystem.ResistanceTorque;
|
||||||
|
float engineLoadTorque = wheelResistanceTorque / (TotalRatio * Efficiency);
|
||||||
|
|
||||||
|
float inertiaLoad = CalculateInertiaLoad(deltaTime);
|
||||||
|
|
||||||
|
return engineLoadTorque + inertiaLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateInertiaLoad(float deltaTime)
|
||||||
|
{
|
||||||
|
float wheelAlpha = (WheelSystem.AngularVelocity - previousWheelOmega) / deltaTime;
|
||||||
|
previousWheelOmega = WheelSystem.AngularVelocity;
|
||||||
|
|
||||||
|
float inertiaTorque = wheelAlpha * WheelSystem.GetTotalInertia();
|
||||||
|
return inertiaTorque / (TotalRatio * TotalRatio * Efficiency);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(float deltaTime)
|
||||||
|
{
|
||||||
|
ApplyDrivetrainWork(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
public float GetSpeedDifferenceRPM()
|
||||||
|
{
|
||||||
|
return SpeedDifference * PhysicsUtil.RAD_PER_SEC_TO_RPM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentGearName()
|
||||||
|
{
|
||||||
|
return currentGear switch
|
||||||
|
{
|
||||||
|
-1 => "R",
|
||||||
|
0 => "N",
|
||||||
|
_ => currentGear.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
Car simulation/Engine.cs
Normal file
143
Car simulation/Engine.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class Engine
|
||||||
|
{
|
||||||
|
// Energy state
|
||||||
|
public float FlywheelEnergy { get; set; } // Joules
|
||||||
|
|
||||||
|
// Values
|
||||||
|
public float RPM => GetRPM();
|
||||||
|
public float AngularVelocity => GetOmega();
|
||||||
|
public float CurrentPower { get; private set; }
|
||||||
|
|
||||||
|
// Physical properties
|
||||||
|
public float MomentOfInertia { get; set; } = 0.25f; // kg·m²
|
||||||
|
public float IdleRPM { get; set; } = 800f;
|
||||||
|
public float StallSpeed { get; set; } = 200f;
|
||||||
|
public float Throttle { get; set; } = 0f;
|
||||||
|
public bool IsRunning => RPM > StallSpeed;
|
||||||
|
|
||||||
|
// Torque characteristics
|
||||||
|
public Dictionary<float, float> TorqueCurve { get; set; } = new()
|
||||||
|
{
|
||||||
|
// RPM - Torque Nm
|
||||||
|
{ 0f, 0f },
|
||||||
|
{ 800f, 150f }, // Idle
|
||||||
|
{ 2000f, 200f }, // Peak torque
|
||||||
|
{ 4500f, 250f },
|
||||||
|
{ 7200f, 250f },
|
||||||
|
{ 9200f, 250f },
|
||||||
|
{ 10000f, 200f },
|
||||||
|
{ 11000f, 0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
public Engine()
|
||||||
|
{
|
||||||
|
// Start with idle energy
|
||||||
|
FlywheelEnergy = GetEnergyFromRPM(IdleRPM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculations
|
||||||
|
|
||||||
|
public float CalculateFrictionEnergy(float deltaTime)
|
||||||
|
{
|
||||||
|
// Real friction torque data for 2.0L engine (Nm)
|
||||||
|
float frictionTorque;
|
||||||
|
|
||||||
|
if (RPM < 500) frictionTorque = 15f; // Static/breakaway
|
||||||
|
else if (RPM < 1000) frictionTorque = 14f;
|
||||||
|
else if (RPM < 2000) frictionTorque = 16f;
|
||||||
|
else if (RPM < 3000) frictionTorque = 18f;
|
||||||
|
else if (RPM < 4000) frictionTorque = 21f;
|
||||||
|
else if (RPM < 5000) frictionTorque = 25f;
|
||||||
|
else if (RPM < 6000) frictionTorque = 30f;
|
||||||
|
else if (RPM < 7000) frictionTorque = 36f;
|
||||||
|
else frictionTorque = 44f;
|
||||||
|
|
||||||
|
float frictionPower = frictionTorque * AngularVelocity;
|
||||||
|
return frictionPower * deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateCombustionEnergy(float deltaTime)
|
||||||
|
{
|
||||||
|
float torque = GetTorqueOutput() * GetActualThrottle();
|
||||||
|
return torque * AngularVelocity * deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateLoadEnergy(float deltaTime, float loadTorque)
|
||||||
|
{
|
||||||
|
return loadTorque * AngularVelocity * deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get
|
||||||
|
|
||||||
|
public float GetActualThrottle()
|
||||||
|
{
|
||||||
|
float idleThrottle = Math.Max((IdleRPM - RPM) / 10, 0);
|
||||||
|
return Math.Clamp(Throttle + idleThrottle, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetOmega()
|
||||||
|
{
|
||||||
|
if (FlywheelEnergy <= 0) return 0;
|
||||||
|
return MathF.Sqrt(2f * FlywheelEnergy / MomentOfInertia);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetRPM()
|
||||||
|
{
|
||||||
|
return GetOmega() * PhysicsUtil.RAD_PER_SEC_TO_RPM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set
|
||||||
|
|
||||||
|
public float GetEnergyFromRPM(float rpm)
|
||||||
|
{
|
||||||
|
float omega = rpm * PhysicsUtil.RPM_TO_RAD_PER_SEC;
|
||||||
|
return 0.5f * MomentOfInertia * omega * omega;
|
||||||
|
}
|
||||||
|
|
||||||
|
// torque curve
|
||||||
|
public float GetTorqueOutput()
|
||||||
|
{
|
||||||
|
if (RPM <= 0) return 0;
|
||||||
|
|
||||||
|
var points = TorqueCurve.OrderBy(p => p.Key).ToList();
|
||||||
|
|
||||||
|
if (RPM <= points.First().Key) return points.First().Value;
|
||||||
|
if (RPM >= points.Last().Key) return points.Last().Value;
|
||||||
|
|
||||||
|
for (int i = 0; i < points.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (RPM >= points[i].Key && RPM <= points[i + 1].Key)
|
||||||
|
{
|
||||||
|
float t = (RPM - points[i].Key) / (points[i + 1].Key - points[i].Key);
|
||||||
|
return PhysicsUtil.Lerp(points[i].Value, points[i + 1].Value, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyTorque(float torque, float deltaTime)
|
||||||
|
{
|
||||||
|
if (torque == 0) return;
|
||||||
|
|
||||||
|
float work = torque * AngularVelocity * deltaTime;
|
||||||
|
|
||||||
|
FlywheelEnergy += work;
|
||||||
|
FlywheelEnergy = Math.Max(FlywheelEnergy, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(float deltaTime, float loadTorque)
|
||||||
|
{
|
||||||
|
float combustionEnergy = CalculateCombustionEnergy(deltaTime);
|
||||||
|
float frictionEnergy = CalculateFrictionEnergy(deltaTime);
|
||||||
|
float loadEnergy = CalculateLoadEnergy(deltaTime, loadTorque);
|
||||||
|
|
||||||
|
float netEnergy = combustionEnergy - frictionEnergy - loadEnergy;
|
||||||
|
CurrentPower = netEnergy / deltaTime;
|
||||||
|
FlywheelEnergy += netEnergy;
|
||||||
|
FlywheelEnergy = Math.Max(FlywheelEnergy, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
Car simulation/EngineSound.cs
Normal file
149
Car simulation/EngineSound.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using SFML.Audio;
|
||||||
|
using SFML.System;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class EngineSound : SoundStream
|
||||||
|
{
|
||||||
|
// Audio properties - smaller buffer for less latency
|
||||||
|
private const uint SAMPLE_RATE = 44100;
|
||||||
|
private const ushort CHANNEL_COUNT = 2; // Stereo
|
||||||
|
private const float BUFFER_DURATION = 0.01f; // 10ms instead of 50ms!
|
||||||
|
|
||||||
|
// Engine sound properties - NO SMOOTHING for instant response
|
||||||
|
private volatile float _currentRPM = 800f; // volatile for thread safety
|
||||||
|
private volatile float _currentThrottle = 0f;
|
||||||
|
private float _volume = 0.3f;
|
||||||
|
private bool _isPlaying = false;
|
||||||
|
|
||||||
|
// Harmonic series - DIRECT RPM TO FREQUENCY
|
||||||
|
private float[] _harmonicRatios = { 1f, 2f, 4f, 6f };
|
||||||
|
private float[] _harmonicAmplitudes = { 1f, 0.3f, 0.1f, 0.05f };
|
||||||
|
private float[] _harmonicPhases = new float[4];
|
||||||
|
|
||||||
|
// Engine configuration - for direct RPM calculation
|
||||||
|
public int CylinderCount { get; set; } = 4;
|
||||||
|
public float FiringFrequencyMultiplier => CylinderCount / 2f; // 4-stroke engines
|
||||||
|
|
||||||
|
// For RPM to frequency mapping
|
||||||
|
private float _rpmToHzFactor;
|
||||||
|
|
||||||
|
private Random _random = new Random();
|
||||||
|
|
||||||
|
public EngineSound()
|
||||||
|
{
|
||||||
|
Initialize(CHANNEL_COUNT, SAMPLE_RATE);
|
||||||
|
|
||||||
|
// Calculate direct conversion factor
|
||||||
|
// RPM to Hz: (RPM / 60) × (Cylinders / 2) for 4-stroke
|
||||||
|
_rpmToHzFactor = (1f / 60f) * (CylinderCount / 2f);
|
||||||
|
|
||||||
|
// Initialize phases
|
||||||
|
for (int i = 0; i < _harmonicPhases.Length; i++)
|
||||||
|
{
|
||||||
|
_harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"EngineSound initialized: {BUFFER_DURATION * 1000:F0}ms buffer, {CylinderCount} cylinders");
|
||||||
|
}
|
||||||
|
|
||||||
|
// CALL THIS FROM YOUR PHYSICS THREAD - INSTANT UPDATE
|
||||||
|
public void SetEngineState(float rpm, float throttle)
|
||||||
|
{
|
||||||
|
// NO LOCK, NO SMOOTHING - DIRECT ASSIGNMENT
|
||||||
|
_currentRPM = rpm;
|
||||||
|
_currentThrottle = throttle;
|
||||||
|
|
||||||
|
// Volume based on throttle (instant)
|
||||||
|
_volume = 0.1f + 0.4f * throttle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartSound()
|
||||||
|
{
|
||||||
|
if (!_isPlaying)
|
||||||
|
{
|
||||||
|
Play();
|
||||||
|
_isPlaying = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopSound()
|
||||||
|
{
|
||||||
|
if (_isPlaying)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
_isPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnGetData(out short[] samples)
|
||||||
|
{
|
||||||
|
// SMALLER BUFFER: 10ms instead of 50ms
|
||||||
|
int sampleCount = (int)(SAMPLE_RATE * BUFFER_DURATION) * 2; // *2 for stereo
|
||||||
|
samples = new short[sampleCount];
|
||||||
|
|
||||||
|
// Get current values ONCE per buffer (not per sample)
|
||||||
|
float rpm = _currentRPM;
|
||||||
|
float throttle = _currentThrottle;
|
||||||
|
float volume = _volume;
|
||||||
|
|
||||||
|
// DIRECT RPM TO FREQUENCY - NO SMOOTHING
|
||||||
|
float baseFrequency = rpm * _rpmToHzFactor; // (RPM/60) × (cylinders/2)
|
||||||
|
|
||||||
|
// Pre-calculate harmonic frequencies
|
||||||
|
float[] harmonicFrequencies = new float[_harmonicRatios.Length];
|
||||||
|
float[] phaseIncrements = new float[_harmonicRatios.Length];
|
||||||
|
|
||||||
|
for (int h = 0; h < _harmonicRatios.Length; h++)
|
||||||
|
{
|
||||||
|
harmonicFrequencies[h] = baseFrequency * _harmonicRatios[h];
|
||||||
|
phaseIncrements[h] = harmonicFrequencies[h] * 2f * MathF.PI / SAMPLE_RATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate roughness factor
|
||||||
|
float roughness = 0.02f * throttle;
|
||||||
|
|
||||||
|
// Generate sound
|
||||||
|
for (int i = 0; i < sampleCount; i += 2)
|
||||||
|
{
|
||||||
|
float sampleValue = 0f;
|
||||||
|
|
||||||
|
// Sum all harmonics
|
||||||
|
for (int h = 0; h < _harmonicRatios.Length; h++)
|
||||||
|
{
|
||||||
|
sampleValue += MathF.Sin(_harmonicPhases[h]) * _harmonicAmplitudes[h];
|
||||||
|
_harmonicPhases[h] += phaseIncrements[h];
|
||||||
|
|
||||||
|
if (_harmonicPhases[h] > 2f * MathF.PI)
|
||||||
|
_harmonicPhases[h] -= 2f * MathF.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add roughness
|
||||||
|
sampleValue += (float)(_random.NextDouble() * 2 - 1) * roughness;
|
||||||
|
|
||||||
|
// Apply volume
|
||||||
|
sampleValue *= volume;
|
||||||
|
|
||||||
|
// Clamp and convert
|
||||||
|
sampleValue = Math.Clamp(sampleValue, -1f, 1f);
|
||||||
|
short sample = (short)(sampleValue * 32767);
|
||||||
|
|
||||||
|
// Stereo
|
||||||
|
samples[i] = sample;
|
||||||
|
samples[i + 1] = sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSeek(Time timeOffset)
|
||||||
|
{
|
||||||
|
// Reset phases
|
||||||
|
for (int i = 0; i < _harmonicPhases.Length; i++)
|
||||||
|
{
|
||||||
|
_harmonicPhases[i] = (float)(_random.NextDouble() * 2 * Math.PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Car simulation/IPhysicsObject.cs
Normal file
14
Car simulation/IPhysicsObject.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public interface IPhysicsObject
|
||||||
|
{
|
||||||
|
Vector2 Position { get; }
|
||||||
|
float GetResistanceForce(Vector2 carPosition, float carSpeed);
|
||||||
|
float GetTractionCoefficient(Vector2 carPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
211
Car simulation/Program.cs
Normal file
211
Car simulation/Program.cs
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
using Car_simulation;
|
||||||
|
using SFML.Window;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
Car car = new Car();
|
||||||
|
private bool _isRunning = true;
|
||||||
|
|
||||||
|
private RenderWindow _window;
|
||||||
|
|
||||||
|
// Timing for physics
|
||||||
|
private Clock _clock = new Clock();
|
||||||
|
private Time _timePerUpdate = Time.FromSeconds(1.0f / 60.0f); // 60 FPS physics
|
||||||
|
private Time _accumulatedTime = Time.Zero;
|
||||||
|
private long _updateCount = 0;
|
||||||
|
|
||||||
|
private Dictionary<Keyboard.Key, bool> _previousKeyStates = new Dictionary<Keyboard.Key, bool>();
|
||||||
|
private Dictionary<Keyboard.Key, bool> _currentKeyStates = new Dictionary<Keyboard.Key, bool>();
|
||||||
|
|
||||||
|
private static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Program program = new Program();
|
||||||
|
program.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
_window = new RenderWindow(new VideoMode(800, 600), "Car Simulation", Styles.Default);
|
||||||
|
_window.SetVisible(true);
|
||||||
|
_window.SetFramerateLimit(60);
|
||||||
|
_window.SetKeyRepeatEnabled(false);
|
||||||
|
|
||||||
|
_window.Closed += (sender, e) => _isRunning = false;
|
||||||
|
_window.KeyPressed += OnKeyPressed;
|
||||||
|
_window.KeyReleased += OnKeyReleased;
|
||||||
|
|
||||||
|
InitializeTrackedKeys();
|
||||||
|
|
||||||
|
_clock.Restart();
|
||||||
|
|
||||||
|
while (_isRunning && _window.IsOpen)
|
||||||
|
{
|
||||||
|
_window.DispatchEvents();
|
||||||
|
|
||||||
|
Time elapsed = _clock.Restart();
|
||||||
|
_accumulatedTime += elapsed;
|
||||||
|
|
||||||
|
while (_accumulatedTime >= _timePerUpdate)
|
||||||
|
{
|
||||||
|
ProcessInput(_timePerUpdate.AsSeconds());
|
||||||
|
car.Update(_timePerUpdate.AsSeconds());
|
||||||
|
_accumulatedTime -= _timePerUpdate;
|
||||||
|
_updateCount++;
|
||||||
|
|
||||||
|
if (_accumulatedTime >= Time.FromSeconds(0.2f))
|
||||||
|
{
|
||||||
|
_accumulatedTime = _timePerUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateDisplay();
|
||||||
|
UpdatePreviousKeyStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
_window.Close();
|
||||||
|
Console.WriteLine($"\nSimulation stopped after {_updateCount} updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeTrackedKeys()
|
||||||
|
{
|
||||||
|
// Initialize all keys we care about
|
||||||
|
var keysToTrack = new Keyboard.Key[]
|
||||||
|
{
|
||||||
|
Keyboard.Key.W,
|
||||||
|
Keyboard.Key.Up,
|
||||||
|
Keyboard.Key.Down,
|
||||||
|
Keyboard.Key.B,
|
||||||
|
Keyboard.Key.Space,
|
||||||
|
Keyboard.Key.Left,
|
||||||
|
Keyboard.Key.Right,
|
||||||
|
Keyboard.Key.Escape
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var key in keysToTrack)
|
||||||
|
{
|
||||||
|
_currentKeyStates[key] = false;
|
||||||
|
_previousKeyStates[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKeyPressed(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
var key = e.Code;
|
||||||
|
|
||||||
|
// Update current state
|
||||||
|
if (_currentKeyStates.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_currentKeyStates[key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKeyReleased(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
var key = e.Code;
|
||||||
|
|
||||||
|
// Update current state
|
||||||
|
if (_currentKeyStates.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_currentKeyStates[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessInput(float deltaTime)
|
||||||
|
{
|
||||||
|
// quit
|
||||||
|
if (IsKeyDown(Keyboard.Key.Escape))
|
||||||
|
{
|
||||||
|
_isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// force clutch
|
||||||
|
car.ForceClutch = (IsKeyDown(Keyboard.Key.Space));
|
||||||
|
|
||||||
|
// throttle
|
||||||
|
if (IsKeyDown(Keyboard.Key.W))
|
||||||
|
{
|
||||||
|
car.ThrottleInput = Math.Min(car.ThrottleInput + 2f * deltaTime, 1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
car.ThrottleInput = Math.Max(car.ThrottleInput - 10f * deltaTime, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// brake
|
||||||
|
if (IsKeyDown(Keyboard.Key.B))
|
||||||
|
{
|
||||||
|
car.BrakeInput = Math.Min(car.BrakeInput + 0.5f * deltaTime, 1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
car.BrakeInput = Math.Max(car.BrakeInput - 1f * deltaTime, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clutch
|
||||||
|
if (IsKeyDown(Keyboard.Key.Up))
|
||||||
|
{
|
||||||
|
car.ClutchInput = Math.Min(car.ClutchInput + 1f * deltaTime, 1.0f);
|
||||||
|
}
|
||||||
|
else if (IsKeyDown(Keyboard.Key.Down))
|
||||||
|
{
|
||||||
|
car.ClutchInput = Math.Max(car.ClutchInput - 1f * deltaTime, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clutch
|
||||||
|
if (IsKeyDown(Keyboard.Key.Up))
|
||||||
|
{
|
||||||
|
car.ClutchInput = Math.Min(car.ClutchInput + 1f * deltaTime, 1.0f);
|
||||||
|
}
|
||||||
|
else if (IsKeyDown(Keyboard.Key.Down))
|
||||||
|
{
|
||||||
|
car.ClutchInput = Math.Max(car.ClutchInput - 1f * deltaTime, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gear
|
||||||
|
if (WasKeyPressed(Keyboard.Key.Left))
|
||||||
|
{
|
||||||
|
car.Drivetrain.GearDown();
|
||||||
|
}
|
||||||
|
else if (WasKeyPressed(Keyboard.Key.Right))
|
||||||
|
{
|
||||||
|
car.Drivetrain.GearUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePreviousKeyStates()
|
||||||
|
{
|
||||||
|
var keys = new List<Keyboard.Key>(_currentKeyStates.Keys);
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
_previousKeyStates[key] = _currentKeyStates[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsKeyDown(Keyboard.Key key)
|
||||||
|
{
|
||||||
|
return _currentKeyStates.ContainsKey(key) && _currentKeyStates[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool WasKeyPressed(Keyboard.Key key)
|
||||||
|
{
|
||||||
|
return IsKeyDown(key) &&
|
||||||
|
(!_previousKeyStates.ContainsKey(key) || !_previousKeyStates[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDisplay()
|
||||||
|
{
|
||||||
|
_window.Clear(Color.Black);
|
||||||
|
|
||||||
|
// Render car or simulation visualization here
|
||||||
|
// For example, if car has Draw() method:
|
||||||
|
// car.Draw(_window);
|
||||||
|
|
||||||
|
car.DisplayUpdate(); // If this updates console display
|
||||||
|
|
||||||
|
_window.Display();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Car simulation/Util.cs
Normal file
45
Car simulation/Util.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class Util
|
||||||
|
{
|
||||||
|
public static float Lerp(float a, float b, float t)
|
||||||
|
{
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PhysicsUtil
|
||||||
|
{
|
||||||
|
public const float G = 9.81f;
|
||||||
|
public const float AirDensity = 1.225f;
|
||||||
|
|
||||||
|
public static float Lerp(float a, float b, float t)
|
||||||
|
{
|
||||||
|
t = Math.Clamp(t, 0f, 1f);
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float RPMToOmega(float rpm) => rpm * MathF.PI * 2f / 60f;
|
||||||
|
public static float OmegaToRPM(float omega) => omega * 60f / (2f * MathF.PI);
|
||||||
|
|
||||||
|
// Calculate kinetic energy: 0.5 * I * ω²
|
||||||
|
public static float CalculateRotationalEnergy(float inertia, float omega)
|
||||||
|
{
|
||||||
|
return 0.5f * inertia * omega * omega;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate omega from energy: ω = sqrt(2E / I)
|
||||||
|
public static float CalculateOmegaFromEnergy(float energy, float inertia)
|
||||||
|
{
|
||||||
|
if (energy <= 0) return 0;
|
||||||
|
return MathF.Sqrt(2f * energy / inertia);
|
||||||
|
}
|
||||||
|
|
||||||
|
public const float RAD_PER_SEC_TO_RPM = 60f / (2f * MathF.PI); // ≈ 9.549
|
||||||
|
public const float RPM_TO_RAD_PER_SEC = (2f * MathF.PI) / 60f; // ≈ 0.1047
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Car simulation/Vector2.cs
Normal file
39
Car simulation/Vector2.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
public struct Vector2
|
||||||
|
{
|
||||||
|
public float X, Y;
|
||||||
|
|
||||||
|
public Vector2(float x, float y) { X = x; Y = y; }
|
||||||
|
|
||||||
|
public float Length => MathF.Sqrt(X * X + Y * Y);
|
||||||
|
public float LengthSquared => X * X + Y * Y;
|
||||||
|
|
||||||
|
// Returns a normalized copy
|
||||||
|
public Vector2 Normalized()
|
||||||
|
{
|
||||||
|
float length = Length;
|
||||||
|
if (length > 0.0001f)
|
||||||
|
return new Vector2(X / length, Y / length);
|
||||||
|
return new Vector2(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizes in place
|
||||||
|
public void Normalize()
|
||||||
|
{
|
||||||
|
float length = Length;
|
||||||
|
if (length > 0.0001f)
|
||||||
|
{
|
||||||
|
X /= length;
|
||||||
|
Y /= length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static normalize
|
||||||
|
public static Vector2 Normalize(Vector2 v) => v.Normalized();
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
public static Vector2 operator *(Vector2 v, float s) => new Vector2(v.X * s, v.Y * s);
|
||||||
|
public static Vector2 operator *(float s, Vector2 v) => new Vector2(v.X * s, v.Y * s);
|
||||||
|
public static Vector2 operator /(Vector2 v, float s) => new Vector2(v.X / s, v.Y / s);
|
||||||
|
public static Vector2 operator +(Vector2 a, Vector2 b) => new Vector2(a.X + b.X, a.Y + b.Y);
|
||||||
|
public static Vector2 operator -(Vector2 a, Vector2 b) => new Vector2(a.X - b.X, a.Y - b.Y);
|
||||||
|
}
|
||||||
92
Car simulation/WheelSystem.cs
Normal file
92
Car simulation/WheelSystem.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
namespace Car_simulation
|
||||||
|
{
|
||||||
|
public class WheelSystem
|
||||||
|
{
|
||||||
|
// Physical properties
|
||||||
|
public float Radius { get; set; } = 0.3f; // meters
|
||||||
|
public float Inertia { get; set; } = 2.0f; // kg·m² per wheel
|
||||||
|
public int WheelCount { get; set; } = 4;
|
||||||
|
public int DrivenWheels { get; set; } = 2; // 2WD
|
||||||
|
|
||||||
|
// State
|
||||||
|
public float WheelEnergy { get; set; } = 0f; // Joules
|
||||||
|
public float AngularVelocity => GetOmega();
|
||||||
|
public float RPM => GetRPM();
|
||||||
|
public float Speed => GetSpeed();
|
||||||
|
public float ResistanceTorque { get; set; } = 0f;
|
||||||
|
|
||||||
|
// Calculations
|
||||||
|
public float GetTotalInertia()
|
||||||
|
{
|
||||||
|
return Inertia * WheelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetOmega()
|
||||||
|
{
|
||||||
|
if (WheelEnergy <= 0 || GetTotalInertia() <= 0) return 0f;
|
||||||
|
return MathF.Sqrt(2f * WheelEnergy / GetTotalInertia());
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetRPM()
|
||||||
|
{
|
||||||
|
return AngularVelocity * PhysicsUtil.RAD_PER_SEC_TO_RPM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetSpeed()
|
||||||
|
{
|
||||||
|
return AngularVelocity * Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetEnergyFromSpeed(float speed)
|
||||||
|
{
|
||||||
|
float omega = speed / Radius;
|
||||||
|
return 0.5f * GetTotalInertia() * omega * omega;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSpeed(float speed)
|
||||||
|
{
|
||||||
|
WheelEnergy = GetEnergyFromSpeed(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply work to the wheels
|
||||||
|
public void ApplyWork(float work)
|
||||||
|
{
|
||||||
|
WheelEnergy += work;
|
||||||
|
WheelEnergy = Math.Max(WheelEnergy, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyTorque(float torque, float deltaTime)
|
||||||
|
{
|
||||||
|
if (torque == 0) return;
|
||||||
|
float work = torque * AngularVelocity * deltaTime;
|
||||||
|
ApplyWork(work);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyResistance(float deltaTime)
|
||||||
|
{
|
||||||
|
if (ResistanceTorque <= 0 || AngularVelocity == 0) return;
|
||||||
|
|
||||||
|
float omega = AngularVelocity;
|
||||||
|
|
||||||
|
if (MathF.Abs(omega) < 0.1f)
|
||||||
|
{
|
||||||
|
// Check if we have enough torque to overcome static friction
|
||||||
|
// For now, just return without applying resistance to allow startup
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float resistanceSign = -MathF.Sign(omega);
|
||||||
|
float alpha = (resistanceSign * ResistanceTorque) / GetTotalInertia();
|
||||||
|
|
||||||
|
float omegaNew = omega + alpha * deltaTime;
|
||||||
|
|
||||||
|
if (MathF.Sign(omegaNew) != MathF.Sign(omega))
|
||||||
|
{
|
||||||
|
omegaNew = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float energyNew = 0.5f * GetTotalInertia() * omegaNew * omegaNew;
|
||||||
|
WheelEnergy = Math.Max(energyNew, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user