fix: clean gitignore for Godot C#

This commit is contained in:
maxwes08
2026-04-17 12:38:11 +02:00
commit 3ac0b6b866
23 changed files with 841 additions and 0 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Godot 4+ imported files
.import/
.godot/
# Mono / C# build folders
.mono/
bin/
obj/
# Visual Studio / Rider / VS Code
.vs/
.vscode/
*.user
*.suo
*.userprefs
*.sln.docstates
# OS junk
.DS_Store
Thumbs.db
# Logs
*.log
# Build/export folders (optional depending on project)
export/
build/
# Temporary files
*.tmp

7
Physics.csproj Normal file
View File

@@ -0,0 +1,7 @@
<Project Sdk="Godot.NET.Sdk/4.6.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net9.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
</Project>

19
Physics.sln Normal file
View File

@@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Physics", "Physics.csproj", "{6DC8B879-E534-4466-8C36-228A171D7521}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6DC8B879-E534-4466-8C36-228A171D7521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DC8B879-E534-4466-8C36-228A171D7521}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DC8B879-E534-4466-8C36-228A171D7521}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{6DC8B879-E534-4466-8C36-228A171D7521}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{6DC8B879-E534-4466-8C36-228A171D7521}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{6DC8B879-E534-4466-8C36-228A171D7521}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,131 @@
using System;
public class Engine : RotatingComponent
{
public double ThrottlePosition { get; set; }
public double CurrentPower { get; private set; }
public double CombustionPower { get; private set; }
public int CylinderCount { get; private set; }
public EngineControlUnit Ecu = new EngineControlUnit();
// Friction
private const double _baseFriction = 12.0; // Seals, oil pump, valvetrain (Nm)
private const double _linearFriction = 0.025; // Hydrodynamic bearing drag (Nm/(rad/s))
private const double _quadraticFriction = 0.00002; // Windage & churning (Nm/(rad/s)²)
// Engine geometry
private double _displacementCC;
private double _compressionRatio;
// Volumetric efficiency tuning now in rad/s
public double VolumetricEfficiencyPeak { get; set; } = 1.15; // was 1.10
public double AngularVelocityForVEPeak { get; set; } = 550.0; // 5250 RPM (rad/s = 550)
public double AngularVelocityForVEMin { get; set; } = 800.0; // 7639 RPM
public double VEminAtRedline { get; set; } = 0.85; // much higher at redline
// Physics constants
private const double ATM_PRESSURE_KPA = 101.3;
private const double AIR_DENSITY_KG_PER_M3 = 1.225;
private const double FUEL_HEAT_VALUE_J_PER_KG = 43e6;
private const double STOICHIOMETRIC_AIR_FUEL_RATIO = 14.7;
public Engine(double inertia, int cylinders, double displacementCC, double compressionRatio)
{
CylinderCount = cylinders;
_displacementCC = displacementCC;
_compressionRatio = compressionRatio;
MomentOfInertia = inertia;
AngularVelocity = 100; // rad/s (~955 RPM)
}
public override void Update(double dt)
{
Ecu.Update(dt, this);
ApplyThrottleTorque();
ApplyFrictionTorque();
CurrentPower = AccumulatedTorque * AngularVelocity;
base.Update(dt);
}
private double ComputeIndicatedTorque()
{
double w = AngularVelocity; // rad/s
double throttle = Math.Clamp(ThrottlePosition, 0.0, 1.0);
// Volumetric Efficiency (WOT) as function of w (rad/s)
double veWOT;
if (w <= AngularVelocityForVEPeak)
{
// Start at a realistic 0.75 at zero RPM, peaking at your target
double t = w / AngularVelocityForVEPeak;
veWOT = 0.75 + (VolumetricEfficiencyPeak - 0.75) * t * (2 - t);
}
else
{
// Linear drop from VE_peak to VEminAtRedline
double t = (w - AngularVelocityForVEPeak) / (AngularVelocityForVEMin - AngularVelocityForVEPeak);
t = Math.Clamp(t, 0.0, 1.0);
veWOT = VolumetricEfficiencyPeak - t * (VolumetricEfficiencyPeak - VEminAtRedline);
}
veWOT = Math.Clamp(veWOT, 0.25, VolumetricEfficiencyPeak);
// Intake loss
double maxEngineDemandSpeed = 700.0; // rad/s, roughly 6700 RPM
double throttleArea = Math.Pow(throttle, 1.5);
double engineDemand = (w / maxEngineDemandSpeed) * veWOT;
const double intakeResistance = 0.03; // was 0.5 now physically realistic
double mapFraction = throttleArea / (throttleArea + intakeResistance * engineDemand);
if (throttle == 0) mapFraction = 0;
double manifoldPressureKpa = ATM_PRESSURE_KPA * mapFraction;
manifoldPressureKpa = Math.Clamp(manifoldPressureKpa, 0, ATM_PRESSURE_KPA);
double veActual = veWOT * (manifoldPressureKpa / ATM_PRESSURE_KPA);
// Exhaust loss (backpressure)
double exhaustBackpressureKpa = 2.0e-5 * w * w;
double exhaustLossFactor = 1.0 - Math.Min(0.25, exhaustBackpressureKpa / ATM_PRESSURE_KPA);
// Air & fuel mass per cycle
double displacementM3 = _displacementCC * 1e-6;
double airMassPerCycleKg = veActual * AIR_DENSITY_KG_PER_M3 * displacementM3;
double fuelMassPerCycleKg = airMassPerCycleKg / STOICHIOMETRIC_AIR_FUEL_RATIO;
double energyPerCycleJ = fuelMassPerCycleKg * FUEL_HEAT_VALUE_J_PER_KG;
// Thermal efficiency
double gamma = 1.4;
double ottoEfficiency = 1.0 - 1.0 / Math.Pow(_compressionRatio, gamma - 1.0);
double thermalEfficiency = 0.65 * ottoEfficiency;
double workPerCycleJ = energyPerCycleJ * thermalEfficiency * exhaustLossFactor;
double indicatedTorque = workPerCycleJ / (4.0 * Math.PI);
indicatedTorque = Math.Min(600.0, Math.Max(0.0, indicatedTorque));
return indicatedTorque;
}
public void ApplyThrottleTorque()
{
double torque = ComputeIndicatedTorque();
CombustionPower = torque * AngularVelocity;
ApplyTorque(torque);
}
public void ApplyFrictionTorque()
{
// Friction uses angular velocity (rad/s) directly
double w = AngularVelocity;
double frictionMag = _baseFriction
+ _linearFriction * Math.Abs(w)
+ _quadraticFriction * w * w;
double frictionTorque = -Math.Sign(w) * frictionMag;
ApplyTorque(frictionTorque);
}
}

