How to create a car controller in Unity

How to create a car controller in Unity

Welcome to another tutorial! Here I will show you how to create a car controller using Unity 3D game engine! You can easily create your own simple vehicles in no time.

Let’s get started!

1. The first step is to import your car object

You can either create your own, make a cube, or import a free one from the assetstore like I did (car model link here).

Drag the GameObject to your scene. But be sure that the scale of the car is set to 1.

2. Let’s add the needed components

In order for the car to simulate physics, add a Rigidbody component. For more information about the component click here.

Select the car from the Hierarchy and click on Add Component – > Physics – > Rigidbody

  • For my settings, I set the mass to 1500. Check picture 1 for more details.

  •  Next, let’s add an Audio Source component. Simply click on Add Component and search for Audio Source. Add the component to the GameObject, and disable Play On Awake and click on Loop.

3. Make the WheelController script

Create a C# script named WheelController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WheelController : MonoBehaviour
{
    
    // Use this for initialization
    void Start ()
    {
       
    }

    void Update ()
    {

    }
}

Variable declarations. You’ll need a WheelCollider component in order to create the physics simulations for the wheels. Also, we’ll need to refer to the body rigidbody component in order to calculate the mass of each wheel.

In order to have physical wheel movements we need to rotate as well the wheel objects, also we need to calculate the speed of the vehicle. This can be optimised to be calculated on only one wheel, but don’t calculate on the traction wheel, or you will have weird stats.

public WheelCollider wc;
public Rigidbody rb;
public Transform wheelModel;
private float wheelRotation = 0f;
public float wheelRPMSpeed;

Next let’s create the wheelColliders for the GameObject.

  • create an empty GameObject in the center of the car, and name it WheelObjects;
  • parent every wheel object to the created GO;

  • be sure that every wheel is centered as in picture 3.

  • check the hierarchy to be like:

  • next duplicate using CTRL+D the created GO and rename it to WheelColliders;
  • delete all components in each child of the WheelColliders GO except the Transform;
  • Add the Wheel Collider Component to each child of the WheelColliders GO like:

  • And set the following values:

  • Keep in mind. You need to adjust the Radius (needs to be fit to the tire) and the Suspension Distance to correspond with your objects, for each wheel.

Now let’s finish the WheelColliderController:

  • Add an awake function to gather the reference values
void Awake ()
    {
        wc = GetComponent<WheelCollider> ();
        rb = gameObject.transform.parent.parent.GetComponent<Rigidbody> ();// this is dependent of the hierarchy. Go to the parent root to be able to access the Rigidbody component
    }
  • Populate the Start function with:
