using UnityEngine; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif namespace RaceFields.Vehicles { [RequireComponent(typeof(Rigidbody))] public sealed class ArcadeBuggyController : MonoBehaviour { [Header("Wheel Colliders")] [SerializeField] private WheelCollider frontLeftCollider; [SerializeField] private WheelCollider frontRightCollider; [SerializeField] private WheelCollider rearLeftCollider; [SerializeField] private WheelCollider rearRightCollider; [Header("Wheel Visuals")] [SerializeField] private Transform frontLeftVisual; [SerializeField] private Transform frontRightVisual; [SerializeField] private Transform rearLeftVisual; [SerializeField] private Transform rearRightVisual; [Header("Driving")] [SerializeField, Range(20f, 190f)] private float maxSpeedKmh = 120f; [SerializeField, Range(10f, 120f)] private float maxReverseSpeedKmh = 45f; [SerializeField, Range(1f, 10f)] private float accelerationMultiplier = 6f; [SerializeField, Range(10f, 45f)] private float maxSteerAngle = 35f; [SerializeField, Range(0.1f, 1f)] private float steeringSpeed = 0.5f; [SerializeField, Range(100f, 600f)] private float brakeForce = 450f; [SerializeField, Range(1f, 10f)] private float decelerationMultiplier = 2f; [SerializeField, Range(1f, 10f)] private float handbrakeDriftMultiplier = 5f; [SerializeField] private float downforce = 35f; [Header("Stability")] [SerializeField] private Vector3 centerOfMass = new Vector3(0f, -0.45f, 0.15f); [Header("Effects")] [SerializeField] private bool useEffects = true; [SerializeField] private ParticleSystem rearLeftSmoke; [SerializeField] private ParticleSystem rearRightSmoke; [SerializeField] private TrailRenderer rearLeftSkid; [SerializeField] private TrailRenderer rearRightSkid; [Header("Sounds")] [SerializeField] private bool useSounds = true; [SerializeField] private AudioSource engineSound; [SerializeField] private AudioSource tireScreechSound; public float SpeedKmh { get; private set; } public bool IsDrifting { get; private set; } public bool IsTractionLocked { get; private set; } private const float TorquePerAccelerationStep = 50f; private const float DriftVelocityThreshold = 2.5f; private const float SkidVelocityThreshold = 5f; private const float EffectsMinSpeedKmh = 12f; private Rigidbody body; private float throttleInput; private float steeringInput; private bool handbrakeInput; private float steeringAxis; private float throttleAxis; private float driftingAxis; private float localVelocityX; private float localVelocityZ; private float initialEnginePitch; private WheelFrictionCurve frontLeftSidewaysFriction; private WheelFrictionCurve frontRightSidewaysFriction; private WheelFrictionCurve rearLeftSidewaysFriction; private WheelFrictionCurve rearRightSidewaysFriction; private float frontLeftExtremumSlip; private float frontRightExtremumSlip; private float rearLeftExtremumSlip; private float rearRightExtremumSlip; private Quaternion frontLeftVisualRotationOffset = Quaternion.identity; private Quaternion frontRightVisualRotationOffset = Quaternion.identity; private Quaternion rearLeftVisualRotationOffset = Quaternion.identity; private Quaternion rearRightVisualRotationOffset = Quaternion.identity; private void Awake() { body = GetComponent(); AutoFindReferences(); CacheVisualRotationOffsets(); CacheWheelFriction(); body.centerOfMass = centerOfMass; initialEnginePitch = engineSound != null ? engineSound.pitch : 1f; SetEffectsActive(false); } private void Update() { ReadInput(); UpdateTelemetry(); UpdateWheelVisuals(); UpdateSounds(); } private void FixedUpdate() { ApplySteering(); ApplyDrive(); ApplyHandbrake(); ApplyDownforce(); UpdateEffects(); } private void ReadInput() { #if ENABLE_INPUT_SYSTEM if (Keyboard.current != null) { float forward = Keyboard.current.wKey.isPressed || Keyboard.current.upArrowKey.isPressed ? 1f : 0f; float reverse = Keyboard.current.sKey.isPressed || Keyboard.current.downArrowKey.isPressed ? 1f : 0f; float left = Keyboard.current.aKey.isPressed || Keyboard.current.leftArrowKey.isPressed ? 1f : 0f; float right = Keyboard.current.dKey.isPressed || Keyboard.current.rightArrowKey.isPressed ? 1f : 0f; throttleInput = forward - reverse; steeringInput = right - left; handbrakeInput = Keyboard.current.spaceKey.isPressed; return; } #endif #if ENABLE_LEGACY_INPUT_MANAGER throttleInput = Input.GetAxisRaw("Vertical"); steeringInput = Input.GetAxisRaw("Horizontal"); handbrakeInput = Input.GetKey(KeyCode.Space); #else throttleInput = 0f; steeringInput = 0f; handbrakeInput = false; #endif } private void UpdateTelemetry() { SpeedKmh = body.linearVelocity.magnitude * 3.6f; Vector3 localVelocity = transform.InverseTransformDirection(body.linearVelocity); localVelocityX = localVelocity.x; localVelocityZ = localVelocity.z; IsDrifting = Mathf.Abs(localVelocityX) > DriftVelocityThreshold; } private void ApplySteering() { if (steeringInput < -0.01f) steeringAxis -= Time.fixedDeltaTime * 10f * steeringSpeed; else if (steeringInput > 0.01f) steeringAxis += Time.fixedDeltaTime * 10f * steeringSpeed; else steeringAxis = Mathf.MoveTowards(steeringAxis, 0f, Time.fixedDeltaTime * 10f * steeringSpeed); steeringAxis = Mathf.Clamp(steeringAxis, -1f, 1f); float steeringAngle = steeringAxis * maxSteerAngle; SetSteerAngle(frontLeftCollider, steeringAngle, steeringSpeed); SetSteerAngle(frontRightCollider, steeringAngle, steeringSpeed); } private void ApplyDrive() { if (throttleInput > 0.01f) { throttleAxis = Mathf.MoveTowards(throttleAxis, 1f, Time.fixedDeltaTime * 3f); if (localVelocityZ < -1f) { ApplyBrakeTorque(brakeForce); ApplyMotorTorque(0f); return; } ApplyBrakeTorque(0f); ApplyMotorTorque(SpeedKmh < maxSpeedKmh ? accelerationMultiplier * TorquePerAccelerationStep * throttleAxis : 0f); return; } if (throttleInput < -0.01f) { throttleAxis = Mathf.MoveTowards(throttleAxis, -1f, Time.fixedDeltaTime * 3f); if (localVelocityZ > 1f) { ApplyBrakeTorque(brakeForce); ApplyMotorTorque(0f); return; } ApplyBrakeTorque(0f); ApplyMotorTorque(SpeedKmh < maxReverseSpeedKmh ? accelerationMultiplier * TorquePerAccelerationStep * throttleAxis : 0f); return; } throttleAxis = Mathf.MoveTowards(throttleAxis, 0f, Time.fixedDeltaTime * 10f); ApplyMotorTorque(0f); if (!handbrakeInput) { ApplyBrakeTorque(0f); body.linearVelocity *= 1f / (1f + 0.025f * decelerationMultiplier); if (body.linearVelocity.magnitude < 0.25f) body.linearVelocity = Vector3.zero; } } private void ApplyHandbrake() { if (handbrakeInput) { driftingAxis = Mathf.Clamp01(driftingAxis + Time.fixedDeltaTime); float startingPoint = driftingAxis * frontLeftExtremumSlip * handbrakeDriftMultiplier; if (startingPoint < frontLeftExtremumSlip) driftingAxis = frontLeftExtremumSlip / (frontLeftExtremumSlip * handbrakeDriftMultiplier); SetDriftFriction(driftingAxis); IsTractionLocked = true; ApplyBrakeTorque(brakeForce); return; } IsTractionLocked = false; driftingAxis = Mathf.MoveTowards(driftingAxis, 0f, Time.fixedDeltaTime / 1.5f); SetDriftFriction(driftingAxis); } private void ApplyDownforce() { body.AddForce(-transform.up * downforce * body.linearVelocity.magnitude, ForceMode.Force); } private void UpdateSounds() { if (!useSounds) { StopAudio(engineSound); StopAudio(tireScreechSound); return; } if (engineSound != null) { if (engineSound.clip != null && !engineSound.isPlaying) engineSound.Play(); engineSound.pitch = initialEnginePitch + body.linearVelocity.magnitude / 25f; } bool rearWheelsGrounded = IsWheelGrounded(rearLeftCollider) || IsWheelGrounded(rearRightCollider); bool shouldScreech = rearWheelsGrounded && (IsDrifting || (IsTractionLocked && SpeedKmh > EffectsMinSpeedKmh)); if (tireScreechSound != null) { if (shouldScreech && tireScreechSound.clip != null && !tireScreechSound.isPlaying) tireScreechSound.Play(); else if (!shouldScreech && tireScreechSound.isPlaying) tireScreechSound.Stop(); } } private void UpdateEffects() { if (!useEffects) { SetEffectsActive(false); return; } bool rearLeftGrounded = IsWheelGrounded(rearLeftCollider); bool rearRightGrounded = IsWheelGrounded(rearRightCollider); bool emitSmoke = IsDrifting && SpeedKmh > EffectsMinSpeedKmh; bool emitSkids = (IsTractionLocked || Mathf.Abs(localVelocityX) > SkidVelocityThreshold) && SpeedKmh > EffectsMinSpeedKmh; SetParticlePlaying(rearLeftSmoke, emitSmoke && rearLeftGrounded); SetParticlePlaying(rearRightSmoke, emitSmoke && rearRightGrounded); SetTrailEmitting(rearLeftSkid, emitSkids && rearLeftGrounded); SetTrailEmitting(rearRightSkid, emitSkids && rearRightGrounded); } private void UpdateWheelVisuals() { SyncWheelVisual(frontLeftCollider, frontLeftVisual, frontLeftVisualRotationOffset); SyncWheelVisual(frontRightCollider, frontRightVisual, frontRightVisualRotationOffset); SyncWheelVisual(rearLeftCollider, rearLeftVisual, rearLeftVisualRotationOffset); SyncWheelVisual(rearRightCollider, rearRightVisual, rearRightVisualRotationOffset); } private void CacheWheelFriction() { frontLeftSidewaysFriction = frontLeftCollider != null ? frontLeftCollider.sidewaysFriction : default; frontRightSidewaysFriction = frontRightCollider != null ? frontRightCollider.sidewaysFriction : default; rearLeftSidewaysFriction = rearLeftCollider != null ? rearLeftCollider.sidewaysFriction : default; rearRightSidewaysFriction = rearRightCollider != null ? rearRightCollider.sidewaysFriction : default; frontLeftExtremumSlip = frontLeftSidewaysFriction.extremumSlip; frontRightExtremumSlip = frontRightSidewaysFriction.extremumSlip; rearLeftExtremumSlip = rearLeftSidewaysFriction.extremumSlip; rearRightExtremumSlip = rearRightSidewaysFriction.extremumSlip; } private void SetDriftFriction(float drift) { ApplySidewaysFriction(frontLeftCollider, ref frontLeftSidewaysFriction, frontLeftExtremumSlip, drift); ApplySidewaysFriction(frontRightCollider, ref frontRightSidewaysFriction, frontRightExtremumSlip, drift); ApplySidewaysFriction(rearLeftCollider, ref rearLeftSidewaysFriction, rearLeftExtremumSlip, drift); ApplySidewaysFriction(rearRightCollider, ref rearRightSidewaysFriction, rearRightExtremumSlip, drift); } private void ApplySidewaysFriction(WheelCollider wheel, ref WheelFrictionCurve friction, float defaultExtremumSlip, float drift) { if (wheel == null) return; float driftExtremumSlip = defaultExtremumSlip * handbrakeDriftMultiplier; friction.extremumSlip = Mathf.Lerp(defaultExtremumSlip, driftExtremumSlip, drift); wheel.sidewaysFriction = friction; } private void ApplyMotorTorque(float torque) { SetDriveTorque(frontLeftCollider, torque); SetDriveTorque(frontRightCollider, torque); SetDriveTorque(rearLeftCollider, torque); SetDriveTorque(rearRightCollider, torque); } private void ApplyBrakeTorque(float torque) { SetBrakeTorque(frontLeftCollider, torque); SetBrakeTorque(frontRightCollider, torque); SetBrakeTorque(rearLeftCollider, torque); SetBrakeTorque(rearRightCollider, torque); } private static void SetSteerAngle(WheelCollider wheel, float angle, float lerp) { if (wheel != null) wheel.steerAngle = Mathf.Lerp(wheel.steerAngle, angle, lerp); } private static void SetDriveTorque(WheelCollider wheel, float torque) { if (wheel != null) wheel.motorTorque = torque; } private static void SetBrakeTorque(WheelCollider wheel, float torque) { if (wheel != null) wheel.brakeTorque = torque; } private static void SyncWheelVisual(WheelCollider collider, Transform visual, Quaternion visualRotationOffset) { if (collider == null || visual == null) return; collider.GetWorldPose(out Vector3 position, out Quaternion rotation); visual.SetPositionAndRotation(position, rotation * visualRotationOffset); } private void SetEffectsActive(bool active) { SetParticlePlaying(rearLeftSmoke, active); SetParticlePlaying(rearRightSmoke, active); SetTrailEmitting(rearLeftSkid, active); SetTrailEmitting(rearRightSkid, active); } private static void SetParticlePlaying(ParticleSystem particles, bool playing) { if (particles == null) return; if (playing && !particles.isPlaying) particles.Play(); else if (!playing && particles.isPlaying) particles.Stop(); } private static void SetTrailEmitting(TrailRenderer trail, bool emitting) { if (trail != null) trail.emitting = emitting; } private static bool IsWheelGrounded(WheelCollider wheel) { return wheel != null && wheel.isGrounded; } private static void StopAudio(AudioSource source) { if (source != null && source.isPlaying) source.Stop(); } private void AutoFindReferences() { frontLeftCollider ??= FindChildComponent("WheelCollider_FL"); frontRightCollider ??= FindChildComponent("WheelCollider_FR"); rearLeftCollider ??= FindChildComponent("WheelCollider_RL"); rearRightCollider ??= FindChildComponent("WheelCollider_RR"); frontLeftVisual ??= FindFirstChildTransform("F_L", "WheelVisual_FL"); frontRightVisual ??= FindFirstChildTransform("F_R", "WheelVisual_FR"); rearLeftVisual ??= FindFirstChildTransform("B_L", "WheelVisual_RL"); rearRightVisual ??= FindFirstChildTransform("B_R", "WheelVisual_RR"); rearLeftSmoke ??= FindChildComponent("LeftTireSmoke_PS"); rearRightSmoke ??= FindChildComponent("RightTireSmoke_PS"); rearLeftSkid ??= FindChildComponent("LeftTireSkid"); rearRightSkid ??= FindChildComponent("RightTireSkid"); engineSound ??= FindChildComponent("EngineSound"); tireScreechSound ??= FindChildComponent("TireScreechSound"); } private void CacheVisualRotationOffsets() { frontLeftVisualRotationOffset = GetVisualRotationOffset(frontLeftCollider, frontLeftVisual); frontRightVisualRotationOffset = GetVisualRotationOffset(frontRightCollider, frontRightVisual); rearLeftVisualRotationOffset = GetVisualRotationOffset(rearLeftCollider, rearLeftVisual); rearRightVisualRotationOffset = GetVisualRotationOffset(rearRightCollider, rearRightVisual); } private static Quaternion GetVisualRotationOffset(WheelCollider collider, Transform visual) { if (collider == null || visual == null) return Quaternion.identity; return Quaternion.Inverse(collider.transform.rotation) * visual.rotation; } private T FindChildComponent(string childName) where T : Component { Transform child = FindChildTransform(childName); return child != null ? child.GetComponent() : null; } private Transform FindChildTransform(string childName) { Transform[] children = GetComponentsInChildren(true); for (int i = 0; i < children.Length; i++) { if (children[i].name == childName) return children[i]; } return null; } private Transform FindFirstChildTransform(params string[] childNames) { for (int i = 0; i < childNames.Length; i++) { Transform child = FindChildTransform(childNames[i]); if (child != null) return child; } return null; } } }