using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; namespace ArcadeVP { public class ArcadeVehicleController : MonoBehaviour { public enum GroundCheckType { RayCast, SphereCast }; public enum MovementMode { Velocity, AngularVelocity }; public MovementMode movementMode; [FormerlySerializedAs("GroundCheck")] public GroundCheckType groundCheck; public LayerMask drivableSurface; [FormerlySerializedAs("MaxSpeed")] public float maxSpeed; [FormerlySerializedAs("accelaration")] public float acceleration; public float turn; public float gravity = 7f; public float downforce = 5f; [FormerlySerializedAs("AirControl")] [Tooltip("if true : can turn vehicle in air")] public bool airControl = false; [Tooltip("if true : vehicle will drift instead of brake while holding space")] public bool kartLike = false; [Tooltip("turn more while drifting (while holding space) only if kart Like is true")] public float driftMultiplier = 1.5f; public Rigidbody rb; public Rigidbody carBody; [HideInInspector] public RaycastHit hit; public AnimationCurve frictionCurve; public AnimationCurve turnCurve; public PhysicsMaterial frictionMaterial; [Header("Visuals")] [FormerlySerializedAs("BodyMesh")] public Transform bodyMesh; [FormerlySerializedAs("FrontWheels")] public Transform[] frontWheels = new Transform[2]; [FormerlySerializedAs("RearWheels")] public Transform[] rearWheels = new Transform[2]; [HideInInspector] public Vector3 carVelocity; [Range(0, 10)] [FormerlySerializedAs("BodyTilt")] public float bodyTilt; [Header("Audio settings")] public AudioSource engineSound; [Range(0, 1)] public float minPitch; [Range(1, 3)] [FormerlySerializedAs("MaxPitch")] public float maxPitch; [FormerlySerializedAs("SkidSound")] public AudioSource skidSound; [HideInInspector] public float skidWidth; private float radius, steeringInput, accelerationInput, brakeInput; private Vector3 origin; private bool isGrounded; private void Start() { radius = rb.GetComponent().radius; if (movementMode == MovementMode.AngularVelocity) { Physics.defaultMaxAngularSpeed = 100; } } private void Update() { Visuals(); AudioManager(); } public void ProvideInputs(float _steeringInput, float _accelerationInput, float _brakeInput) { steeringInput = _steeringInput; accelerationInput = _accelerationInput; brakeInput = _brakeInput; } public void AudioManager() { engineSound.pitch = Mathf.Lerp(minPitch, maxPitch, Mathf.Abs(carVelocity.z) / maxSpeed); if (Mathf.Abs(carVelocity.x) > 10 && grounded()) { skidSound.mute = false; } else { skidSound.mute = true; } } private void FixedUpdate() { UpdateGroundedState(); carVelocity = carBody.transform.InverseTransformDirection(carBody.linearVelocity); if (Mathf.Abs(carVelocity.x) > 0) { frictionMaterial.dynamicFriction = frictionCurve.Evaluate(Mathf.Abs(carVelocity.x / 100)); } if (grounded()) { float sign = Mathf.Sign(carVelocity.z); float TurnMultiplyer = turnCurve.Evaluate(carVelocity.magnitude / maxSpeed); if (kartLike && brakeInput > 0.1f) { TurnMultiplyer *= driftMultiplier; } if (accelerationInput > 0.1f || carVelocity.z > 1) { carBody.AddTorque(Vector3.up * steeringInput * sign * turn * 100 * TurnMultiplyer); } else if (accelerationInput < -0.1f || carVelocity.z < -1) { carBody.AddTorque(Vector3.up * steeringInput * sign * turn * 100 * TurnMultiplyer); } if (!kartLike) { if (brakeInput > 0.1f) { rb.constraints = RigidbodyConstraints.FreezeRotationX; } else { rb.constraints = RigidbodyConstraints.None; } } if (movementMode == MovementMode.AngularVelocity) { if (Mathf.Abs(accelerationInput) > 0.1f && brakeInput < 0.1f && !kartLike) { rb.angularVelocity = Vector3.Lerp(rb.angularVelocity, carBody.transform.right * accelerationInput * maxSpeed / radius, acceleration * Time.deltaTime); } else if (Mathf.Abs(accelerationInput) > 0.1f && kartLike) { rb.angularVelocity = Vector3.Lerp(rb.angularVelocity, carBody.transform.right * accelerationInput * maxSpeed / radius, acceleration * Time.deltaTime); } } else if (movementMode == MovementMode.Velocity) { if (Mathf.Abs(accelerationInput) > 0.1f && brakeInput < 0.1f && !kartLike) { rb.linearVelocity = Vector3.Lerp(rb.linearVelocity, carBody.transform.forward * accelerationInput * maxSpeed, acceleration / 10 * Time.deltaTime); } else if (Mathf.Abs(accelerationInput) > 0.1f && kartLike) { rb.linearVelocity = Vector3.Lerp(rb.linearVelocity, carBody.transform.forward * accelerationInput * maxSpeed, acceleration / 10 * Time.deltaTime); } } rb.AddForce(-transform.up * downforce * rb.mass); carBody.MoveRotation(Quaternion.Slerp(carBody.rotation, Quaternion.FromToRotation(carBody.transform.up, hit.normal) * carBody.transform.rotation, 8f * Time.fixedDeltaTime)); } else { if (airControl) { float TurnMultiplyer = turnCurve.Evaluate(carVelocity.magnitude / maxSpeed); carBody.AddTorque(Vector3.up * steeringInput * turn * 100 * TurnMultiplyer); } // In the air, level out much slower so jumps look natural, not flat instantly carBody.MoveRotation(Quaternion.Slerp(carBody.rotation, Quaternion.FromToRotation(carBody.transform.up, Vector3.up) * carBody.transform.rotation, 1.5f * Time.fixedDeltaTime)); rb.linearVelocity = Vector3.Lerp(rb.linearVelocity, rb.linearVelocity + Vector3.down * gravity, Time.deltaTime * gravity); } } public void Visuals() { foreach (Transform FW in frontWheels) { FW.localRotation = Quaternion.Slerp(FW.localRotation, Quaternion.Euler(FW.localRotation.eulerAngles.x, 30 * steeringInput, FW.localRotation.eulerAngles.z), 0.7f * Time.deltaTime / Time.fixedDeltaTime); FW.GetChild(0).localRotation = rb.transform.localRotation; } rearWheels[0].localRotation = rb.transform.localRotation; rearWheels[1].localRotation = rb.transform.localRotation; if (carVelocity.z > 1) { bodyMesh.localRotation = Quaternion.Slerp(bodyMesh.localRotation, Quaternion.Euler(Mathf.Lerp(0, -5, carVelocity.z / maxSpeed), bodyMesh.localRotation.eulerAngles.y, bodyTilt * steeringInput), 0.4f * Time.deltaTime / Time.fixedDeltaTime); } else { bodyMesh.localRotation = Quaternion.Slerp(bodyMesh.localRotation, Quaternion.Euler(0, 0, 0), 0.4f * Time.deltaTime / Time.fixedDeltaTime); } if (kartLike) { if (brakeInput > 0.1f) { bodyMesh.parent.localRotation = Quaternion.Slerp(bodyMesh.parent.localRotation, Quaternion.Euler(0, 45 * steeringInput * Mathf.Sign(carVelocity.z), 0), 0.1f * Time.deltaTime / Time.fixedDeltaTime); } else { bodyMesh.parent.localRotation = Quaternion.Slerp(bodyMesh.parent.localRotation, Quaternion.Euler(0, 0, 0), 0.1f * Time.deltaTime / Time.fixedDeltaTime); } } } private void UpdateGroundedState() { // Cast from the car's local UP, not world UP origin = rb.position + radius * transform.up; Vector3 direction = -transform.up; // Give it a bit more reach so it doesn't instantly think it's flying on bumps/slopes float maxdistance = radius + 0.8f; if (groundCheck == GroundCheckType.RayCast) { // Cast locally downward so it stays stuck to walls/slopes isGrounded = Physics.Raycast(rb.position, direction, out hit, maxdistance, drivableSurface); // Fallback: If local down fails (e.g. cresting a hill), check world down just in case if (!isGrounded) { isGrounded = Physics.Raycast(rb.position, Vector3.down, out hit, maxdistance, drivableSurface); } } else if (groundCheck == GroundCheckType.SphereCast) { isGrounded = Physics.SphereCast(origin, radius * 0.8f, direction, out hit, maxdistance, drivableSurface); if (!isGrounded) { isGrounded = Physics.SphereCast(origin, radius * 0.8f, Vector3.down, out hit, maxdistance, drivableSurface); } } else { isGrounded = false; } } public bool grounded() { return isGrounded; } private void OnDrawGizmos() { if (rb == null || Application.isPlaying) return; SphereCollider sphere = rb.GetComponent(); if (sphere == null) return; float rad = sphere.radius; float width = 0.02f; Gizmos.color = Color.yellow; Gizmos.DrawWireCube(rb.transform.position + ((rad + width) * Vector3.down), new Vector3(2 * rad, 2 * width, 4 * rad)); BoxCollider box = GetComponent(); if (box != null) { Gizmos.color = Color.green; Gizmos.DrawWireCube(transform.position, box.size); } } } }