View File

@@ -0,0 +1 @@
uid://c3gg5mg8rjw50

View File

@@ -0,0 +1,48 @@
using System;
public class EngineControlUnit
{
public double Throttle;
public bool IsInNeutral = true;
private const double _revLimit = 7000;
private const double _idleRpm = 800;
private const double _idleSensitivity = 1e-2;
private const double _throttleSensitivity = 20;
private const double _neutralThrottleLimit = 0.2;
private double _idlePosition = 0;
private double _targetThrottle = 0;
private double _currentThrottle = 0;
public EngineControlUnit()
{
}
public void Update(double dt, Engine engine)
{
if (engine.RPM > _revLimit)
{
engine.ThrottlePosition = 0; return;
}
_targetThrottle = Math.Clamp(GetIdleThrottle(dt, engine.RPM) + Throttle, 0, 1);
_currentThrottle = Util.Lerp(_currentThrottle, _targetThrottle, dt * _throttleSensitivity);
if (IsInNeutral) _currentThrottle = Math.Min(_currentThrottle, _neutralThrottleLimit);
engine.ThrottlePosition = _currentThrottle;
}
public double GetIdleThrottle(double dt, double rpm)
{
if (rpm > _idleRpm) return 0;
double diff = rpm - _idleRpm;
_idlePosition = Math.Clamp(_idlePosition - (diff * _idleSensitivity * dt), 0, 1.0);
return _idlePosition;
}
}

View File

@@ -0,0 +1 @@
uid://0k3uqu06s0im

View File

