This is part of an ongoing mission to explore making a simple controlled vehicle using the Unity Wheel Collider. See the introduction for a chapter list.

The Devil in the Details

Now we have a very basic control script in place I am going to add some pretties.

Some Clean Up

If you remember at the end of Part 2 I bemoaning the fact that our truck was very liable to do wheelies and roll over when cornering. The solution to both those problems is to lower the center of mass on the rigidbody on the truck. This will make the truck much more stable.

Moving the center of mass of a rigidbody requires script. It only needs to done once when the scene first starts, so we will add a start function to our script.

function Start(){
rigidbody.centerOfMass = Vector3(0, -0.05, 0);
}

This script moves the center of mass down (the negative Y direction) from its original position at the center of the truck’s rigidbody. I had to play around with the value here to get something that looks right. If the center of mass id too low it can be effectively below the plane of the ground, then when the truck turns at speed it leans into the corner, not out. That’s good if it were a tilt train, but not too realistic for our top heavy truck.

Backing Up

Our truck goes forward and stops, but does not go in reverse. To fix that I want to test if the truck is stationary and the backward (S) key is pressed. If so then we reverse the effects of the forward and back keys: the back key will accelerate the truck in the reverse direction and the forward key (W) will apply the brakes. I will also need to test for changing back into forward gear when the truck stops reversing.

speed = rigidbody.velocity.sqrMagnitude;

steer = Mathf.Clamp(Input.GetAxis("Horizontal"), -1, 1);
forward = Mathf.Clamp(Input.GetAxis("Vertical"), 0, 1);
back = -1 * Mathf.Clamp(Input.GetAxis("Vertical"), -1, 0);

if(speed == 0) {
  if(back > 0) { reverse = true; }
  if(forward > 0) { reverse = false; }
}

if(reverse) {
  motor = -1 * back;
  brake = forward;
} else {
  motor = forward;
  brake = back;
}

There are more variables here. Speed is the velocity of the truck. When it’s magnitude is zero we know the truck is stationary, if only briefly. If the truck is stationary we check to see if forward or back are being pressed and set the state of reverse variable appropriately.

I have changed the capturing of user input so it is separate from the motor and brake. This is so when in reverse we can swap the actions on the user controls.

Turning the Wheels

There are two rotations we need to deal with when it comes to turning the wheels. The first is turning the front wheels to match the steering. The second is to rotate the wheels to simulate their rolling along the ground.

To turn the wheels with the script we first need to let the script know what the wheels are. So I add some public variables for the wheel meshes so we can hook them up in the script inspector.

var wheelFL : Transform;
var wheelFR : Transform;
var wheelRL : Transform;
var wheelRR : Transform;

To simulate the steering we match the Y rotation of the front wheel meshes to the same steerAngle as the corresponding wheel colliders.

wheelFL.localEulerAngles.y = steer_max * steer;
wheelFR.localEulerAngles.y = steer_max * steer;

I used localEulerAngles here because I want to set the wheels to a specific angle. The local version of eulerAngles means the angle is relative to the truck, not relative to the world. I only set the Y component so the wheel is free to rotate around the Z axis.

Then we tie the rotation of the wheel meshes to the (virtual) rotation of the wheel colliders.

wheelFR.Rotate(0, 0, frontWheel1.rpm * -6 * Time.deltaTime);
wheelFL.Rotate(0, 0, frontWheel2.rpm * -6 * Time.deltaTime);
wheelRR.Rotate(0, 0, rearWheel1.rpm * -6 * Time.deltaTime);
wheelRL.Rotate(0, 0, rearWheel2.rpm * -6 * Time.deltaTime);

The rpm of the wheels (handily provided by the wheel collider) is divided by 60 to get revolutions per second, and multiplied by 360 to get the degrees rotated per second (360 / 60 = 6). I found I had to stick a minus sign in here to have the wheels turn in the right direction. We then multiply by deltaTime (the time passed since the last frame in seconds) to find the wheel’s rotation in since the last frame. We rotate the wheel meshes by that amount.

In this case we use rotate rather than eulerAngles because rotate is cumulative.

The Complete Script

var rearWheel1 : WheelCollider;
var rearWheel2 : WheelCollider;
var frontWheel1 : WheelCollider;
var frontWheel2 : WheelCollider;

var wheelFL : Transform;
var wheelFR : Transform;
var wheelRL : Transform;
var wheelRR : Transform;

var steer_max = 20;
var motor_max = 10;
var brake_max = 100;

private var steer = 0;
private var forward = 0;
private var back = 0;
private var motor = 0;
private var brake = 0;
private var reverse = false;
private var speed = 0;

function Start() {
rigidbody.centerOfMass = Vector3(0, -0.05, 0);
}

function FixedUpdate () {

speed = rigidbody.velocity.sqrMagnitude;
steer = Mathf.Clamp(Input.GetAxis("Horizontal"), -1, 1);
forward = Mathf.Clamp(Input.GetAxis("Vertical"), 0, 1);
back = -1 * Mathf.Clamp(Input.GetAxis("Vertical"), -1, 0);

if(speed == 0) {
if(back > 0) { reverse = true; }
if(forward > 0) { reverse = false; }
}

if(reverse) {
motor = -1 * back;
brake = forward;
} else {
motor = forward;
brake = back;
}

rearWheel1.motorTorque = motor_max * motor;
rearWheel2.motorTorque = motor_max * motor;
rearWheel1.brakeTorque = brake_max * brake;
rearWheel2.brakeTorque = brake_max * brake;

frontWheel1.steerAngle = steer_max * steer;
frontWheel2.steerAngle = steer_max * steer;
wheelFL.localEulerAngles.y = steer_max * steer;
wheelFR.localEulerAngles.y = steer_max * steer;

wheelFR.Rotate(0, 0, frontWheel1.rpm * -6 * Time.deltaTime);
wheelFL.Rotate(0, 0, frontWheel2.rpm * -6 * Time.deltaTime);
wheelRR.Rotate(0, 0, rearWheel1.rpm * -6 * Time.deltaTime);
wheelRL.Rotate(0, 0, rearWheel2.rpm * -6 * Time.deltaTime);

}

What Next

At this stage I think we have a simple but serviceable script. There are a few more things that can be added to improve it. In this case our truck has six wheels and I have only added wheel colliders to four of them. Adding the other two would simply be a case of duplicating the rear wheels throughout the script.

There are many cosmetic things that could be done like adding brake and reversing lights that react to the user’s input as well as engine noise.

Speaking of engines the truck does not accelerate like a real vehicle. Hitting forward applies a constant torque to the wheels. In a real vehicle revs, power-bands and gears mean that torque is anything but constant. However simulating that is beyond what I set out to achieve here.

Finally there are the other pieces of setting and behaviour that would make the simulation more game like. Using terrain instead of a dead flat plane would make driving more interesting. Having the camera follow behind the truck would make it much easier to drive.

This is one of my very first scripts in Unity. Feel free to take it, try it and tweak it. I would love to hear any feedback anyone has using it. If you have questions about how it works or know how I could improve it please let me know in the comments below.