diff --git a/scripts/CarCamera.cs b/scripts/CarCamera.cs new file mode 100644 index 0000000..f528e14 --- /dev/null +++ b/scripts/CarCamera.cs @@ -0,0 +1,60 @@ +using Godot; +using System; + +public class CarCamera : Camera +{ + [Export] public NodePath carPath; + + CarController car; + + public override void _Ready() + { + car = GetNode(carPath); + camPos = Translation; + + var config = new ConfigFile(); + const string CONFIG_PATH = "config.ini"; + if (config.Load(CONFIG_PATH) == Error.Ok) + { + raceSmoothing = float.Parse(config.GetValue("camera", "smoothing_rate", 7).ToString()); + height = float.Parse(config.GetValue("camera", "height", 3).ToString()); + distance = float.Parse(config.GetValue("camera", "distance", 6).ToString()); + } + else GD.Print("Couldn't load " + CONFIG_PATH); + } + + Vector3 camPos; + Vector3 carPos; + + float startSmoothing = 1; + float raceSmoothing = 7; + float height = 3; + float distance = 6; + + public override void _PhysicsProcess(float dt) + { + Vector3 carForward = car.Transform.basis.z; + carPos = car.Translation; + + Vector3 rearTargetPoint = carPos - carForward * distance; + rearTargetPoint.y = carPos.y + height; + + float smoothing = car.RaceStarted ? raceSmoothing : startSmoothing; + + camPos = camPos.LinearInterpolate(rearTargetPoint, dt * smoothing); + + /* + float limit = 8; + Vector3 diff = camPos - carPos; + if ((diff).Length() > limit) + { + camPos = carPos + diff.Normalized() * limit; + }*/ + } + + public override void _Process(float dt) + { + Translation = camPos; + LookAt(carPos, Vector3.Up); + } +} diff --git a/scripts/CarController.cs b/scripts/CarController.cs new file mode 100644 index 0000000..c813b66 --- /dev/null +++ b/scripts/CarController.cs @@ -0,0 +1,540 @@ +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]; + } + } + } +} diff --git a/scripts/CrateMover2.cs b/scripts/CrateMover2.cs new file mode 100644 index 0000000..ea4eb02 --- /dev/null +++ b/scripts/CrateMover2.cs @@ -0,0 +1,18 @@ +using Godot; +using System; + +public class CrateMover2 : Spatial +{ + public override void _Ready() + { + + } + + public override void _Process(float delta) + { + if (Input.IsKeyPressed((int)KeyList.A)) + { + Transform = Transform.Translated(Vector3.Forward * delta); + } + } +} diff --git a/scripts/Game.cs b/scripts/Game.cs new file mode 100644 index 0000000..31e9e56 --- /dev/null +++ b/scripts/Game.cs @@ -0,0 +1,55 @@ +using Godot; +using System; + +public class Game : Node +{ + [Export] NodePath hoodCameraPath; + [Export] NodePath wingmanCameraPath; + [Export] NodePath tracksideCameraPath; + + Camera hoodCamera; + Camera wingmanCamera; + Camera tracksideCamera; + + bool hoodCameraIsActive; + + public override void _Ready() + { + wingmanCamera = GetNode(wingmanCameraPath); + hoodCamera = GetNode(hoodCameraPath); + tracksideCamera = GetNode(tracksideCameraPath); + } + + bool hasRestarted; + + public override void _Input(InputEvent e) + { + if (e is InputEventKey keyEvent) + { + if (keyEvent.Scancode == (uint)KeyList.R && keyEvent.Pressed) + { + GetTree().ReloadCurrentScene(); + hasRestarted = true; + } + + if (keyEvent.Scancode == (uint)KeyList.C && keyEvent.Pressed) + { + hoodCameraIsActive = !hoodCameraIsActive; + + if (hoodCameraIsActive) + hoodCamera.MakeCurrent(); + else + wingmanCamera.MakeCurrent(); + } + + if (keyEvent.Scancode == (uint)KeyList.V) + { + tracksideCamera.MakeCurrent(); + } + } + } + + public override void _Process(float delta) + { + } +} diff --git a/scripts/LineDrawer3D.cs b/scripts/LineDrawer3D.cs new file mode 100644 index 0000000..6bff8f3 --- /dev/null +++ b/scripts/LineDrawer3D.cs @@ -0,0 +1,45 @@ +using Godot; +using System.Collections.Generic; + +namespace Utility +{ + class LineDrawer3D : ImmediateGeometry + { + struct Line + { + public Vector3 p1; + public Vector3 p2; + public Color color; + } + + List lines = new List(); + + public void AddLine(Vector3 p1, Vector3 p2, Color color) + { + lines.Add(new Line() { p1 = p1, p2 = p2, color = color }); + } + + public void ClearLines() + { + lines.Clear(); + } + + public override void _Process(float delta) + { + base._Process(delta); + + Clear(); + + Begin(Mesh.PrimitiveType.Lines); + + for (int i = 0; i < lines.Count; ++i) + { + SetColor(lines[i].color); + AddVertex(ToLocal(lines[i].p1)); + AddVertex(ToLocal(lines[i].p2)); + } + + End(); + } + } +} \ No newline at end of file diff --git a/scripts/ParticlesTest.cs b/scripts/ParticlesTest.cs new file mode 100644 index 0000000..65cd3e1 --- /dev/null +++ b/scripts/ParticlesTest.cs @@ -0,0 +1,12 @@ +using Godot; +using System; + +public class ParticlesTest : Particles +{ + public override void _Process(float delta) + { + float time = OS.GetTicksMsec() / 1000.0f; + + Emitting = Mathf.Sin(time * 20) > 0; + } +}