@@ -0,0 +1,58 @@
using System;
public abstract class RotatingComponent
{
public double MomentOfInertia { get; set; } // kg·m²
public double AngularVelocity { get; set; } // rad/s
public double AngularPosition { get; set; } // rad (0..2π, wrapped)
public double TotalAngularPosition { get; set; } // rad (never wraps)
public double AccumulatedTorque {get; private set; }
public RotatingComponent(double momentOfInertia = 0.1, double angularVelocity = 0, double angularPosition = 0)
{
MomentOfInertia = momentOfInertia;
AngularVelocity = angularVelocity;
AngularPosition = angularPosition;
TotalAngularPosition = angularPosition;
AccumulatedTorque = 0;
}
public virtual void ApplyTorque(double torqueNm)
{
AccumulatedTorque += torqueNm;
}
public virtual void Update(double dt)
{
if (dt <= 0) return;
if (MomentOfInertia > 0)
{
double angularAcceleration = AccumulatedTorque / MomentOfInertia;
AngularVelocity += angularAcceleration * dt;
}
double deltaAngle = AngularVelocity * dt;
TotalAngularPosition += deltaAngle;
AngularPosition += deltaAngle;
// Wrap AngularPosition to 0..2π for sin/cos
AngularPosition = AngularPosition % (2 * Math.PI);
if (AngularPosition < 0) AngularPosition += 2 * Math.PI;
AccumulatedTorque = 0;
}
public double RPM => AngularVelocity * 60 / (2 * Math.PI);
public double TotalAngleDeg => TotalAngularPosition * 180 / Math.PI;
public void SetRPM(double rpm)
{
AngularVelocity = rpm * 2 * Math.PI / 60;
}
public virtual void ResetTorque()
{
AccumulatedTorque = 0;
}
}

View File

@@ -0,0 +1 @@
uid://cym3n25hyryxy

55
Scripts/Core/TorqueMap.cs Normal file
View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
public class TorqueMap
{
public Dictionary<int, double> points;
public TorqueMap()
{
points = new Dictionary<int, double>();
}
public void AddPoint(int rpm, double torque)
{
points.Add(rpm, torque);
}
public double GetTorque(int rpm)
{
if (points == null || points.Count == 0)
throw new System.InvalidOperationException("Torque map has no points.");
var sortedRpms = points.Keys.OrderBy(r => r).ToList();
int firstRpm = sortedRpms[0];
int lastRpm = sortedRpms[sortedRpms.Count - 1];
if (rpm <= firstRpm)
return points[firstRpm];
if (rpm >= lastRpm)
return points[lastRpm];
for (int i = 0; i < sortedRpms.Count - 1; i++)
{
int r1 = sortedRpms[i];
int r2 = sortedRpms[i + 1];
if (rpm >= r1 && rpm <= r2)
{
double t1 = points[r1];
double t2 = points[r2];
return t1 + (rpm - r1) * (t2 - t1) / (r2 - r1);
}
}
throw new System.InvalidOperationException("Interpolation failed.");
}
public double GetTorque(double angularVelocityRadPerSec)
{
// Convert rad/s to RPM: 1 rad/s = 60/(2π) RPM
double rpmDouble = angularVelocityRadPerSec * 60.0 / (2.0 * Math.PI);
int rpm = (int)rpmDouble; // or use rounding if preferred: (int)Math.Round(rpmDouble)
return GetTorque(rpm);
}
}

View File

@@ -0,0 +1 @@
uid://bn0khacav57pi

95
Scripts/PhysicsManager.cs Normal file
View File

