using UnityEngine; namespace UnityAgent { /// /// Arcade car physics: motor torque, steering, braking and a simple /// four-wheel raycast suspension. Place this on a root GameObject with /// a Rigidbody and four wheel transforms. /// [RequireComponent(typeof(Rigidbody))] public class VehicleController : MonoBehaviour { [System.Serializable] public class Wheel { public Transform transform; public bool isSteering; public bool isPowered; public bool isBraking; [HideInInspector] public bool isGrounded; [HideInInspector] public float suspensionOffset; } [Header("Wheels")] public Wheel[] wheels = new Wheel[4]; [Header("Engine")] public float motorTorque = 1500f; public float brakeTorque = 3000f; public float maxSteerAngle = 30f; public float maxSpeedKph = 180f; [Header("Suspension")] public float suspensionRestLength = 0.5f; public float suspensionSpring = 35000f; public float suspensionDamper = 4500f; public float wheelRadius = 0.35f; public LayerMask wheelMask = ~0; [Header("Aerodynamics")] public float downforce = 100f; public float centerOfMassOffset = -0.5f; private Rigidbody _rb; private float _throttle; private float _steer; private float _brake; private void Awake() { _rb = GetComponent(); _rb.centerOfMass += Vector3.up * centerOfMassOffset; _rb.interpolation = RigidbodyInterpolation.Interpolate; } private void Update() { _throttle = Input.GetAxis("Vertical"); _steer = Input.GetAxis("Horizontal"); _brake = Input.GetKey(KeyCode.Space) ? 1f : 0f; } private void FixedUpdate() { UpdateWheels(); ApplySuspension(); ApplyDrive(); ApplyDownforce(); } private void UpdateWheels() { foreach (var w in wheels) { if (w.transform == null) continue; Vector3 origin = w.transform.position; bool hit = Physics.Raycast( origin, -transform.up, out RaycastHit r, suspensionRestLength + wheelRadius, wheelMask, QueryTriggerInteraction.Ignore); w.isGrounded = hit; w.suspensionOffset = hit ? r.distance - wheelRadius : suspensionRestLength; } } private void ApplySuspension() { foreach (var w in wheels) { if (!w.isGrounded) continue; float force = (suspensionRestLength - w.suspensionOffset) * suspensionSpring; Vector3 velocity = _rb.GetPointVelocity(w.transform.position); float upwardVel = Vector3.Dot(transform.up, velocity); force -= upwardVel * suspensionDamper; _rb.AddForceAtPosition(transform.up * Mathf.Max(0f, force), w.transform.position); } } private void ApplyDrive() { float kph = _rb.velocity.magnitude * 3.6f; float speedFactor = Mathf.Clamp01(1f - kph / maxSpeedKph); foreach (var w in wheels) { if (!w.isGrounded) continue; if (w.isPowered) { Vector3 force = transform.forward * (_throttle * motorTorque * speedFactor); _rb.AddForceAtPosition(force, w.transform.position, ForceMode.Force); } if (w.isBraking && _brake > 0.01f) { Vector3 brakeForce = -transform.forward * (_brake * brakeTorque); _rb.AddForceAtPosition(brakeForce, w.transform.position, ForceMode.Force); } if (w.isSteering) { // Rotate the visual wheel and apply a small yaw torque. w.transform.localRotation = Quaternion.Euler(0f, _steer * maxSteerAngle, 0f); } } // Steering yaw torque. if (Mathf.Abs(_steer) > 0.01f) { float turn = _steer * maxSteerAngle * 0.02f; _rb.AddTorque(transform.up * turn * (_rb.velocity.magnitude + 1f), ForceMode.Force); } } private void ApplyDownforce() { _rb.AddForce(-transform.up * downforce * _rb.velocity.magnitude); } } }