using UnityEngine; using CnControls; public enum SurfaceDetection { RayCast, SphereCast } public class VehicleController : MonoBehaviour { [SerializeField] float currentSpeed = 0f; [SerializeField] float steeringAI = 0f; public bool isAIControlled = false; [SerializeField] SurfaceDetection groundDetection = SurfaceDetection.RayCast; [SerializeField] LayerMask driveableSurface = -1; [SerializeField] float maxSpeed = 100f; [SerializeField] float baseAcceleration = 10f; [SerializeField] float steeringSensitivity = 10f; [SerializeField] float gravityForce = 9.8f; [SerializeField] float downforce = 5f; [SerializeField] float brakeThreshold = 30f; [SerializeField] float targetStoppingDistance = 5f; [SerializeField] bool allowAirControl = false; [SerializeField] AnimationCurve _frictionCurve = new AnimationCurve(); [SerializeField] AnimationCurve _turnCurve = new AnimationCurve(); [SerializeField, Range(0, 10)] float bodyTiltAmount = 0.5f; [SerializeField] Transform carBodyVisual = null; [SerializeField, Range(0.1f, 1f)] float skidMarkWidth = 0.25f; [SerializeField, Range(0.1f, 10f)] float skidMarkDuration = 5f; [SerializeField] Transform wheelFrontLeft = null; [SerializeField] Transform wheelFrontRight = null; [SerializeField] Transform wheelRearLeft = null; [SerializeField] Transform wheelRearRight = null; [SerializeField] Transform frontLeftAxle = null; [SerializeField] Transform frontRightAxle = null; [SerializeField] GameObject speedVisualEffects = null; [SerializeField] Rigidbody sphereRigidbody = null; [SerializeField] PhysicMaterial wheelFrictionMaterial = null; [SerializeField] Transform cameraFollowTarget = null; private bool isRunning = false; private bool isOnGround = false; private bool hasTurboBoost = true; public bool nosActive = false; private float accelerationInput = 0f; private float brakeInput = 0f; private float accelerationForce = 0f; private float steeringSign = 0f; private float inputHorizontal = 0f; private float inputVertical = 0f; private float wheelRadius = 0f; private float desiredTurnAngle = 0f; private float distanceToTarget = 0f; private float angleDifference = 0f; private float maxGroundDistance = 0f; private float counterSteerValue = 0f; private float steeringMultiplier = 0f; private Vector3 velocityLocal = Vector3.zero; private Vector3 forwardDirection = Vector3.zero; private Vector3 directionToTarget = Vector3.zero; private Vector3 movementDirection = Vector3.zero; private RaycastHit surfaceHit; private Rigidbody vehicleRigidbody = null; private VehicleTracker targetTracker = null; public bool AIControlled => isAIControlled; public bool Running => isRunning; public bool Grounded => isOnGround; public float SkidWidth => skidMarkWidth; public float SkidDuration => skidMarkDuration; public float CurrentSpeed => velocityLocal.z; public float MaximumSpeed => maxSpeed; public bool InputsActive => inputHorizontal != 0 || inputVertical != 0 || nosActive; public NOSController nosController; [SerializeField] private CarData carData; [Range(0.3f, 1f)] public float steeringFactor = 1f; internal float SteeringSensitivity { get => steeringSensitivity; set => steeringSensitivity = value; } public Vector3 LocalVelocity => velocityLocal; public Transform FollowTarget => cameraFollowTarget; private void Awake() { vehicleRigidbody = GetComponent(); targetTracker = GetComponent(); wheelRadius = sphereRigidbody.GetComponent().radius; maxGroundDistance = wheelRadius + 0.5f; nosController = GetComponent(); } public void InitializeWithData(CarData data) { carData = data; InitializeVehicle(); } public void InitializeVehicle() { isRunning = true; if (carData != null) { maxSpeed = carData.maxSpeed; baseAcceleration = carData.acceleration; downforce = carData.traction; } accelerationForce = baseAcceleration; } public void StopVehicle() { isRunning = false; velocityLocal = Vector3.zero; currentSpeed = 0f; steeringAI = 0f; } private void Update() { if (!isRunning) return; ProcessInputs(); ApplyControls(); UpdateVisuals(); if (isAIControlled) { accelerationForce = Mathf.Lerp(accelerationForce, baseAcceleration, Time.deltaTime); } } private void FixedUpdate() { if (!isRunning) return; CheckGround(); HandleMovement(); } //private void ProcessInputs() //{ // if (isAIControlled) // { // Vector3 targetPoint = targetTracker.TargetPos; // targetPoint.y = transform.position.y; // Vector3 directionToTarget = (targetPoint - transform.position).normalized; // float angleToTarget = Vector3.Angle(transform.forward, directionToTarget); // float distanceToTarget = Vector3.Distance(transform.position, targetPoint); // if (distanceToTarget < targetStoppingDistance) // inputVertical = 0f; // else if (angleToTarget > 60f) // inputVertical = 0.3f; // else if (angleToTarget > 30f) // inputVertical = 0.6f; // else // inputVertical = 0.95f; // inputHorizontal = 0f; // } // else // { // inputVertical = CnInputManager.GetAxis("Vertical"); // inputHorizontal = CnInputManager.GetAxis("Horizontal"); // } //} private void ProcessInputs() { if (isAIControlled) { Vector3 targetPoint = targetTracker.TargetPos; targetPoint.y = transform.position.y; Vector3 directionToTarget = (targetPoint - transform.position).normalized; float angleToTarget = Vector3.Angle(transform.forward, directionToTarget); float distanceToTarget = Vector3.Distance(transform.position, targetPoint); if (distanceToTarget < targetStoppingDistance) inputVertical = 0f; else if (angleToTarget > 60f) inputVertical = 0.3f; else if (angleToTarget > 30f) inputVertical = 0.6f; else inputVertical = 0.95f; inputHorizontal = 0f; } else { // 👇 Dampen steering sensitivity inputHorizontal = Mathf.Clamp(CnInputManager.GetAxis("Horizontal") * 0.85f, -1f, 1f); inputVertical = Mathf.Clamp(CnInputManager.GetAxis("Vertical"), -1f, 1f); } } private void UpdateVisuals() { counterSteerValue = (Mathf.Abs(velocityLocal.x) > 20f) ? -1f : 1f; float steerAngle = 45f * counterSteerValue * Mathf.Clamp(steeringAI, -1f, 1f); ApplySteeringRotation(wheelFrontLeft, steerAngle); ApplySteeringRotation(wheelFrontRight, steerAngle); float wheelSpin = CurrentSpeed * 0.75f; frontLeftAxle?.Rotate(Vector3.right * wheelSpin); frontRightAxle?.Rotate(Vector3.right * wheelSpin); wheelRearLeft?.Rotate(Vector3.right * wheelSpin); wheelRearRight?.Rotate(Vector3.right * wheelSpin); if (carBodyVisual != null) { if (velocityLocal.z > 1) { float tiltZ = Mathf.Clamp(desiredTurnAngle * steeringAI, -bodyTiltAmount, bodyTiltAmount); carBodyVisual.localRotation = Quaternion.Slerp( carBodyVisual.localRotation, Quaternion.Euler(Mathf.Lerp(0, -5f, velocityLocal.z / maxSpeed), 0f, tiltZ), 0.05f ); } else { carBodyVisual.localRotation = Quaternion.Slerp(carBodyVisual.localRotation, Quaternion.identity, 0.05f); } } if (speedVisualEffects) speedVisualEffects.SetActive(velocityLocal.z > 10); } private void ApplySteeringRotation(Transform wheel, float steerAngle) { if (wheel == null) return; Quaternion targetRot = Quaternion.Euler(0f, steerAngle, 0f); wheel.localRotation = Quaternion.Slerp(wheel.localRotation, targetRot, 0.1f); } private float previousSteeringAI = 0f; //private void ApplyControls() //{ // Vector3 targetPoint = targetTracker.TargetPos; // targetPoint.y = transform.position.y; // directionToTarget = (targetPoint - transform.position).normalized; // forwardDirection = transform.forward.normalized; // desiredTurnAngle = Mathf.Abs(Vector3.Angle(forwardDirection, Vector3.ProjectOnPlane(directionToTarget, transform.up))); // distanceToTarget = Vector3.Distance(transform.position, targetTracker.TargetPos); // movementDirection = (targetTracker.TargetPos - transform.position).normalized; // float alignment = Vector3.Dot(transform.forward, movementDirection); // angleDifference = Vector3.Angle(transform.forward, movementDirection); // brakeInput = 0f; // if (distanceToTarget > targetStoppingDistance) // { // accelerationInput = alignment > 0f ? inputVertical : (distanceToTarget > 5f ? inputVertical : -1f); // steeringAI = Mathf.Sign(Vector3.SignedAngle(transform.forward, movementDirection, Vector3.up)) // * _turnCurve.Evaluate(desiredTurnAngle / 160f); // } // else // { // steeringAI = 0f; // } // if (!isAIControlled) // steeringAI += inputHorizontal; // // ✅ Speed-based steering reduction (but not too much) // float speedFactor = Mathf.Clamp01(CurrentSpeed / maxSpeed); // steeringAI *= (1f - speedFactor * 0.3f); // only 30% reduction at top speed // // ✅ No input smoothing for racing responsiveness // steeringAI = Mathf.Clamp(steeringAI, -1f, 1f); // accelerationInput = Mathf.Clamp(accelerationInput, -1f, 1f); // currentSpeed = Mathf.Round(CurrentSpeed); //} private void ApplyControls() { Vector3 targetPoint = targetTracker.TargetPos; targetPoint.y = transform.position.y; directionToTarget = (targetPoint - transform.position).normalized; forwardDirection = transform.forward.normalized; desiredTurnAngle = Mathf.Abs(Vector3.Angle(forwardDirection, Vector3.ProjectOnPlane(directionToTarget, transform.up))); distanceToTarget = Vector3.Distance(transform.position, targetTracker.TargetPos); movementDirection = (targetTracker.TargetPos - transform.position).normalized; float alignment = Vector3.Dot(transform.forward, movementDirection); angleDifference = Vector3.Angle(transform.forward, movementDirection); brakeInput = 0f; if (distanceToTarget > targetStoppingDistance) { accelerationInput = alignment > 0f ? inputVertical : (distanceToTarget > 5f ? inputVertical : -1f); steeringAI = Mathf.Sign(Vector3.SignedAngle(transform.forward, movementDirection, Vector3.up)) * _turnCurve.Evaluate(desiredTurnAngle / 160f); } else { steeringAI = 0f; } if (!isAIControlled) steeringAI += inputHorizontal; steeringAI = Mathf.Clamp(steeringAI, -1f, 1f); accelerationInput = Mathf.Clamp(accelerationInput, -1f, 1f); currentSpeed = Mathf.Round(CurrentSpeed); } private void CheckGround() { if (groundDetection == SurfaceDetection.RayCast) { isOnGround = Physics.Raycast(sphereRigidbody.position, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface) || Physics.Raycast(sphereRigidbody.position + Vector3.forward * 3f, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface) || Physics.Raycast(sphereRigidbody.position - Vector3.forward * 3f, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface); } else if (groundDetection == SurfaceDetection.SphereCast) { isOnGround = Physics.SphereCast(sphereRigidbody.position + wheelRadius * Vector3.up, wheelRadius + 0.25f, -transform.up, out surfaceHit, maxGroundDistance, driveableSurface); } else { isOnGround = false; } } private void HandleMovement() { velocityLocal = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity); // Apply lateral friction wheelFrictionMaterial.dynamicFriction = _frictionCurve.Evaluate(Mathf.Abs(velocityLocal.x / 100f)); steeringMultiplier = _turnCurve.Evaluate(velocityLocal.magnitude / maxSpeed); if (Grounded) { steeringSign = Mathf.Sign(velocityLocal.z); // Steering torque if (inputVertical > 0.1f || velocityLocal.z > 1f) vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSign * steeringSensitivity * 100f * steeringMultiplier)); else if (inputVertical < -0.1f || velocityLocal.z < -1f) vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSign * steeringSensitivity * 100f * steeringMultiplier)); // Brake logic (if needed) // You can add: sphereRigidbody.constraints = brakeInput > 0.1f ? RigidbodyConstraints.FreezeRotationX : RigidbodyConstraints.None; // Velocity alignment using Lerp (matches reference) if (Mathf.Abs(inputVertical) > 0.1f && brakeInput < 0.1f) { Vector3 targetVelocity = vehicleRigidbody.transform.forward * (inputVertical * maxSpeed); sphereRigidbody.velocity = Vector3.Lerp( sphereRigidbody.velocity, targetVelocity, accelerationForce / 10f * Time.fixedDeltaTime ); } // Downforce for grip sphereRigidbody.AddForce(-transform.up * (downforce * sphereRigidbody.mass)); // Align car with surface vehicleRigidbody.MoveRotation( Quaternion.Slerp( vehicleRigidbody.rotation, Quaternion.FromToRotation(vehicleRigidbody.transform.up, surfaceHit.normal) * vehicleRigidbody.transform.rotation, 0.12f ) ); } else { // Optional: allow air control if (allowAirControl) { vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSensitivity * 100f * steeringMultiplier)); } // Apply velocity while airborne sphereRigidbody.velocity = Vector3.Lerp( sphereRigidbody.velocity, vehicleRigidbody.transform.forward * (inputVertical * maxSpeed) + Vector3.down * gravityForce * 9.8f, accelerationForce / 25f * Time.deltaTime ); } } //private void HandleMovement() //second //{ // velocityLocal = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity); // wheelFrictionMaterial.dynamicFriction = _frictionCurve.Evaluate(Mathf.Abs(velocityLocal.x / 100f)); // steeringMultiplier = _turnCurve.Evaluate(velocityLocal.magnitude / maxSpeed); // if (Grounded) // { // steeringSign = Mathf.Sign(velocityLocal.z); // if (Mathf.Abs(accelerationInput) > 0.1f) // vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSign * steeringSensitivity * 100f * steeringMultiplier /** steeringFactor*/)); // if (Mathf.Abs(accelerationInput) > 0.1f) // sphereRigidbody.velocity = Vector3.Lerp(sphereRigidbody.velocity, vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed), accelerationForce / 10f * Time.fixedDeltaTime); // sphereRigidbody.AddForce(-transform.up * (downforce * sphereRigidbody.mass)); // vehicleRigidbody.MoveRotation( // Quaternion.Slerp( // vehicleRigidbody.rotation, // Quaternion.FromToRotation(vehicleRigidbody.transform.up, surfaceHit.normal) * vehicleRigidbody.transform.rotation, // 0.12f // ) // ); // } // else // { // if (allowAirControl) // vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSensitivity * 100f * steeringMultiplier /** steeringFactor*/)); // sphereRigidbody.velocity = Vector3.Lerp( // sphereRigidbody.velocity, // (vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed)) + Vector3.down * (gravityForce * 9.8f), // (accelerationForce / 25f) * Time.deltaTime // ); // } //} //private void HandleMovement() //original //{ // velocityLocal = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity); // Stronger grip(less drifting) // wheelFrictionMaterial.dynamicFriction = _frictionCurve.Evaluate(Mathf.Abs(velocityLocal.x / 100f)); // steeringMultiplier = _turnCurve.Evaluate(velocityLocal.magnitude / maxSpeed); // if (Grounded) // { // steeringSign = Mathf.Sign(velocityLocal.z); // Steering torque with reduced sensitivity and damping // if (Mathf.Abs(accelerationInput) > 0.1f) // { // float torqueAmount = steeringAI * steeringSign * steeringSensitivity * 100f * steeringMultiplier; // vehicleRigidbody.AddTorque(Vector3.up * torqueAmount); // } // if (Mathf.Abs(accelerationInput) > 0.1f) // { // Vector3 targetVelocity = vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed); // sphereRigidbody.velocity = Vector3.Lerp(sphereRigidbody.velocity, targetVelocity, accelerationForce / 10f * Time.fixedDeltaTime); // } // Add more downforce to reduce slip // sphereRigidbody.AddForce(-transform.up * (downforce * sphereRigidbody.mass)); // Align car with surface normal // vehicleRigidbody.MoveRotation( // Quaternion.Slerp( // vehicleRigidbody.rotation, // Quaternion.FromToRotation(vehicleRigidbody.transform.up, surfaceHit.normal) * vehicleRigidbody.transform.rotation, // 0.12f // ) // ); // Dampen lateral velocity to reduce sliding // Vector3 localVel = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity); // localVel.x *= 0.85f; // Reduce side slip // vehicleRigidbody.velocity = vehicleRigidbody.transform.TransformDirection(localVel); // } // else // { // Allow some air control if enabled // if (allowAirControl) // { // vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSensitivity * 100f * steeringMultiplier)); // } // Gravity and air movement // sphereRigidbody.velocity = Vector3.Lerp( // sphereRigidbody.velocity, // (vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed)) + Vector3.down * (gravityForce * 9.8f), // (accelerationForce / 25f) * Time.deltaTime // ); // } //} internal void DisableVehicle() { enabled = false; sphereRigidbody.gameObject.SetActive(false); sphereRigidbody.velocity = Vector3.zero; sphereRigidbody.Sleep(); vehicleRigidbody.velocity = Vector3.zero; vehicleRigidbody.Sleep(); } public void ActivateNOS() { ActivateNOS(true); } public void ActivateNOS(bool active, float bonusAcceleration = 10f) { nosActive = active; accelerationForce = active ? accelerationForce + bonusAcceleration : baseAcceleration; } internal void EnableVehicle() { enabled = true; sphereRigidbody.gameObject.SetActive(true); } private void OnCollisionEnter(Collision collision) { Debug.Log("collision with: " + collision.gameObject.name, collision.gameObject); } }