@@ -0,0 +1,95 @@
using Godot;
using System;
public partial class PhysicsManager : Node2D
{
private const double TARGET_DT = 1.0 / 2000.0;
private const double VALUE_SENSITIVITY = 5;
private double _accumulator = 0.0;
private double _simulationTime = 0.0;
private double _rpmSmooth = 0, _powerSmooth = 0, _throttleSmooth = 0;
private Label _rpmLabel, _powerLabel, _throttleLabel;
private Engine _engine;
[Export] public ProceduralEngine EngineAudio;
public override void _Ready()
{
_rpmLabel = GetNode<Label>("UI/EngineRPM");
_powerLabel = GetNode<Label>("UI/EnginePower");
_throttleLabel = GetNode<Label>("UI/EngineThrottle");
_engine = new Engine(inertia: 0.2, cylinders: 1, displacementCC: 1998, compressionRatio: 10.5);
_engine.MomentOfInertia = 0.4;
// --- Audio initialization ---
if (EngineAudio != null)
{
EngineAudio.CylinderCount = _engine.CylinderCount;
EngineAudio.EngineLayout = "v";
EngineAudio.IsCrossplane = true;
EngineAudio.BaseEngineVolume = 0.35f;
EngineAudio.IntakeRoarAmount = 0.28f;
EngineAudio.IntakeHissAmount = 0.12f;
EngineAudio.ExhaustRumbleAmount = 0.32f;
EngineAudio.ExhaustCrackleAmount = 0.08f;
EngineAudio.ResetAudioState();
EngineAudio.CurrentRPM = 750f;
EngineAudio.CurrentThrottle = 0f;
EngineAudio.CurrentCombustionPower = 0f;
if (!EngineAudio.Playing)
EngineAudio.Play();
EngineAudio.PrewarmBuffer();
}
}
public override void _Process(double dt)
{
bool wPressed = Input.IsKeyPressed(Key.W);
double throttle = wPressed ? 1.0f : 0.0f;
_engine.Ecu.Throttle = throttle;
double safeDelta = Math.Min(dt, 0.025);
_accumulator += safeDelta;
while (_accumulator >= TARGET_DT)
{
SimulateStep(TARGET_DT);
_simulationTime += TARGET_DT;
_accumulator -= TARGET_DT;
}
_rpmSmooth = Util.Lerp(_rpmSmooth, _engine.RPM, VALUE_SENSITIVITY * dt);
_powerSmooth = Util.Lerp(_powerSmooth, _engine.CurrentPower/1000, VALUE_SENSITIVITY * dt);
_throttleSmooth = Util.Lerp(_throttleSmooth, _engine.ThrottlePosition * 100, VALUE_SENSITIVITY * dt);
_rpmLabel.Text = $"RPM: {_rpmSmooth:F0}";
_powerLabel.Text = $"Power: {_powerSmooth:F2} Kw";
_throttleLabel.Text = $"Throttle: {_throttleSmooth:F1} %";
}
public override void _Draw()
{
base._Draw();
}
private void SimulateStep(double dt)
{
_engine.Update(dt); // Engine uses _engine.Throttle inside its Update
if (EngineAudio != null)
{
EngineAudio.CurrentRPM = (float)_engine.RPM;
EngineAudio.CurrentThrottle = (float)_engine.ThrottlePosition;
EngineAudio.CurrentCombustionPower = (float)_engine.CombustionPower;
}
}
}

View File

@@ -0,0 +1 @@
uid://b7yhefukbbd1h

269
Scripts/ProceduralEngine.cs Normal file
View File