void Start ()
    {
        JointSpring spring = wc.suspensionSpring;

        spring.spring = 40000f;
        spring.damper = 2000f;
        spring.targetPosition = 0.4f;

        wc.suspensionSpring = spring;
        wc.suspensionDistance = 0.2f;
        wc.forceAppPointDistance = 0.1f;
        wc.mass = rb.mass / 20f;
        wc.wheelDampingRate = 1f;

        WheelFrictionCurve sidewaysFriction;
        WheelFrictionCurve forwardFriction;

        sidewaysFriction = wc.sidewaysFriction;
        forwardFriction = wc.forwardFriction;

        forwardFriction.extremumSlip = 0.2f;
        forwardFriction.extremumValue = 1;
        forwardFriction.asymptoteSlip = 0.8f;
        forwardFriction.asymptoteValue = 0.75f;
        forwardFriction.stiffness = 1.5f;

        sidewaysFriction.extremumSlip = 0.25f;
        sidewaysFriction.extremumValue = 1;
        sidewaysFriction.asymptoteSlip = 0.5f;
        sidewaysFriction.asymptoteValue = 0.75f;
        sidewaysFriction.stiffness = 1.5f;

        wc.sidewaysFriction = sidewaysFriction;
        wc.forwardFriction = forwardFriction;

    }
  • The Update will be:
    void Update ()
    {
        RotateWheels ();
    }

    public void RotateWheels ()
    {

        RaycastHit hit;
        WheelHit CorrespondingGroundHit;

        Vector3 ColliderCenterPoint = wc.transform.TransformPoint (wc.center);
        wc.GetGroundHit (out CorrespondingGroundHit);

        if (Physics.Raycast (ColliderCenterPoint, -wc.transform.up, out hit, (wc.suspensionDistance + wc.radius) * transform.localScale.y) && hit.transform.gameObject.layer != (int)Mathf.Log (rb.transform.gameObject.layer, 2) && !hit.collider.isTrigger) {
            wheelModel.transform.position = hit.point + (wc.transform.up * wc.radius) * transform.localScale.y;
            float extension = (-wc.transform.InverseTransformPoint (CorrespondingGroundHit.point).y - wc.radius) / wc.suspensionDistance;
            Debug.DrawLine (CorrespondingGroundHit.point, CorrespondingGroundHit.point + wc.transform.up * (CorrespondingGroundHit.force / rb.mass), extension <= 0.0 ? Color.magenta : Color.white);
            Debug.DrawLine (CorrespondingGroundHit.point, CorrespondingGroundHit.point - wc.transform.forward * CorrespondingGroundHit.forwardSlip * 2f, Color.green);
            Debug.DrawLine (CorrespondingGroundHit.point, CorrespondingGroundHit.point - wc.transform.right * CorrespondingGroundHit.sidewaysSlip * 2f, Color.red);
        } else {
            wheelModel.transform.position = ColliderCenterPoint - (wc.transform.up * wc.suspensionDistance) * transform.localScale.y;
        }

        wheelRotation += wc.rpm * 6 * Time.deltaTime;
        wheelModel.transform.rotation = wc.transform.rotation * Quaternion.Euler (wheelRotation, wc.steerAngle, wc.transform.rotation.z);
    }
  • And we’ll need a FixedUpdate to do all the RPM math:
    void FixedUpdate ()
    {
        WheelHit hit;
        isGrounded = wc.GetGroundHit (out hit);
        wheelRPMSpeed = (((wc.rpm * wc.radius) / 2.8f) * Mathf.Lerp (1f, .75f, hit.forwardSlip)) * rb.transform.lossyScale.y;
    }
  • Don’t forget to add the WheelCollider Script to each GO that has the wheel collider component and Drag & Drop the wheel model for each wheel:

4. Let’s create a CarController Script

Create a C# script named CarController

Before we edit the CarController, please create an empty gameObject with the car as a parent and name it COM. This will act as the center of mass. I placed mine, exactly in the center as:

COM

COM

