using Godot; using System; using Utility; using System.Collections.Generic; public class CarController : RigidBody { LineDrawer3D line; [Export] public float raycastHeightOffset = 0; [Export] public float springRate = 20; [Export] public float dampRate = 2; [Export(PropertyHint.ExpEasing)] public float tractionEase = 2; [Export] public float maxSpeedKmh = 60; [Export(PropertyHint.ExpEasing)] public float sidewaysTractionEase = 1; [Export] public float maxTraction = 30; [Export] public float tractionForceMult = 10; [Export] public float sidewaysTractionMult = 1; [Export] public NodePath engineAudioPath; [Export] public NodePath timingPath; [Export] public NodePath countdownPath; [Export] public NodePath checkpointSoundPath; [Export] public NodePath countdownSoundPath; [Export] public NodePath finishSoundPath; [Export] public Material material; [Export] public NodePath bodyNode; AudioStreamPlayer checkpointSound; AudioStreamPlayer countdownSound; AudioStreamPlayer finishSound; AudioStreamPlayer3D engineAudio; public float torqueMult = 10; public float wheelBase = 1.05f; public float wheelTrack = 0.7f; Spatial wheelRoot; //MeshInstance[] graphicalWheels = new MeshInstance[4]; //CPUParticles[] dirts = new CPUParticles[4]; float smoothThrottle; int checkpointPassed = -1; RichTextLabel timingText; RichTextLabel countdownText; public float countdown = 4; public float sceneStartTime; const int CHECKPOINT_NUM = 13; static float bestTime; static float[] bestCheckpointTimes = new float[CHECKPOINT_NUM]; float[] checkpointTimes = new float[CHECKPOINT_NUM]; float[] prevBestCheckpointTimes = new float[CHECKPOINT_NUM]; readonly Color red = new Color(1.0f, 0.0f, 0.0f); readonly Color blue = new Color(0f, 0.0f, 1.0f); readonly Color green = new Color(0.0f, 1.0f, 0.0f); readonly Color darkGreen = new Color(0.0f, 0.9f, 0.0f); readonly Color black = new Color(0, 0, 0); float prevYInput; ConfigFile config; bool drawParticles = true; bool drawLines = false; bool debugSplits = false; struct Wheel { public Vector3 point; public Spatial graphical; public Particles dirt; public bool wasGrounded; } Wheel[] wheels = new Wheel[4]; bool stageEnded; public bool RaceStarted => stageTime > 0; float stageTime; float speedPitch; float smoothSteer; int lastCountdownTime = 0; struct ReplaySample { public Transform t; public float time; public float throttle; } List samples = new List(10000); public override void _Ready() { wheels[0].point = new Vector3(-wheelTrack, 0, wheelBase); wheels[1].point = new Vector3(wheelTrack, 0, wheelBase); wheels[2].point = new Vector3(-wheelTrack, 0, -wheelBase); wheels[3].point = new Vector3(wheelTrack, 0, -wheelBase); var lineGeometry = GetNode("wheel_debug"); //GD.Print(lineGeometry); line = lineGeometry as LineDrawer3D; wheels[0].graphical = GetNode("car/RootNode/fl"); wheels[1].graphical = GetNode("car/RootNode/fr"); wheels[2].graphical = GetNode("car/RootNode/rl"); wheels[3].graphical = GetNode("car/RootNode/rr"); wheelRoot = wheels[0].graphical.GetParent(); wheels[0].dirt = GetNode("dirt_fl"); wheels[1].dirt = GetNode("dirt_fr"); wheels[2].dirt = GetNode("dirt_rl"); wheels[3].dirt = GetNode("dirt_rr"); engineAudio = GetNode(engineAudioPath); timingText = GetNode(timingPath); countdownText = GetNode(countdownPath); sceneStartTime = OS.GetTicksMsec() / 1000.0f; checkpointSound = GetNode(checkpointSoundPath); countdownSound = GetNode(countdownSoundPath); finishSound = GetNode(finishSoundPath); GetNode(bodyNode).MaterialOverride = material; foreach (var w in wheels) w.dirt.Emitting = false; config = new ConfigFile(); const string CONFIG_PATH = "config.ini"; if (config.Load(CONFIG_PATH) == Error.Ok) { springRate = float.Parse(config.GetValue("setup", "spring_rate", 40).ToString()); dampRate = float.Parse(config.GetValue("setup", "damp_rate", 3).ToString()); float volume = float.Parse(config.GetValue("audio", "master_volume", 1).ToString()); AudioServer.SetBusVolumeDb(0, Mathf.Log(volume) * 8.685f); drawParticles = float.Parse(config.GetValue("graphics", "draw_particles", 1).ToString()) != 0; drawLines = float.Parse(config.GetValue("debug", "lines", 0).ToString()) != 0; debugSplits = float.Parse(config.GetValue("debug", "splits", 0).ToString()) != 0; } else GD.Print("Couldn't load " + CONFIG_PATH); } Vector3 GetVelocityAtPoint(Vector3 point) { return LinearVelocity + AngularVelocity.Cross(point - GlobalTransform.origin); } public static float Repeat(float t, float length) { return Mathf.Clamp(t - Mathf.Floor(t / length) * length, 0.0f, length); } float sat(float value) { return Mathf.Clamp(value, 0, 1); } float GetSectorTime(float[] splits, int i) { float lastCheckTime = i == 0 ? 0 : splits[i - 1]; return splits[i] - lastCheckTime; } bool isReplay; int replaySample = 0; public override void _Input(InputEvent e) { if (e is InputEventKey keyEvent) { if (keyEvent.Scancode == (uint)KeyList.F && keyEvent.Pressed) { isReplay = true; replaySample = 0; } } } public override void _PhysicsProcess(float dt) { if (isReplay) { Transform = samples[replaySample].t; replaySample++; if (replaySample == samples.Count) replaySample = 0; } float time = OS.GetTicksMsec() / 1000.0f; if (!stageEnded) stageTime = time - sceneStartTime - countdown; int countdownTime = (int)-stageTime + 1; //if (stageTime < 0) //stageTime = 0; timingText.Clear(); timingText.PushColor(black); string stageTimeStr = stageTime < 0 ? "0.000" : stageTime.ToString("F3"); string bestTimeStr = bestTime == 0 ? "--.---" : bestTime.ToString("F3"); timingText.AppendBbcode("Best: " + bestTimeStr + "\n"); if (!stageEnded) timingText.AppendBbcode( "Time: " + stageTimeStr + "\n"); else timingText.AppendBbcode( "Time: " + stageTimeStr + "\n"); if (checkpointPassed >= 0) { var bestTimes = stageEnded ? prevBestCheckpointTimes : bestCheckpointTimes; for (int c = 0; c <= checkpointPassed; c++) { float sectorTime = GetSectorTime(checkpointTimes, c); float bestSectorTime = GetSectorTime(bestTimes, c); if (bestTime == 0 || sectorTime < bestSectorTime) timingText.PushColor(darkGreen); else timingText.PushColor(red); timingText.AppendBbcode("#"); } float diff = GetSectorTime(checkpointTimes, checkpointPassed) - GetSectorTime(bestTimes, checkpointPassed); timingText.AppendBbcode("\nSplit: " + (diff > 0 ? "+" : "") + diff.ToString("F3")); } if (stageEnded) { timingText.PushColor(black); timingText.AppendBbcode("\nFinished! Press R to restart"); } if (stageTime < 0) { if (countdownTime != lastCountdownTime) countdownSound.Play(); lastCountdownTime = countdownTime; countdownText.Text = countdownTime.ToString(); } else countdownText.Text = ""; float xInput = Input.IsKeyPressed((int)KeyList.A) ? -1 : (Input.IsKeyPressed((int)KeyList.D) ? 1 : 0); float yInput = Input.IsKeyPressed((int)KeyList.S) ? -1 : (Input.IsKeyPressed((int)KeyList.W) ? 1 : 0); float throttleInput = yInput; if (isReplay) { yInput = samples[replaySample].throttle; } smoothSteer = Mathf.Lerp(smoothSteer, xInput, dt * 10); smoothThrottle = Mathf.Lerp(smoothThrottle, yInput, dt * 3); if (stageTime < 0) // Disable input before stage start yInput = 0; // Drag shit: //AddCentralForce(-LinearVelocity * (1 - 0.05f)); float speed = LinearVelocity.Length(); //float forceFactor = 1 - (speed / 10.0f); //float forceFactor = Mathf.InverseLerp(10, 0, speed); var state = GetWorld().DirectSpaceState; var up = Transform.basis.y; var forward = Transform.basis.z; line.ClearLines(); int wheelsOnGround = 0; float rayLength = 0.6f; //Color red = Color.ColorN(Colors.Red.ToString()); //Color blue = Color.ColorN(Colors.Blue.ToString()); //line.AddLine(Vector3.One * -10, Vector3.One * 10, red); Vector3 tractionPoint = new Vector3(); Vector3 right = Transform.basis.x; float sidewaysSpeed = right.Dot(LinearVelocity); int i = 0; foreach (var w in wheels) { Vector3 wp = ToGlobal(w.point); var origin = wp + up * raycastHeightOffset; var dest = origin - up * rayLength; var dict = state.IntersectRay(origin, dest); Vector3 wheelP = dest; bool grounded = dict.Count > 0; if (grounded) { var obj = (Godot.Object)dict["collider"]; Vector3 hit = (Vector3)dict["position"]; Vector3 normal = (Vector3)dict["normal"]; if (drawLines) line.AddLine(origin, hit, red); float distFromTarget = (dest - hit).Length(); float spring = springRate * distFromTarget; Vector3 veloAtWheel = GetVelocityAtPoint(origin); float verticalVeloAtWheel = up.Dot(veloAtWheel); float damp = -verticalVeloAtWheel * dampRate; AddForce(normal * (spring + damp), hit - Transform.origin); wheelsOnGround++; wheelP = hit; tractionPoint += hit; //w.dirt.Direction = new Vector3(sidewaysSpeed * 0.1f, 0, -1); } else if (drawLines) { line.AddLine(origin, dest, blue); } if (drawParticles && (grounded != w.wasGrounded || prevYInput != yInput)) { w.dirt.Emitting = grounded && yInput > 0; } float off = -0.1f; float rightoff = i % 2 == 0 ? -off : off; Vector3 localWheelCenter = wheelRoot.ToLocal(wheelP + up * 0.3f) + Vector3.Right * rightoff; w.graphical.Translation = localWheelCenter; //w.dirt.Translation = localWheelCenter; Vector3 wheelRot = Vector3.Zero; if (i % 2 == 0) wheelRot = new Vector3(0, Mathf.Deg2Rad(180), 0); else wheelRot = new Vector3(0, Mathf.Deg2Rad(0), 0); w.graphical.Rotation = wheelRot; if (i < 2) w.graphical.Rotate(Vector3.Up, -smoothSteer * Mathf.Deg2Rad(30)); /* w.dirt.Rotation = wheelRot; if (i % 2 == 0) { w.dirt.Rotate(Vector3.Up, Mathf.Deg2Rad(180)); } w.dirt.Rotate(Vector3.Right, Mathf.Deg2Rad(20)); */ if (drawParticles) { float dirtSteerFactor = i < 2 ? -smoothSteer : 0; w.dirt.Rotation = new Vector3(Mathf.Deg2Rad(20), Mathf.Atan(-sidewaysSpeed * 0.1f + dirtSteerFactor), 0); } wheels[i].wasGrounded = grounded; i++; } Vector3 upPoint = Translation + up * 1.1f; if (drawLines) { line.AddLine(upPoint, upPoint + LinearVelocity, new Color(1, 1, 0)); line.AddLine(upPoint, upPoint + right * sidewaysSpeed, red); } float forwardVelocity = forward.Dot(LinearVelocity); int gear = Mathf.FloorToInt(forwardVelocity / 8); float gearPitch = Repeat(forwardVelocity, 8) / 8.0f; speedPitch = Mathf.Lerp(speedPitch, speed * 0.1f * gearPitch, dt * 10); engineAudio.PitchScale = Mathf.Clamp(Mathf.Lerp(speedPitch, smoothThrottle * 3, 0.5f), 0.3f, 10); if (wheelsOnGround > 0) { float wheelFactor = wheelsOnGround / 4.0f; Vector3 midPoint = tractionPoint / wheelsOnGround; if (drawLines) { line.AddLine(midPoint, midPoint + up * 1, red); line.AddLine(midPoint, midPoint + Vector3.Right * 1, red); line.AddLine(midPoint, midPoint + Vector3.Forward * 1, red); } float steeringFactor = Mathf.Clamp(Mathf.InverseLerp(0, 5, speed), 0, 1); AddTorque(-Transform.basis.y * xInput * torqueMult * steeringFactor); float maxSpeed = maxSpeedKmh / 3.6f; float tractionMult = 1 - Mathf.Ease(Mathf.Abs(forwardVelocity) / maxSpeed, tractionEase); float tractionForce = tractionMult * yInput * tractionForceMult * wheelFactor; if (drawLines) line.AddLine(Vector3.Zero, Vector3.Up * tractionMult, red); float sideAbs = Mathf.Abs(sidewaysSpeed); int sidewaysSign = Mathf.Sign(sidewaysSpeed); float earlyTraction = sat(sideAbs * 2) * sat(1 - sideAbs / 20) * 10; float sidewaysTractionFac = (earlyTraction + Mathf.Ease(Mathf.Abs(sidewaysSpeed) / maxTraction, sidewaysTractionEase) * maxTraction) * sidewaysSign; Vector3 sidewaysTraction = -right * sidewaysTractionMult * sidewaysTractionFac; AddForce( forward * tractionForce + sidewaysTraction, midPoint - Transform.origin); } prevYInput = yInput; if (debugSplits) { string str = ""; for (int c = 0; c < checkpointTimes.Length; c++) { str += checkpointTimes[c].ToString() + ", " + bestCheckpointTimes[c] + "\n"; } countdownText.Text = str; } if (!isReplay) { samples.Add(new ReplaySample() { t = Transform, time = stageTime, throttle = throttleInput }); } } public void BodyEntered(Node body, int i) { if (body == this) { GD.Print("Entered: " + body.Name + " id: " + i); if (checkpointPassed + 1 == i) { Check(i); } if (checkpointPassed == 12 && i == 0) { End(); } } } void Check(int i) { checkpointPassed = i; checkpointSound.Play(); checkpointTimes[i] = stageTime; } void End() { stageEnded = true; finishSound.Play(); if (bestTime == 0 || stageTime < bestTime) { bestTime = stageTime; for (int i = 0; i < CHECKPOINT_NUM; i++) { prevBestCheckpointTimes[i] = bestCheckpointTimes[i]; bestCheckpointTimes[i] = checkpointTimes[i]; } } } }