@@ -0,0 +1,269 @@
using Godot;
using System;
public partial class ProceduralEngine : AudioStreamPlayer
{
private AudioStreamGeneratorPlayback _playback;
private float _crankAngle = 0f;
private int[] _firingOrder = Array.Empty<int>();
private float[] _combustionEnvelope;
// Persistent phases for engine order harmonics
private float _phase05 = 0f, _phase1 = 0f, _phase2 = 0f, _phase4 = 0f;
// Intake resonance filter state
private float _intakeFilterState = 0f, _intakeFilterPrev = 0f;
private float _currentIntakeResFreq = 100f; // smoothed resonance frequency
private float _targetIntakeResFreq = 100f;
// Noise generator state (simple LCG)
private uint _noiseState = 123456789;
public float CurrentRPM { get; set; }
public float CurrentThrottle { get; set; }
public float CurrentCombustionPower { get; set; }
public float RumbleIntensity { get; set; } = 1.0f; // 0=off, 1=normal, >1=extra rumble
private int _cylinderCount = 1;
public int CylinderCount
{
get => _cylinderCount;
set
{
if (value != _cylinderCount && value >= 1 && value <= 12)
{
_cylinderCount = value;
UpdateFiringOrder();
}
}
}
public string EngineLayout { get; set; } = "inline";
public bool IsCrossplane { get; set; } = true;
// Tunable parameters
public float BaseEngineVolume { get; set; } = 0.4f;
public float IntakeRoarAmount { get; set; } = 0.25f;
public float IntakeHissAmount { get; set; } = 0.08f; // reduced, now noise
public float ExhaustRumbleAmount { get; set; } = 0.3f;
public float ExhaustCrackleAmount { get; set; } = 0.1f;
public override void _Ready()
{
Play();
_playback = (AudioStreamGeneratorPlayback)GetStreamPlayback();
PrecomputeCombustionEnvelope();
UpdateFiringOrder();
}
private void UpdateFiringOrder()
{
// (unchanged same as your code)
if (_cylinderCount == 1) _firingOrder = new int[] { 0 };
else if (_cylinderCount == 2) _firingOrder = new int[] { 0, 1 };
else if (_cylinderCount == 3) _firingOrder = new int[] { 0, 2, 1 };
else if (_cylinderCount == 4) _firingOrder = new int[] { 0, 2, 3, 1 };
else if (_cylinderCount == 5) _firingOrder = new int[] { 0, 1, 3, 4, 2 };
else if (_cylinderCount == 6) _firingOrder = new int[] { 0, 4, 2, 5, 1, 3 };
else if (_cylinderCount == 8)
{
if (IsCrossplane && EngineLayout == "v")
_firingOrder = new int[] { 0, 7, 3, 2, 5, 4, 6, 1 };
else if (!IsCrossplane && EngineLayout == "v")
_firingOrder = new int[] { 0, 7, 1, 6, 3, 4, 2, 5 };
else if (EngineLayout == "flat")
_firingOrder = new int[] { 0, 7, 1, 6, 3, 4, 2, 5 };
else
{
_firingOrder = new int[8];
for (int i = 0; i < 8; i++) _firingOrder[i] = i;
}
}
else if (_cylinderCount == 10) _firingOrder = new int[] { 0, 9, 4, 6, 1, 7, 2, 8, 3, 5 };
else if (_cylinderCount == 12) _firingOrder = new int[] { 0, 11, 3, 8, 1, 10, 5, 6, 2, 9, 4, 7 };
else
{
_firingOrder = new int[_cylinderCount];
for (int i = 0; i < _cylinderCount; i++) _firingOrder[i] = i;
}
}
private void PrecomputeCombustionEnvelope()
{
const int steps = 128;
_combustionEnvelope = new float[steps];
float peakPhase = 0.26f;
float decay = 12f;
for (int i = 0; i < steps; i++)
{
float phase = (float)i / (steps - 1) * Mathf.Pi;
if (phase < peakPhase)
_combustionEnvelope[i] = phase / peakPhase;
else
_combustionEnvelope[i] = Mathf.Exp(-decay * (phase - peakPhase));
}
}
public override void _Process(double delta) => FillBuffer();
public void ResetAudioState()
{
_crankAngle = 0f;
_phase05 = _phase1 = _phase2 = _phase4 = 0f;
_intakeFilterState = _intakeFilterPrev = 0f;
_currentIntakeResFreq = _targetIntakeResFreq = 100f;
_noiseState = 123456789;
}
public void PrewarmBuffer() => FillBuffer();
// Simple white noise generator (xorshift style)
private float GetNoise()
{
_noiseState ^= _noiseState << 13;
_noiseState ^= _noiseState >> 17;
_noiseState ^= _noiseState << 5;
return (_noiseState % 65536) / 32768.0f - 1.0f;
}
private void FillBuffer()
{
if (_playback == null || _firingOrder.Length == 0) return;
float sampleRate = ((AudioStreamGenerator)Stream).MixRate;
float dt = (float)GetProcessDeltaTime();
int framesToFill = (int)(sampleRate * dt);
int framesAvailable = _playback.GetFramesAvailable();
int pushCount = Mathf.Min(framesToFill, framesAvailable);
float cycleRad = Mathf.Tau * 2f;
float radPerSec = (CurrentRPM / 60f) * Mathf.Tau;
float rpmNorm = Mathf.Clamp(CurrentRPM / 7000f, 0f, 1f);
float loadNorm = Mathf.Clamp(CurrentCombustionPower / 150000f, 0f, 1f);
float crankFreq = CurrentRPM / 60f;
// Engine order frequencies and delta phases
float deltaPhase05 = (crankFreq * 0.5f) / sampleRate * Mathf.Tau;
float deltaPhase1 = crankFreq / sampleRate * Mathf.Tau;
float deltaPhase2 = (crankFreq * 2f) / sampleRate * Mathf.Tau;
float deltaPhase4 = (crankFreq * 4f) / sampleRate * Mathf.Tau;
// Smooth intake resonance frequency to avoid filter pops
_targetIntakeResFreq = Mathf.Clamp(crankFreq * (_cylinderCount / 2f) * 0.8f, 60f, 400f);
_currentIntakeResFreq = Mathf.Lerp(_currentIntakeResFreq, _targetIntakeResFreq, 0.01f);
float intakeOmega = 2f * Mathf.Pi * _currentIntakeResFreq / sampleRate;
float intakeQ = 2.5f;
float intakeAlpha = Mathf.Sin(intakeOmega) / (2f * intakeQ);
float intakeA0 = 1f + intakeAlpha;
float intakeA1 = -2f * Mathf.Cos(intakeOmega);
float intakeA2 = 1f - intakeAlpha;
float intakeB0 = intakeAlpha;
float intakeB1 = 0;
float intakeB2 = -intakeAlpha;
// Lowpass filter for noise (onepole, cutoff 2000 Hz)
float noiseCutoff = 2000f / sampleRate;
float noiseA = Mathf.Exp(-Mathf.Tau * noiseCutoff);
float noiseB = 1f - noiseA;
float filteredNoise = 0f;
for (int i = 0; i < pushCount; i++)
{
float sample = 0f;
// ---- 1. Combustion pulses (unchanged) ----
float clatterPulse = Mathf.Max(0, Mathf.Sin(_phase05));
float clatter = GetNoise() * Mathf.Pow(clatterPulse, 10.0f) * 0.02f;
clatter *= (1.0f - rpmNorm * 0.5f); // Quieter as oil pressure/RPM rises
sample += clatter;
for (int idx = 0; idx < CylinderCount; idx++)
{
int cyl = _firingOrder[idx];
float cylinderOffset = (cycleRad / CylinderCount) * cyl;
float jitter = GetNoise() * 0.02f;
float cylPhase = (_crankAngle + cylinderOffset + jitter) % cycleRad;
if (cylPhase < Mathf.Pi)
{
float t = cylPhase / Mathf.Pi;
int envIdx = (int)(t * (_combustionEnvelope.Length - 1));
float pulse = _combustionEnvelope[envIdx];
float powerStroke = pulse * loadNorm * (0.8f + 0.2f * rpmNorm);
sample += powerStroke;
}
// ---- 2. Intake pulses (per cylinder) ----
if (cylPhase > Mathf.Pi * 3f)
{
float t = (cylPhase - Mathf.Pi * 3f) / Mathf.Pi;
float pulse = Mathf.Sin(t * Mathf.Pi);
float throttleFactor = Mathf.Pow(CurrentThrottle, 0.5f);
float airRush = pulse * throttleFactor * IntakeRoarAmount;
// === FIXED HISS: filtered noise instead of pure sine ===
// Generate white noise, lowpass filter it
float rawNoise = GetNoise();
filteredNoise = noiseA * filteredNoise + noiseB * rawNoise;
float hiss = filteredNoise * IntakeHissAmount * throttleFactor * (0.3f + 0.7f * rpmNorm);
airRush += hiss;
// ===================================================
sample += airRush;
}
// ---- 3. Exhaust pulses (unchanged) ----
if (cylPhase >= Mathf.Pi && cylPhase < Mathf.Tau)
{
float t = (cylPhase - Mathf.Pi) / Mathf.Pi;
float rumble = Mathf.Sin(t * Mathf.Pi * 4f) * (1f - t);
float exhaustAmp = ExhaustRumbleAmount * (0.3f + 0.7f * rpmNorm);
sample += rumble * exhaustAmp;
bool overrun = CurrentThrottle < 0.1f && CurrentRPM > 2000f;
if (overrun && t > 0.7f)
{
float crackle = Mathf.Sin(t * Mathf.Pi * 20f) * (1f - t) * 0.5f;
sample += crackle * ExhaustCrackleAmount * rpmNorm;
}
}
if (cylPhase < 0.1f) {
sample += GetNoise() * 0.03f * loadNorm;
}
}
sample = Mathf.Tanh(sample * (1.0f + loadNorm * 0.5f));
// ---- 4. Global intake resonance (unchanged, but with smoothed frequency) ----
float intakeResonance = Mathf.Sin(_crankAngle * (_currentIntakeResFreq / crankFreq) * 2f);
float filteredRes = intakeB0 * intakeResonance + intakeB1 * _intakeFilterPrev + intakeB2 * _intakeFilterState
- intakeA1 * _intakeFilterPrev - intakeA2 * _intakeFilterState;
filteredRes /= intakeA0;
_intakeFilterState = _intakeFilterPrev;
_intakeFilterPrev = filteredRes;
sample += filteredRes * IntakeRoarAmount * 0.5f * CurrentThrottle * rpmNorm;
// ---- 5. Engine order harmonics (unchanged) ----
float roughnessFactor = (EngineLayout == "v" && IsCrossplane) ? 1.2f : 1.0f;
float harmonic = 0f;
harmonic += Mathf.Sin(_phase05) * 0.08f * rpmNorm * (1f - loadNorm) * roughnessFactor;
harmonic += Mathf.Sin(_phase1) * 0.25f * rpmNorm;
harmonic += Mathf.Sin(_phase2) * 0.12f * rpmNorm;
harmonic += Mathf.Sin(_phase4) * 0.06f * rpmNorm;
sample += harmonic;
sample = Mathf.Clamp(sample * BaseEngineVolume, -0.95f, 0.95f);
_playback.PushFrame(Vector2.One * sample);
// Update phases
_phase05 = (_phase05 + deltaPhase05) % Mathf.Tau;
_phase1 = (_phase1 + deltaPhase1) % Mathf.Tau;
_phase2 = (_phase2 + deltaPhase2) % Mathf.Tau;
_phase4 = (_phase4 + deltaPhase4) % Mathf.Tau;
_crankAngle = (_crankAngle + radPerSec / sampleRate) % cycleRad;
}
}
}

