Az

Tank Controls In Unity

I’ve been wanting to do a tank-based game for some time and thought it would be the perfect excuse to muck around in Unity again. One of the things I wanted though is the ability to control the tank using two separate throttles like an old M113 Armoured Personnel Carrier. This would give a more unique way of controlling a tank that would require some extra thinking. The concept is that the left throttle controls power to the left tracks, while the right throttle controls power to the right tracks. So you’d push both forward equally to go forwards, or one further forwards to move and turn, or pull them equally in opposite directions to turn on the spot.

To get this working in Unity I developed two separate possible solutions, both of which will be investigated here.

Setup

For this tutorial, we’ll start from a blank slate, but as you get more familiar with the interface you should be able to integrate this into your existing work.

Firstly, create a new project. You’ll be faced with a blank scene.

Go GameObject -> 3D Object -> Plane, and then adjust the scale so each dimension is 100. This will give us a nice area to drive around on.

Next up, GameObject -> 3D Object -> Cube. Set the Y position to 1, and the Z scale to 3. This gives us our “tank” object, puts it above the ground, and lengthens it on the forward/backward axis. Now on the Cube, click Add Component in the Inspector pane and search for Rigidbody and add it, then update the mass to 1500.

Now go to Edit -> Project Settings -> Input and right click the first “Vertical” and Duplicate Array Element. Now call one of them VerticalLeft and the other VerticalRight, and then make it so one uses up/down and the other uses w/s for positive and negative button (with no alt buttons).

Save the scene.

No Wheel Colliders

Click on the Cube, Add Component, and start typing “new script”, then click the option that appears. Call it TankMovement1.

Add the following script:

using UnityEngine;

public class TankMovement1 : MonoBehaviour
{
    public const int MaxThrottle = 10;
    public const float SmoothMovement = 0.5f;
    public const float SmoothTurning = 2f;

    private float leftThrottleValue = 0f;
    private float rightThrottleValue = 0f;
    private Rigidbody tankRigidbody;

    // Start is called before the first frame update
    void Start()
    {
        tankRigidbody = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        leftThrottleValue += Input.GetAxis("VerticalLeft") / 10;
        leftThrottleValue = (leftThrottleValue > MaxThrottle) ? MaxThrottle : leftThrottleValue;
        float roundedLeftThrottle = Mathf.Round(leftThrottleValue);
        if (Mathf.Abs(roundedLeftThrottle - leftThrottleValue) < 0.02f)
        {
            leftThrottleValue = roundedLeftThrottle;
        }
        else if (roundedLeftThrottle > leftThrottleValue)
        {
            leftThrottleValue += 0.01f;
        }
        else if (roundedLeftThrottle <leftThrottleValue)
        {
            leftThrottleValue -= 0.01f;
        }

        rightThrottleValue += Input.GetAxis("VerticalRight") / 10;
        rightThrottleValue = (rightThrottleValue > MaxThrottle) ? MaxThrottle : rightThrottleValue;
        float roundedRightThrottle = Mathf.Round(rightThrottleValue);
        if (Mathf.Abs(roundedRightThrottle - rightThrottleValue) < 0.02f)
        {
            rightThrottleValue = roundedRightThrottle;
        }
        else if (roundedRightThrottle > rightThrottleValue)
        {
            rightThrottleValue += 0.01f;
        }
        else if (roundedRightThrottle < rightThrottleValue)
        {
            rightThrottleValue -= 0.01f;
        }

        // Move the tank.
        Vector3 movement = transform.forward * ((leftThrottleValue + rightThrottleValue) / 2f) * SmoothMovement * Time.deltaTime;
        tankRigidbody.MovePosition(tankRigidbody.position + movement);

        // Turn the tank.
        float turn = (leftThrottleValue - rightThrottleValue) * SmoothTurning * Time.deltaTime;
        Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f);
        tankRigidbody.MoveRotation(tankRigidbody.rotation * turnRotation);
    }
}

Now you should be able to run the script and everything should work. The w/s keys will control positive/negative power on the left throttle, and the up/down keys will control positive/negative power on the right throttle. You’ll notice I’ve included a couple of constants such as MaxThrottle which limit the amount of power, as well as SmoothMovement and SmoothTurning which work together to prevent rotation/velocity from getting out of sync. You can muck around with all these values if you like though!

A lot of it is just making sure the throttle doesn’t go two high/low and returns to an average value when released (to allow for easier straight movement). The main takeaway should be under the comments for moving/turning the tank, where you’ll see that movement is approximately:

forward * ((leftThrottle + rightThrottle) / 2)

And that turning is:

leftThrottle - rightThrottle

Of course, we need some smoothing values and want to use Time.deltaTime to ensure it doesn’t run too many/few times a second.

You can save this as a new scene now, and then go back to your original SampleScene if you want to try the next option.

Wheel Colliders

Props to @__eater__ for suggesting wheel colliders and giving me tips/hints.

From the original scene we started with (with the single extruded cube GameObject), right click our Cube in the Hierarchy and select Create Empty. This will create an empty GameObject that we will rename leftFront and set the transform position to an x/y/z of -1/0/0.25. Now go Add Component in the Inspector and select Wheel Collider (it’s easier to start searching for it).

Now right click leftFront in the hierarchy and click Duplicate. Call this one rightFront. Also change the x of the transform position to positive 1.

Now Ctrl+Click to select both the leftFront and rightFront, then right click one of them and select Duplicate. Name them leftRear and rightRear and change their z position to -0.25.

Now select the Cube object and Add Component, choose a new script and call it TankMovement2. Add the following code to it:

using UnityEngine;

public class TankMovement2 : MonoBehaviour
{
    public AxleInfo leftAxleInfo;
    public AxleInfo rightAxleInfo;
    public float maxMotorTorque; // maximum torque the motor can apply to wheel
    public void FixedUpdate()
    {
        float leftMotor = maxMotorTorque * Input.GetAxis("VerticalLeft");
        float rightMotor = maxMotorTorque * Input.GetAxis("VerticalRight");

        leftAxleInfo.frontWheel.motorTorque = leftMotor;
        leftAxleInfo.backWheel.motorTorque = leftMotor;
        rightAxleInfo.frontWheel.motorTorque = rightMotor;
        rightAxleInfo.backWheel.motorTorque = rightMotor;
    }
}

[System.Serializable]
public class AxleInfo
{
    public WheelCollider frontWheel;
    public WheelCollider backWheel;
}

Now focusing on the Cube in the Hierarchy, you should see under “Tank Movement 2 (Script)”" that there is a Left Axle Info and Right Axle Info. Click the label of each one (or the arrow to the left) to expand it. Then drag each wheel from the Hierarchy to the appropriate Wheel Collider section in the script. Also set the torque to about 400.

The script should now run fine, although you’ll notice some difficulty when trying to turn the vehicle. You can play around with various values in the transforms and the torque/mass and friction to get something more approaching accurate movement, but the basis of tank movement on two throttles using wheel colliders works.

Theoretically it should be possible to wrap a wheel collider in loose plates/belt and then actually have tank tracks, but that’s an idea for another time perhaps.

Conclusion

TWO (2) TANK MOVEMENT IDEAS IN UNITY FOR THE PRICE OF ONE!