I will put the whole code here and will explain on the go:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CarController : MonoBehaviour
{
    public static CarController instance;

    #region PrivateVars

    private float wheelRad;
    private Rigidbody _rigidbody;

    private float beforeLength;
    private float currLength;
    private float velocitySprings;

    private bool changingGear = false;

    [HideInInspector] public float gasInput = 0f;
    [HideInInspector] public float brakeInput = 0f;
    [HideInInspector] public float steerInput = 0f;
    [HideInInspector] public float clutchInput = 0f;
    [HideInInspector] public float handbrakeInput = 0f;
    [HideInInspector] public float boostInput = 1f;
    [HideInInspector] public bool cutGas = false;
    [HideInInspector] public float idleInput = 0f;

    float rawEngineRPM = 0.0f;
    private float defMaxSpeed;
    private float orgSteerAngle = 0f;

    #endregion

    #region PublicVars

    public enum CarTypes
    {
        FWD,
        RWD,
        AWD
    }

    [Header("* Car Controller")]
    public CarTypes _carType = CarTypes.FWD;
    public Transform COM;
    [Header("* Wheel Objects")]
    public List<Transform> wheelList;
    [Header("* Wheel Colliders")]
    public List<WheelController> wheelControllerList;

    public float maxEngineRPM = 7000f;
    public float minEngineRPM = 1000f;
    public float engineTorque = 3000.0f;
    public float engineRPM = 0.0f;

    public float[] gearSpeed;
    public float speed;
    public float brakeForce = 2500f;
    public float brake = 2500f;     // Maximum Brake Torque.
    public float maxspeed = 220f;
    public float steerAngle = 40f;
    public float highspeedsteerAngle = 15f;
    public float highspeedsteerAngleAtspeed = 100f;

    public int direction = 1;       // Reverse Gear Currently.
    public float downForce = 25f;       // Applies Downforce Related With Vehicle Speed.
    public float fuelInput = 1f;
    public AnimationCurve[] engineTorqueCurve;

    bool OverTorque()
    {

        if (speed > maxspeed)
            return true;

        return false;

    }
    private int currentGear = 0;
    public int totalGears = 6;

    [Range(0f, .5f)] public float gearShiftingDelay = .35f;

    public AudioClip carEngineIdle;
    public AudioSource _audioSource;

    public bool autoReverse = true;     // Enables/Disables auto reversing when player press brake button. Useful for if you are making parking style game.
    public bool automaticGear = true;   // Enables/Disables automatic gear shifting of the vehicle.
    public bool semiAutomaticGear = false;      // Enables/Disables automatic gear shifting of the vehicle.
    public bool automaticClutch = true;     // Enables/Disables automatic clutch of the vehicle.
    private bool canGoReverseNow = false;

    //Driving Assistances.
    public bool ABS = true;
    public bool TCS = true;
    public bool ESP = true;
    public bool tractionHelper = true;

    public bool ABSAct = false;
    public bool TCSAct = false;
    public bool ESPAct = false;

    [Range(.05f, .5f)] public float ABSThreshold = .35f;
    [Range(.05f, .5f)] public float TCSThreshold = .25f;
    [Range(0f, 1f)] public float TCSStrength = 1f;
    [Range(.05f, .5f)] public float ESPThreshold = .25f;
    [Range(.1f, 1f)] public float ESPStrength = .5f;
    [Range(0f, 1f)] public float steerHelperStrength = .1f;
    [Range(0f, 1f)] public float tractionHelperStrength = .1f;
    [Range(.75f, 2f)] public float engineInertia = 1f;

    public float turboBoost = 0f;
    public float NoS = 100f;
    private float NoSConsumption = 25f;
    private float NoSRegenerateTime = 10f;
    public bool useRevLimiter = true;
    public bool useExhaustFlame = true;
    public bool useNOS = false;
    public bool useTurbo = false;
    public bool engineRunning = false;      // Engine Running Now?
    private bool engineStarting = false;
    #endregion

    #region Internal
    internal float _gasInput
    {
        get
        {
            if (fuelInput <= .25f) //TODO
                return 0f;

            if (!automaticGear || semiAutomaticGear)
            {
                if (!changingGear && !cutGas)
                    return Mathf.Clamp01(gasInput);
                else
                    return 0f;
            }
            else
            {
                if (!changingGear && !cutGas)
                    return (direction == 1 ? Mathf.Clamp01(gasInput) : Mathf.Clamp01(brakeInput));
                else
                    return 0f;
            }

        }
        set { gasInput = value; }
    }

    internal float _brakeInput
    {
        get
        {

            if (!automaticGear || semiAutomaticGear)
            {
                return Mathf.Clamp01(brakeInput);
            }
            else
            {
                if (!cutGas)
                    return (direction == 1 ? Mathf.Clamp01(brakeInput) : Mathf.Clamp01(gasInput));
                else
                    return 0f;
            }

        }
        set { brakeInput = value; }
    }

    internal float _boostInput
    {
        get
        {

            if (useNOS && NoS > 5 && _gasInput >= .5f)
            {
                return boostInput;
            }
            else
            {
                return 1f;
            }

        }
        set { boostInput = value; }
    }
    #endregion

    void Awake()
    {
        instance = this; // this is an instance, meaning that the script and all public variables can be accessed with just the name of the script.instance
    }
    void Start()
    {
        if (GetComponent<Rigidbody>() != null)
        {
            _rigidbody = GetComponent<Rigidbody>(); // we will need the Rigidbody component that we created on the second step
        }
        else
            Debug.Log("Must add Rigidbody to Object " + gameObject.name);
        _audioSource = GetComponent<AudioSource>(); // we need to have the engine sound as well
        _audioSource.clip = carEngineIdle;
        for (int i = 0; i < wheelControllerList.Count; i++)
        {
            wheelControllerList[i].wheelModel = wheelList[i]; //we need to make sure that the correct wheel has been set the the reference
        }
        _rigidbody.centerOfMass = transform.InverseTransformPoint(COM.transform.position);
        orgSteerAngle = steerAngle;
        TorqueCurve();
    }

    public void KillOrStartEngine()
    {

        if (engineRunning && !engineStarting)
        {
            engineRunning = false;
            fuelInput = 0f;
        }
        else if (!engineStarting)
        {
            StartCoroutine("StartEngine");
        }

    }

    void Inputs()
    {
        gasInput = Input.GetAxis("Vertical");
        brakeInput = Mathf.Clamp01(-Input.GetAxis("Vertical"));
        handbrakeInput = Input.GetKey(KeyCode.Space) ? 1f : 0f;
        steerInput = Input.GetAxis("Horizontal");
        boostInput = Input.GetKey(KeyCode.F) ? 2.5f : 1f;
    }

    void Update()
    {
        Inputs();
        GearBox();
        Clutch();
        Turbo();
        Audio();
    }

    void Audio()
    {
        if (GameManager.instance.paused == false && GameManager.instance.finish == false && GameManager.instance.gameOver == false)
        {
            _audioSource.volume = gasInput / 2f;
            _audioSource.pitch = Mathf.Lerp(_audioSource.pitch, Mathf.Lerp(.5f, 3.3f, (engineRPM) / (maxEngineRPM)), Time.deltaTime * 50f);
            if (!_audioSource.isPlaying)
                _audioSource.Play();

        }
        else
            _audioSource.volume = 0.0f;
    }

    void FixedUpdate()
    {
        speed = _rigidbody.velocity.magnitude * 3.6f;
        steerAngle = Mathf.Lerp(orgSteerAngle, highspeedsteerAngle, (speed / highspeedsteerAngleAtspeed));

        rawEngineRPM = Mathf.Clamp(Mathf.MoveTowards(rawEngineRPM,
            (maxEngineRPM * 1.1f) *
            (Mathf.Clamp01(Mathf.Lerp(0f, 1f, (1f - clutchInput) * ((((wheelControllerList[2].wheelRPMSpeed + wheelControllerList[3].wheelRPMSpeed) * direction) / 2f) / gearSpeed[currentGear])) + (((_gasInput) * clutchInput) + idleInput) * fuelInput))
            , engineInertia * 100f), 0f, maxEngineRPM * 1.1f);

        engineRPM = Mathf.Lerp(engineRPM, rawEngineRPM, Mathf.Lerp(Time.fixedDeltaTime * 5f, Time.fixedDeltaTime * 50f, rawEngineRPM / maxEngineRPM));

        //Auto Reverse Bool.
        if (autoReverse)
        {
            canGoReverseNow = true;
        }
        else
        {
            if (_brakeInput < .1f && speed < 5)
                canGoReverseNow = true;
            else if (_brakeInput > 0 && transform.InverseTransformDirection(_rigidbody.velocity).z > 1f)
                canGoReverseNow = false;
        }

        switch (_carType)
        {

            case CarTypes.FWD:
                ApplyMotorTorque(wheelControllerList[0].wc, engineTorque);
                ApplyMotorTorque(wheelControllerList[1].wc, engineTorque);
                break;
            case CarTypes.RWD:
                ApplyMotorTorque(wheelControllerList[2].wc, engineTorque);
                ApplyMotorTorque(wheelControllerList[3].wc, engineTorque);
                break;
            case CarTypes.AWD:
                ApplyMotorTorque(wheelControllerList[0].wc, engineTorque / 2f);
                ApplyMotorTorque(wheelControllerList[1].wc, engineTorque / 2f);
                ApplyMotorTorque(wheelControllerList[2].wc, engineTorque / 2f);
                ApplyMotorTorque(wheelControllerList[3].wc, engineTorque / 2f);
                break;
        }

        Braking();
        RevLimiter();
        NOS();


        ApplySteering(wheelControllerList[0].wc);
        ApplySteering(wheelControllerList[1].wc);
        if (tractionHelper)
            TractionHelper();

        TorqueCurve();
    }


    void TractionHelper()
    {

        Vector3 velocity = _rigidbody.velocity;
        velocity -= transform.up * Vector3.Dot(velocity, transform.up);
        velocity.Normalize();

    }


    public void ApplyMotorTorque(WheelCollider wc, float torque)
    {
        if (TCS)
        {

            WheelHit hit;
            wc.GetGroundHit(out hit);

            if (Mathf.Abs(wc.rpm) >= 100)
            {
                if (hit.forwardSlip > .25f)
                {
                    TCSAct = true;
                    torque -= Mathf.Clamp(torque * (hit.forwardSlip) * TCSStrength, 0f, engineTorque);
                }
                else
                {
                    TCSAct = false;
                    torque += Mathf.Clamp(torque * (hit.forwardSlip) * TCSStrength, -engineTorque, 0f);
                }
            }
            else
            {
                TCSAct = false;
            }

        }

        if (OverTorque())
            torque = 0;

        wc.motorTorque = ((torque * (1 - clutchInput) * _boostInput) * _gasInput) * (engineTorqueCurve[currentGear].Evaluate(wc.GetComponent<WheelController>().wheelRPMSpeed * direction) * direction);
    }

    void ApplyBrakeTorque(WheelCollider wc, float brake)
    {

        if (ABS && handbrakeInput <= .1f)
        {

            WheelHit hit;
            wc.GetGroundHit(out hit);

            if ((Mathf.Abs(hit.forwardSlip) * Mathf.Clamp01(brake)) >= ABSThreshold)
            {
                ABSAct = true;
                brake = 0;
            }
            else
            {
                ABSAct = false;
            }

        }

        wc.brakeTorque = brake;

    }

    void Braking()
    {

        //Handbrake
        if (handbrakeInput > .1f)
        {

            ApplyBrakeTorque(wheelControllerList[2].wc, (brake * 1.5f) * handbrakeInput);
            ApplyBrakeTorque(wheelControllerList[3].wc, (brake * 1.5f) * handbrakeInput);

        }
        else
        {

            // Braking.
            ApplyBrakeTorque(wheelControllerList[0].wc, brake * (Mathf.Clamp(_brakeInput, 0, 1)));
            ApplyBrakeTorque(wheelControllerList[1].wc, brake * (Mathf.Clamp(_brakeInput, 0, 1)));
            ApplyBrakeTorque(wheelControllerList[2].wc, brake * Mathf.Clamp(_brakeInput, 0, 1) / 2f);
            ApplyBrakeTorque(wheelControllerList[3].wc, brake * Mathf.Clamp(_brakeInput, 0, 1) / 2f);

        }

    }

    void ApplySteering(WheelCollider wc)
    {
        wc.steerAngle = Mathf.Clamp((steerAngle * steerInput), -steerAngle, steerAngle);
    }

    void GearBox()
    {
        if (engineRunning)
            idleInput = Mathf.Lerp(1f, 0f, engineRPM / minEngineRPM);
        else
            idleInput = 0f;

        //Reversing Bool.
        if (brakeInput > .9f && transform.InverseTransformDirection(_rigidbody.velocity).z < 1f && canGoReverseNow && automaticGear && !semiAutomaticGear && !changingGear && direction != -1)
            StartCoroutine("ChangingGear", -1);
        else if (brakeInput < .1f && transform.InverseTransformDirection(_rigidbody.velocity).z > -1f && direction == -1 && !changingGear && automaticGear && !semiAutomaticGear)
            StartCoroutine("ChangingGear", 0);

        if (automaticGear)
        {

            if (currentGear < totalGears - 1 && !changingGear)
            {
                if (speed >= (gearSpeed[currentGear] * .7f) && wheelControllerList[0].wc.rpm > 0)
                {
                    if (!semiAutomaticGear)
                        StartCoroutine("ChangingGear", currentGear + 1);
                    else if (semiAutomaticGear && direction != -1)
                        StartCoroutine("ChangingGear", currentGear + 1);
                }
            }

            if (currentGear > 0)
            {

                if (!changingGear)
                {

                    if (speed < (gearSpeed[currentGear - 1] * .5f) && direction != -1)
                    {
                        StartCoroutine("ChangingGear", currentGear - 1);
                    }

                }

            }

        }
    }

    void Clutch()
    {
        if (speed <= 10f && !cutGas)
        {
            clutchInput = Mathf.Lerp(clutchInput, (Mathf.Lerp(1f, (Mathf.Lerp(.2f, 0f, ((wheelControllerList[2].wheelRPMSpeed + wheelControllerList[2].wheelRPMSpeed) / 2f) / (10))), Mathf.Abs(_gasInput))), Time.deltaTime * 50f);
        }
        else if (!cutGas)
        {
            if (changingGear)
                clutchInput = Mathf.Lerp(clutchInput, 1, Time.deltaTime * 10f);
            else
                clutchInput = Mathf.Lerp(clutchInput, 0, Time.deltaTime * 10f);
        }
        if (cutGas || handbrakeInput >= .1f)
            clutchInput = 1f;
        clutchInput = Mathf.Clamp01(clutchInput);
    }

    public void TorqueCurve()
    {
        if (defMaxSpeed != maxspeed)
        {

            if (totalGears < 1)
            {
                totalGears = 1;
                return;
            }
            gearSpeed = new float[totalGears];
            engineTorqueCurve = new AnimationCurve[totalGears];
            currentGear = 0;

            for (int i = 0; i < engineTorqueCurve.Length; i++)
            {
                engineTorqueCurve[i] = new AnimationCurve(new Keyframe(0, 1));
            }
            for (int i = 0; i < totalGears; i++)
            {
                gearSpeed[i] = Mathf.Lerp(0, maxspeed, ((float)(i + 1) / (float)(totalGears)));
                if (i != 0)
                {
                    engineTorqueCurve[i].MoveKey(0, new Keyframe(0, Mathf.Lerp(.25f, 0, (float)(i + 1) / (float)totalGears)));
                    engineTorqueCurve[i].AddKey(Mathf.Lerp(0, maxspeed / 1f, ((float)(i) / (float)(totalGears))), Mathf.Lerp(1f, .25f, ((float)(i) / (float)(totalGears))));
                    engineTorqueCurve[i].AddKey(gearSpeed[i], .1f);
                    engineTorqueCurve[i].AddKey(gearSpeed[i] * 2f, -3f);
                    engineTorqueCurve[i].postWrapMode = WrapMode.Clamp;
                }
                else
                {
                    engineTorqueCurve[i].MoveKey(0, new Keyframe(0, 1));
                    engineTorqueCurve[i].AddKey(Mathf.Lerp(0, maxspeed / 1f, (float)(i + 1) / (float)totalGears), 1f);
                    engineTorqueCurve[i].AddKey(Mathf.Lerp(25, maxspeed / 1f, ((float)(i + 1) / (float)(totalGears))), 0f);
                    engineTorqueCurve[i].postWrapMode = WrapMode.Clamp;
                }
            }
        }
        defMaxSpeed = maxspeed;
    }

    internal IEnumerator ChangingGear(int gear)
    {

        changingGear = true;
        yield return new WaitForSeconds(gearShiftingDelay);

        if (gear == -1)
        {
            currentGear = 0;
            direction = -1;
        }
        else
        {
            currentGear = gear;
            direction = 1;
        }

        changingGear = false;

    }

    void RevLimiter()
    {

        if ((useRevLimiter && engineRPM >= maxEngineRPM * 1.05f))
            cutGas = true;
        else if (engineRPM < maxEngineRPM)
            cutGas = false;

    }

    void NOS()
    {

        if (!useNOS)
            return;

        if (boostInput > 1.5f && _gasInput >= .8f && NoS > 5)
        {
            NoS -= NoSConsumption * Time.deltaTime;
            NoSRegenerateTime = 0f;
        }
        else
        {
            if (NoS < 100 && NoSRegenerateTime > 3)
                NoS += (NoSConsumption / 1.5f) * Time.deltaTime;
            NoSRegenerateTime += Time.deltaTime;
        }

    }

    void Turbo()
    {

        if (!useTurbo)
            return;

        turboBoost = Mathf.Lerp(turboBoost, Mathf.Clamp(Mathf.Pow(_gasInput, 10) * 30f + Mathf.Pow(engineRPM / maxEngineRPM, 10) * 30f, 0f, 30f), Time.deltaTime * 10f);

     }
}

Don’t forget to add the CarController Script to the car GO.

5. Populate the CarController Script in the inspector

You can either use my values or make your own.

To download the car engine sound click here

Share this post

Leave a Reply