View File

@@ -0,0 +1 @@
uid://q8j0rw61k0sw

7
Scripts/Util.cs Normal file
View File

@@ -0,0 +1,7 @@
public static class Util
{
public static double Lerp(double start, double end, double t)
{
return start * (1 - t) + end * t;
}
}

1
Scripts/Util.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://dagmu6lght0f7

1
icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

43
icon.svg.import Normal file
View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://xxpkhh3iv1r5"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

28
project.godot Normal file
View File

@@ -0,0 +1,28 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Physics"
run/main_scene="uid://b7aah147mnd1r"
config/features=PackedStringArray("4.6", "C#", "Forward Plus")
config/icon="res://icon.svg"
[dotnet]
project/assembly_name="Physics"
[physics]
3d/physics_engine="Jolt Physics"
[rendering]
rendering_device/driver.windows="d3d12"

37
scene.tscn Normal file
View File

@@ -0,0 +1,37 @@
[gd_scene format=3 uid="uid://b7aah147mnd1r"]
[ext_resource type="Script" uid="uid://b7yhefukbbd1h" path="res://Scripts/PhysicsManager.cs" id="1_ulcgi"]
[ext_resource type="Script" uid="uid://q8j0rw61k0sw" path="res://Scripts/ProceduralEngine.cs" id="2_nxogm"]
[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_nxogm"]
[node name="Scene" type="Node2D" unique_id=1646116984 node_paths=PackedStringArray("EngineAudio")]
script = ExtResource("1_ulcgi")
EngineAudio = NodePath("AudioStreamPlayer")
[node name="UI" type="Node" parent="." unique_id=1659532716]
[node name="EngineRPM" type="Label" parent="UI" unique_id=792596633]
offset_right = 152.0
offset_bottom = 26.0
text = "RPM:"
[node name="EnginePower" type="Label" parent="UI" unique_id=283884278]
offset_left = 1.0
offset_top = 28.0
offset_right = 153.0
offset_bottom = 77.0
text = "Power:
"
[node name="EngineThrottle" type="Label" parent="UI" unique_id=298487516]
offset_left = 2.0
offset_top = 58.0
offset_right = 154.0
offset_bottom = 107.0
text = "Throttle:
"
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1105764304]
stream = SubResource("AudioStreamGenerator_nxogm")
script = ExtResource("2_nxogm")