Chapter 5

Building Player Movement Controllers

As discussed in Chapter 3, what we called a player controller is focused on movement; it is the script that determines the way that a player moves and behaves in the physics world. It is entirely focused on movement—making a ship fly, a human move along the ground, or a car to drive.

The example games call for three different vehicle types:

  1. Shoot ’em up spaceship—A spaceship that moves up, down, left, and right. Its movement need not be physics based.
  2. Humanoid character—This control script is capable of general human things, such as running forward, backward, and turning around.
  3. Wheeled vehicle—The vehicle controller utilizes Unity’s wheel colliders to work toward a more realistic physics simulation. There will be no gearing, but the vehicle will be able to accelerate, brake, or steer left and right.

5.1 Shoot ’Em Up Spaceship

This script moves the player transform left, right, up, or down on the screen. It does not use any physics forces, but instead Transform.Translate manipulates the gameObject’s transform position directly.

public class BaseTopDownSpaceShip : ExtendedCustomMonoBehavior
{	
	private Quaternion targetRotation;
	
	private float thePos;
	private float moveXAmount;
	private float moveZAmount;
		
	public float moveXSpeed=40f;
	public float moveZSpeed=15f;
	
	public float limitX=15f;
	public float limitZ=15f;
		
	private float originZ;
	
	[System.NonSerialized]
	public Keyboard_Input default_input;
	
	public float horizontal_input;
	public float vertical_input;
	
	public virtual void Start()
	{
	// we are overriding Start() so as not to call Init, as we 			// want the game controller to do this in this game.
	didInit=false;
		
	this.Init();
	}
	
	public virtual void Init ()
	{	
	// cache refs to our transform and gameObject
	myTransform= transform;
	myGO= gameObject;
	myBody= rigidbody;
	// add default keyboard input
	default_input= myGO.AddComponent<Keyboard_Input>();
	// grab the starting Z position to use as a baseline for Z 			// position limiting
	originZ=myTransform.localPosition.z;
	
	// set a flag so that our Update function knows when we are OK			// to use
	didInit=true;
	}
	
	public virtual void GameStart ()
	{
	// we are good to go, so let's get moving!
	canControl=true;
	}
	
	public virtual void GetInput ()
	{
	// this is just a 'default' function that (if needs be) should			// be overridden in the glue code
	horizontal_input= default_input.GetHorizontal();
	vertical_input= default_input.GetVertical();
	}
	
	public virtual void Update ()
	{
	UpdateShip ();
	}
	
	public virtual void UpdateShip ()
	{
	// don't do anything until Init() has been run
	if(!didInit)
	return;
	// check to see if we're supposed to be controlling the player			// before moving it
	if(!canControl)
	return;
	GetInput();
	// calculate movement amounts for X and Z axis
	moveXAmount = horizontal_input * Time.deltaTime * moveXSpeed;
	moveZAmount = vertical_input * Time.deltaTime * moveZSpeed;
	Vector3 tempRotation= myTransform.eulerAngles;
	tempRotation.z= horizontal_input * -30f;
	myTransform.eulerAngles=tempRotation;
	
	// move our transform to its updated position
	myTransform.localPosition += new Vector3(moveXAmount, 0, 			moveZAmount);
	// check the position to make sure that it is within boundaries
	if (myTransform.localPosition.x <= -limitX || myTransform.			localPosition.x >= limitX)
	{
	thePos = Mathf.Clamp(myTransform.localPosition.x, -limitX, 			limitX);
	myTransform.localPosition = new Vector3(thePos, myTransform.			localPosition.y, myTransform.localPosition.z);
	}
	// we also check the Z position to make sure that it is within			// boundaries
	if (myTransform.localPosition.z <= originZ || myTransform.			localPosition.z >= limitZ)
	{
	thePos = Mathf.Clamp(myTransform.localPosition.z, originZ, 			limitZ);
	myTransform.localPosition = new Vector3(myTransform.				localPosition.x, myTransform.localPosition.y, thePos);
	}
	}
}

Just like many of the other scripts in this book, after the variable declarations, the Start() function uses a Boolean called didInit to track whether or not the script has been initialized.

public virtual void Start()
	{
	// we are overriding Start() so as not to call Init, as we want			// the game controller to do this in this game.
	didInit=false;
	this.Init();
	}

Rather than having all of the initialization in the Start() function, it has its own function called Init() to keep things tidy. Init() begins by caching references to the transform, gameObject, and rigidbody.

	public virtual void Init ()
	{	
	// cache refs to our transform and gameObject
	myTransform= transform;
	myGO= gameObject;
	myBody= rigidbody;

Input comes from an instance of Keyboard_Controller.cs held in a variable called default_input, which we will look into in detail later in this chapter.

	// add default keyboard input
	default_input= myGO.AddComponent<Keyboard_Input>();

To keep the player within a certain space in the play area, this script uses the z position it starts at to work out how to limit movement. A float named originZ is set:

	// grab the starting Z position to use as a baseline for Z 			// position limiting
	originZ=myTransform.localPosition.z;

Once the Init() function has finished, didInit is set to true.

	// set a flag so that our Update function knows when we are OK to use
	didInit=true;
	}

For the script to function correctly within the framework, all player scripts use a Boolean variable named canControl to decide whether or not the object should be controlled by a third party such as AI or input. Note that this is not the same as enabling or disabling player movement—it just decides whether or not to allow control of the player. The function called GameStart() is called by the game controller script when the time is right to allow control to begin.

	public virtual void GameStart ()
	{
	// we are good to go, so let's get moving!
	canControl=true;
	}

The GetInput() function is virtual and is designed to be overridden if needed. This makes it easier to switch out control systems if you derive a new class from this one for other games. The variables horizontal_input and vertical_input are float values populated by the input controller in this GetInput() function and used to control the player later in the script:

	public virtual void GetInput ()
	{
	// this is just a 'default' function that (if needs be) should 			// be overridden in the glue code
	horizontal_input= default_input.GetHorizontal();
	vertical_input= default_input.GetVertical();
	}

The Update() function calls UpdateShip():

	public virtual void Update ()
	{
	UpdateShip ();
	}
	
	public virtual void UpdateShip ()
	{
	// don't do anything until Init() has been run
	if(!didInit)
	return;
		
	// check to see if we're supposed to be controlling the player 			// before moving it
	if(!canControl)
	return;
	GetInput();

When the amount to move is calculated (using the input values as a multiplier), we use Time.deltaTime to make movement framerate independent. By multiplying the movement amount by deltaTime (the time since the last update), the amount we move adapts to the amount of time in between updates rather than only updating when the engine has time to do so:

	// calculate movement amounts for X and Z axis
	moveXAmount = horizontal_input * Time.deltaTime * moveXSpeed;
	moveZAmount = vertical_input * Time.deltaTime * moveZSpeed;

Quaternion rotations are the underlying form that angles take in the Unity engine. You can use quaternions in your games, but they are notoriously difficult to get to grips with. Thankfully, for the rest of us, there is an alternative in Euler angles. Euler angles are a representation of a rotation in a three-dimensional vector, and it is much easier to define or manipulate a rotation by x, y, and z values.

Unlike UnityScript, in C#, you cannot individually alter the values of each axis in the rotation value of a transform. It has to be copied out into another variable and that variable’s values change before being copied back into the transform. In the code below, the vector from eulerAngles is copied into a variable called tempRotation, and then its z rotation value is altered to an arbitrary multiplier of the horizontal input. This is purely a visual rotation to show that the ship is turning left or right—it makes the spaceship tilt nicely when the left or right keys are held down:

	Vector3 tempRotation= myTransform.eulerAngles;
	tempRotation.z= horizontal_input * -30f;
	myTransform.eulerAngles=tempRotation;

The movement of the spaceship is applied to its transform.localPosition. The reason for this is that, in Interstellar Paranoids, the player is parented to the camera as it makes its way through the level. We do not want to affect the global position of the ship; it needs to stay within the coordinate space of the camera to stay on screen, and only its local position is changed when we move it around:

	// move our transform to its updated position
	myTransform.localPosition += new Vector3(moveXAmount, 0, 			moveZAmount);

Limit checking is very basic for this script, with arbitrary values for the left and right and an amount added to the original local z position of the ship. The center of the screen is at 0 (we start the camera at zero on the x-axis) to keep things clear, using the variable limitX and a negative −limitX to stop the ship leaving the screen.

	// check the position to make sure that it is within boundaries
	if (myTransform.localPosition.x <= -limitX ||					myTransform.localPosition.x >= limitX)
	{

Mathf.Clamp is a useful built-in utility function to take a value and clamp it between a minimum and a maximum value. When you pass a value into Mathf.Clamp, if it is more than the maximum value, then the return value will be the maximum value. If the value passed in is less than the minimum value, it will return the minimum.

In this function, Mathf.Clamp is used to populate the float variable thePos with a value (to use for the z position value) within both horizontal and, later in the code, vertical position limits (limitX and −limitX or limitZ and originZ) from the localPosition of the player transform:

	thePos = Mathf.Clamp(myTransform.localPosition.x, -limitX, limitX);

Once the variable thePos contains a clamped (limited) value for positioning, the transform position is set:

	myTransform.localPosition = new Vector3(thePos,					myTransform.localPosition.y, myTransform.localPosition.z);
	}

The final part of this function deals with the clamping of the z position in the same way that the x was clamped:

	// we also check the Z position to make sure that it is within 			// boundaries
	if (myTransform.localPosition.z <= originZ ||					myTransform.localPosition.z >= limitZ)
	{
	thePos = Mathf.Clamp(myTransform.localPosition.z, originZ, limitZ);
	myTransform.localPosition = new Vector3
	(myTransform.localPosition.x, 
	myTransform.localPosition.y, thePos);
	}
	}	
}

5.2 Humanoid Character

The human control script uses Unity’s character controller and is based on the third-person character controller provided by Unity (which is provided free, as part of the included assets with the Unity game engine).

The complete BaseTopDown.cs script looks like this:

using UnityEngine;
using System.Collections;
public class BaseTopDown : ExtendedCustomMonoBehavior
{
	public AnimationClip idleAnimation;
	public AnimationClip walkAnimation;
	
	public float walkMaxAnimationSpeed = 0.75f;
	public float runMaxAnimationSpeed = 1.0f;
	
	// When did the user start walking (Used for going into run after a			// while)
	private float walkTimeStart= 0.0f;
	
	// we've made the following variable public so that we can use an 			// animation on a different gameObject if needed
	public Animation _animation;
	
	enum CharacterState {
	Idle = 0,
	Walking = 1,
	Running = 2,
	}
	
	private CharacterState _characterState;
	
	// The speed when walking
	public float walkSpeed= 2.0f;
	
	// after runAfterSeconds of walking we run with runSpeed
	public float runSpeed= 4.0f;
	
	public float speedSmoothing= 10.0f;
	public float rotateSpeed= 500.0f;
	public float runAfterSeconds= 3.0f;
	
	// The current move direction in x-z
	private Vector3 moveDirection= Vector3.zero;
	
	// The current vertical speed
	private float verticalSpeed= 0.0f;
	// The current x-z move speed
	public float moveSpeed= 0.0f;
	
	// The last collision flags returned from controller.Move
	private CollisionFlags;
			
	public BasePlayerManager myPlayerController;
	
	[System.NonSerialized]
	public Keyboard_Input default_input;
	
	public float horz;
	public float vert;
	
	private CharacterController controller;
	
	// ----------------------------------------------------------------
	
	void Awake ()
	{
		// we need to do this before anything happens to the script 			// or object, so it happens in Awake.
		// if you need to add specific set-up, consider adding it to			// the Init() function instead to keep this function
		// limited only to things we need to do before 					// anything else happens.
		
		moveDirection = transform.TransformDirection(Vector3.forward);
		
		// if _animation has not been set up in the inspector, we’ll			// try to find it on the current gameobject
		if(_animation==null)
	_animation = GetComponent<Animation>();
		
		if(!_animation)
	Debug.Log("The character you would like to control 			doesn't have animations. Moving her might look 				weird.");
		
		if(!idleAnimation) {
	_animation = null;
	Debug.Log("No idle animation found. Turning off 			animations.");
		}
		if(!walkAnimation) {
	_animation = null;
	Debug.Log("No walk animation found. Turning off 			animations.");
		}
		controller = GetComponent<CharacterController>();
	}
	
	public virtual void Start ()
	{
		Init ();	
	}
	
	public virtual void Init ()
	{
		// cache the usual suspects
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
		// add default keyboard input
		default_input= myGO.AddComponent<Keyboard_Input>();
		// cache a reference to the player controller
		myPlayerController= myGO.GetComponent<BasePlayerManager>();
		if(myPlayerController!=null)
	myPlayerController.Init();
	}
	public void SetUserInput(bool setInput)
	{
		canControl= setInput;	
	}
	public virtual void GetInput()
	{
		horz= Mathf.Clamp(default_input.GetHorizontal() , -1, 1);
		vert= Mathf.Clamp(default_input.GetVertical() , -1, 1);
	}
	public virtual void LateUpdate()
	{
		// we check for input in LateUpdate because Unity recommends			// this
		if(canControl)
	GetInput();
	}
	public bool moveDirectionally;
	private Vector3 targetDirection;
	private float curSmooth;
	private float targetSpeed;
	private float curSpeed;
	private Vector3 forward;
	private Vector3 right;
	void UpdateSmoothedMovementDirection ()
	{
		if(moveDirectionally)
		{
	UpdateDirectionalMovement();
		} else {
	UpdateRotationMovement();
		}
	}
	void UpdateDirectionalMovement()
	{
		// find target direction
		targetDirection= horz * Vector3.right;
		targetDirection+= vert * Vector3.forward;
		
		// We store speed and direction seperately,
		// so that when the character stands still we still have a 				// valid forward direction
		// moveDirection is always normalized, and we only update it			// if there is user input.
		if (targetDirection != Vector3.zero)
		{
	moveDirection = Vector3.RotateTowards(moveDirection, 			targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.			deltaTime, 1000);
	moveDirection = moveDirection.normalized;
		}
		// Smooth the speed based on the current target direction
		curSmooth= speedSmoothing * Time.deltaTime;
		// Choose target speed
		//* We want to support analog input but make sure you can't 			// walk faster diagonally than just forward or sideways
		targetSpeed= Mathf.Min(targetDirection.magnitude, 1.0f);
		_characterState = CharacterState.Idle;
		// decide on animation state and adjust move speed
		if (Time.time - runAfterSeconds > walkTimeStart)
		{
	targetSpeed *= runSpeed;
	_characterState = CharacterState.Running;
		}
		else
		{
	targetSpeed *= walkSpeed;
	_characterState = CharacterState.Walking;
		}
		moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
		// Reset walk time start when we slow down
		if (moveSpeed < walkSpeed * 0.3f)
	walkTimeStart = Time.time;
		// Calculate actual motion
		Vector3 movement= moveDirection * moveSpeed;
		movement *= Time.deltaTime;
		// Move the controller
		collisionFlags = controller.Move(movement);
		// Set rotation to the move direction
		myTransform.rotation = Quaternion.LookRotation(moveDirection);
	}
	void UpdateRotationMovement ()
	{
		// this character movement is based on the code in the Unity			// help file for CharacterController.SimpleMove
		// http://docs.unity3d.com/Documentation/ScriptReference/				// CharacterController.SimpleMove.html
	myTransform.Rotate(0, horz * rotateSpeed * Time.deltaTime, 0);
	curSpeed = moveSpeed * vert;
		controller.SimpleMove(myTransform.forward * curSpeed);
		// Target direction (the max we want to move, used for 				// calculating target speed)
		targetDirection= vert * myTransform.forward;
		// Smooth the speed based on the current target direction
		float curSmooth= speedSmoothing * Time.deltaTime;
		// Choose target speed
		//* We want to support analog input but make sure you can't 			// walk faster diagonally than just forward or sideways
		targetSpeed= Mathf.Min(targetDirection.magnitude, 1.0f);
		_characterState = CharacterState.Idle;
		// decide on animation state and adjust move speed
		if (Time.time - runAfterSeconds > walkTimeStart)
		{
	targetSpeed *= runSpeed;
	_characterState = CharacterState.Running;
		}
		else
		{
	targetSpeed *= walkSpeed;
	_characterState = CharacterState.Walking;
		}
		moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
		// Reset walk time start when we slow down
		if (moveSpeed < walkSpeed * 0.3f)
	walkTimeStart = Time.time;
	}
	void Update ()
	{	
		if (!canControl)
		{
	// kill all inputs if not controllable.
	Input.ResetInputAxes();
		}
		UpdateSmoothedMovementDirection();
		// ANIMATION sector
		if(_animation) {
	if(controller.velocity.sqrMagnitude < 0.1f) {
	_animation.CrossFade(idleAnimation.name);
	}
	else
	{
	if(_characterState == CharacterState.Running) {
		_animation[walkAnimation.name].speed = 			Mathf.Clamp(controller.velocity.magnitude, 0.0f, 				runMaxAnimationSpeed);
	_animation.CrossFade(walkAnimation.name);	
	}
	else if(_characterState == CharacterState.Walking) {
		  _animation[walkAnimation.name].speed = 				  Mathf.Clamp(controller.velocity.magnitude, 			  0.0f, walkMaxAnimationSpeed);
	
	_animation.CrossFade(walkAnimation.name);
	}
	}
		}	
	}
	
	public float GetSpeed ()
	{
		return moveSpeed;
	}
		
	public Vector3 GetDirection ()
	{
		return moveDirection;
	}
	
	public bool IsMoving ()
	{
		return Mathf.Abs(vert) + Mathf.Abs(horz) > 0.5f;
	}
	
	public void Reset ()
	{
		gameObject.tag = "Player";
	}
}

5.2.1 Script Breakdown

The script derives from ExtendedCustomMonoBehavior, as described in Chapter 4, and it adds some extra common variables to the class:

public class BaseTopDown : ExtendedCustomMonoBehavior
{

By using @script RequireComponent, this code tells the Unity engine that it requires the controller specified by RequireComponent. If there is not one already attached to the gameObject this script is attached to, the Unity engine will automatically add an instance of one at runtime, although it is intended that the CharacterController reference be set up in the Unity editor before the script is run:

	// Require a character controller to be attached to the same game object
	// @script RequireComponent(CharacterController)

Note that many of the variable declarations will make more sense later in this section, once we get to look at the actual code using them. For that reason, we skip most of them here and just highlight the ones that either stand out or require more information.

The Animation component should be attached to the gameObject that is to be animated. As this may not always be the same object as this script is attached to, the _animation variable used to coordinate animations later in the script is declared as public. Use the Unity editor Inspector window to reference the Animation component:

	public Animation _animation;

The enum keyword is used to declare an enumeration. An enumeration is a set of constants called an enumeration list, which is a method of describing something with words (keeping the code easily readable), yet at the same time, having the inner workings of the engine use numbers to make processing more efficient. At the beginning of the script, we start with the declaration of an enumeration called CharacterState. This will be used to hold the state of the player’s animation:

	enum CharacterState {
		Idle = 0,
		Walking = 1,
		Running = 2,
	}

Most of the variable declarations are uneventful and do not require full descriptions, although there are a few more variables of note:

CharacterState _characterState

This holds the player’s animation state (based on the enumeration list CharacterState shown earlier in this section).

BasePlayerManager myPlayerController

Although the player controller is not used by this script, it holds a reference to it so that other scripts can find it easily.

Keyboard_Input default_input

The default input system is the standard keyboard input script.

CharacterController controller

Unity’s built-in character controller is used for the physics simulation of the character controlled by this script.

On to the Awake() function:

	void Awake ()
	{

The moveDirection variable is a Vector3 used to tell the character controller which direction to move in. Here, it is given the world forward vector as a default value:

	moveDirection = transform.TransformDirection(Vector3.forward);

The Animation component (a component built into Unity) is used to animate the character. GetComponent() is used to find the instance of the animation component on the gameObject this script is attached to:

	// if _animation has not been set up in the inspector, we'll try to			// find it on the current gameobject
	if(_animation==null)
	_animation = GetComponent<Animation>();
	if(!_animation)
		Debug.Log("The character you would like to control doesn't 				have animations. Moving her might look weird.");

Animations for idle, walking, running, and jumping should be referenced by the Inspector window on the character for them to work with this script. If any of them are missing, however, we don’t want it to stop working altogether; a message to report what is missing will be written to the debug window instead via Debug.Log:

	if(!idleAnimation) {
	_animation = null;
	Debug.Log("No idle animation found. Turning off animations.");
	}
	if(!walkAnimation) {
	_animation = null;
	Debug.Log("No walk animation found. Turning off animations.");
		}

Init() caches references to the characters rigidbody, gameObject, and transform before grabbing references to some required scripts. To work within the player structure, Keyboard_Input.cs is used to provide input from the player to move the character around. BasePlayerManager.cs is also referenced in the Init() function, and although this script does not use it directly, it does store a reference to the player manager for other scripts to access:

	public virtual void Start ()
	{
		Init ();
	}
	
	public virtual void Init ()
	{
		// cache the usual suspects
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
		
		// add default keyboard input
		default_input= myGO.AddComponent<Keyboard_Input>();
		
		// cache a reference to the player controller
		myPlayerController= myGO.GetComponent<BasePlayerManager>();
		
		if(myPlayerController!=null)
	myPlayerController.Init();
	}

The SetUserInput() function is used to set canControl, which is used to check whether user input should be disabled or not:

	public void SetUserInput(bool setInput)
	{
		canControl= setInput;	
	}

Having input separated into a single GetInput() function should make it easy to customize inputs if required. Here, the horz, vert, and jumpButton Boolean variables are set by the default_input (a Keyboard_Input.cs instance) functions. These variables are used to control the character controller further in the script:

	public virtual void GetInput()
	{
		horz= Mathf.Clamp(default_input.GetHorizontal() , -1, 1);
		vert= Mathf.Clamp(default_input.GetVertical() , -1, 1);
	}

LateUpdate() is used to call for an update to the input Boolean variables via the GetInput() script. Unity advises LateUpdate as the best place to check for keyboard entry, after the engine has carried out its own input updates:

	public virtual void LateUpdate()
	{
		// we check for input in LateUpdate because Unity recommends			// this
		if(canControl)
			GetInput();
	}

This script was adapted from the third-person controller code provided by Unity. Its original behavior was to move the player based on the direction of the camera. For example, when the right key was pressed, the player would move toward the right of the screen based on the camera location and rotation (right being along the camera’s x-axis). Player rotation in the original script was determined by the camera rotation, and for the movement to make sense, it was reliant on the camera being behind the player during gameplay. Of course, this control scheme would not work with a top-down camera system or any camera system other than the one it was intended for.

As the script was adapted, the dependence on the camera rotation was removed and replaced with a directional approach. In this system, pressing the right key moves right along the world x-axis and pressing up or down moves up or down along the world z-axis.

For some top-down character-based games, the directional approach is a good one, but the genre is not exactly locked into this scheme so we also provide a rotational movement approach. Whenever moveDirectionally is false, pressing the left or right button will rotate the player left or right around its y-axis. Pressing the up button will walk forward along the player’s z-axis, and pressing down will move it backward.

In the UpdateSmoothedMovementDirection() script, the moveDirectionally variable is checked to determine which type of movement function to call to update player movement:

	void UpdateSmoothedMovementDirection ()
	{			
		if(moveDirectionally)
		{
			UpdateDirectionalMovement();
		} else {
			UpdateRotationMovement();
		}
	}

Directional movement uses the world x- and z-axes to decide which way to move; that is, the player moves along the world x- or z-axis based on the button pressed by the player. The UpdateDirectionalMovement() function starts by taking the horizontal and vertical user inputs (from the variables horz and vert) and multiplying them by either the world forward vector or the world right vector, respectively. Note that the targetDirection variable is of Vector3 type and that it starts out being set to the horizontal direction and then has the vertical added to it, encompassing both vertical and horizontal movement into the same vector:

	void UpdateDirectionalMovement()
	{
		// find target direction
		targetDirection= horz * Vector3.right;
		targetDirection+= vert * Vector3.forward;

As the code comments will tell you, we store speed and direction separately. By doing so, we can have the character face a particular direction without actually moving, a behavior sometimes useful for this type of game.

The targetDirection variable will be zero when there is no user input, so before calculating any movement amounts, we check that movement is intended first:

		// We store speed and direction separately,
		// so that when the character stands still we still have a 				// valid forward direction
		// moveDirection is always normalized, and we only update it			// if there is user input.
		if (targetDirection != Vector3.zero)
		{

The modeDirection vector will be used to move the player around the arena. Its value comes from an interpolation between the current value of moveDirection and the targetDirection calculated earlier from the user inputs. This vector is then multiplied by rotateSpeed, then converted into radians, and again multiplied by Time.deltaTime to make the rotation speed time based. Vector3.RotateTowards is used to make the turn, which ensures that the rotation ends in the exact specified position.

The Unity documentation describes Vector3.RotateTowards as follows:

This function is similar to MoveTowards except that the vector is treated as a direction rather than a position. The current vector will be rotated round toward the target direction by an angle of maxRadiansDelta, although it will land exactly on the target rather than overshoot. If the magnitudes of current and target are different then the magnitude of the result will be linearly interpolated during the rotation. If a negative value is used for maxRadiansDelta, the vector will rotate away from target until it is pointing in exactly the opposite direction, then stop.

The moveDirection vector is then normalized to remove any magnitude from the equation:

	moveDirection = Vector3.RotateTowards(moveDirection, 			targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.			deltaTime, 1000);
	moveDirection = moveDirection.normalized;
		}

The next part of the BaseTopDown.cs script deals with speed. The variable curSmooth will be used further in the script to decide how long it takes for the movement speed of the player to go from zero to max speed:

		// Smooth the speed based on the current target direction
		curSmooth= speedSmoothing * Time.deltaTime;

The variable targetSpeed is capped to a maximum of 1. To do this, Mathf.Min is fed with the magnitude of the targetDirection and 1. Whichever is least will go into targetSpeed:

		// Choose target speed
		//* We want to support analog input but make sure you can't 			// walk faster diagonally than just forwards or sideways
		targetSpeed= Mathf.Min(targetDirection.magnitude, 1.0f);

The variable _characterState starts out set to CharacterState.Idle:

		_characterState = CharacterState.Idle;

This script allows for two states of movement: running and walking. The transition between running and walking is timed—the character will begin walking and continue walking until vertical input is applied for more than (in seconds) the value of runAfterSeconds. The animation and movement speed will then transition into running and continue to run until the vertical input stops.

Time.time is used to track how long the player has been walking, which means that Time.timeScale will affect it (if the game is paused or time is scaled, this timing will remain relative to the timescale):

		// decide on animation state and adjust move speed
		if (Time.time - runAfterSeconds > walkTimeStart)
		{

If the state is running, then we multiply the targetSpeed by runSpeed, and then set the animation state to running:

	targetSpeed *= runSpeed;
	_characterState = CharacterState.Running;
		}
		else
		{

The walkSpeed variable is a multiplier for the targetSpeed when the player is in its walking state:

	targetSpeed *= walkSpeed;
	_characterState = CharacterState.Walking;
	}

The moveSpeed variable was used earlier in the function, but it only gets calculated here because it relies on targetSpeed and curSmooth to have been worked out. It’s OK that it gets calculated so late in the function, as although it only gets picked up on the next update, the delay is so small that it is unnoticeable.

moveSpeed is an interpolation (using Mathf.Lerp) between its current value and the variable targetSpeed over an amount of time set by curSmooth.

When moveSpeed drops below the threshold set by walkSpeed (essentially, when the player releases the move input enough to slow movement down), walkTimeStart gets reset so that the walk will start again next time the player moves:

	moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
	// Reset walk time start when we slow down
	if (moveSpeed < walkSpeed * 0.3f)
	walkTimeStart = Time.time;

The final part of the UpdateDirectionalMovement() function tells the character controller to move and sets the rotation of myTransform to match the movement direction. In the original Unity third-person controller script this code was adapted from, this code was in the Update() function. It was moved here for the purpose of keeping all directional movement code within a single function.

The variable movement takes moveDirection and multiplies it by moveSpeed; then, on the next line, the result is multiplied by Time.deltaTime to provide a time-based movement vector with magnitude suitable for passing into the character controller. When the controller is told to Move (with the movement vector passed in as a parameter), it will return a bitmask called CollisionFlags. When a collision occurs with the character controller, the collisionFlags variable may be used to tell where the collision happened; the collisionFlags mask represents None, Sides, Above, or Below. Although the collision data are not used in this version of BaseTopDown.cs, it is implemented for future functionality:

	// Calculate actual motion
	Vector3 movement= moveDirection * moveSpeed;
	movement *= Time.deltaTime;
	// Move the controller
	collisionFlags = controller.Move(movement);

In the last line of the function, Quaternion.LookRotation creates a rotation toward moveDirection and sets myTransform.rotation to it—making the player face in the correct direction for the movement:

	// Set rotation to the move direction
	myTransform.rotation = Quaternion.LookRotation(moveDirection);	
	}

The second type of movement in BaseTopDown.cs is rotational movement. UpdateRotationalMovement() uses a combination of transform.Rotate and the character controller’s SimpleMove function to provide a control system that rotates the player on its y-axis and moves along its z-axis (in the case of the humanoid, this is forward and backward walking or running).

For horizontal input, the variable horz is multiplied by the variable rotateSpeed and multiplied by Time.deltaTime to be used as the rotation for the player’s (myTransform) y rotation:

	void UpdateRotationMovement ()
	{
		myTransform.Rotate(0, horz * rotateSpeed * Time.deltaTime, 0);

Vertical input (stored in vert) is dealt with next, as vert multiplied by moveSpeed makes up the speed at which to traverse the player’s z-axis.

Note that moveSpeed is calculated further down in the function, so don’t worry about that right away—just keep in mind that moveSpeed is the desired speed of movement:

	curSpeed = moveSpeed * vert;

CharacterController.SimpleMove moves a character controller, taking into account the velocity set by the vector passed into it. In this line, the vector provided to SimpleMove is the player’s forward vector multiplied by curSpeed:

	controller.SimpleMove(myTransform.forward * curSpeed);

targetDirection is a Vector3-type variable that determines how much, based on the vert input variable, we would like to move, even though its vector is only used in calculating for its magnitude later on in the function:

	// Target direction (the max we want to move, used for calculating 			// target speed)
	targetDirection= vert * myTransform.forward;

When the speed changes, rather than changing the animation instantly from slow to fast, the variable curSmooth is used to transition speed smoothly. The variable speedSmoothing is intended to be set in the Unity Inspector window, and it is then multiplied by Time.deltaTime to make the speed transition time based:

	// Smooth the speed based on the current target direction
	float curSmooth= speedSmoothing * Time.deltaTime;

The next line of the script caps the targetSpeed at 1. Taking the targetDirection vector from earlier, targetSpeed takes a maximum value of 1 or, if it is less than 1, the value held by targetDirection.magnitude:

	// Choose target speed
	//* We want to support analog input but make sure you can't walk 			// faster diagonally than just forwards or sideways
	targetSpeed= Mathf.Min(targetDirection.magnitude, 1.0f);

The rest of the function transitions between walking and running in the same way that the UpdateDirectionalMovement()function did earlier in this section:

	_characterState = CharacterState.Idle;
	// decide on animation state and adjust move speed
	if (Time.time - runAfterSeconds > walkTimeStart)
	{
		targetSpeed *= runSpeed;
		_characterState = CharacterState.Running;
	}
	else
	{
		targetSpeed *= walkSpeed;
		_characterState = CharacterState.Walking;
	}
	moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
	// Reset walk time start when we slow down
	if (moveSpeed < walkSpeed * 0.3f)
		walkTimeStart = Time.time;
	}

Update() begins with a call to stop input when canControl is set to false. More than anything, this is designed to bring the player to a stop when canControl is set at the end of the game. When the game ends, rather than inputs continuing and the player being left to run around behind the game-over message, inputs are set to zero:

	void Update ()
	{
		if (!canControl)
		{
			// kill all inputs if not controllable.
			Input.ResetInputAxes();
		}

UpdateSmoothedMovementDirection(), as described earlier in this chapter, calls one of two movement update scripts, based on the value of the Boolean variable moveDirectionally. After this is called, the final part of the Update() function takes care of coordinating the animation of the player (provided that there is a reference to an animation component in the variable _animation):

		UpdateSmoothedMovementDirection();
		// ANIMATION sector
		if(_animation) {
			if(controller.velocity.sqrMagnitude < 0.1f) {

The controller’s velocity is used to track when it is time to play the idle animation. The _characterState will not come into play if velocity is less than 0.1f, since we can guarantee at this velocity that the idle animation should be playing above anything else. Animation.CrossFade() transitions from the current animation to the new one smoothly or continues to play the current animation if it is the same as the one being passed in.

The actual animation data are stored in their own variables of type AnimationClip: runAnimation, walkAnimation, and idleAnimation. Note that Animation.CrossFade uses the name of the animation rather than the object:

	_animation.CrossFade(idleAnimation.name);
			}
			else
			{

When the velocity is higher than 0.1, the script relies on _characterState to provide the correct animation for whatever the player is doing. _characterState refers to the enumeration list at the beginning of the script called CharacterState.

The CharacterState animation state held by _characterState is set by the movement scripts (UpdateDirectionalMovement() and UpdateRotationalMovement() functions), but the speeds that the animations play at are decided here using the controller.velocity.magnitude (the speed at which the character controller is moving) clamped to runMaxAnimationSpeed to keep the number reasonable.

First, the _characterState is checked for running, and the run animation is played at the speed of runMaxAnimationSpeed:

			if(_characterState == CharacterState.Running) {
	_animation[walkAnimation.name].speed = 						Mathf.Clamp(controller.velocity.magnitude, 0.0f, 					runMaxAnimationSpeed);
	_animation.CrossFade(walkAnimation.name);
			}

The final state is CharacterState.Walking played at the speed of walkMaxAnimationSpeed:

			else if(_characterState == CharacterState.Walking) {
	_animation[walkAnimation.name].speed = 						Mathf.Clamp(controller.velocity.magnitude, 0.0f, 					walkMaxAnimationSpeed);
	_animation.CrossFade(walkAnimation.name);
				}
			}
		}
	}

At the end of the BaseTopDown.cs script, GetSpeed(), GetDirection(), and IsMoving() are utility functions provided for other scripts to have access to a few of the character’s properties:

	public float GetSpeed ()
	{
		return moveSpeed;
	}
		
	public Vector3 GetDirection ()
	{
		return moveDirection;
	}
	
	public bool IsMoving ()
	{
		 return Mathf.Abs(vert) + Mathf.Abs(horz) > 0.5f;
	}
}

5.3 Wheeled Vehicle

The BaseVehicle.cs script relies on Unity’s built-in WheelCollider system. WheelCollider components are added to gameObjects positioned in place of the wheels supporting the car body. The WheelCollider components themselves do not have any visual form, and meshes representing wheels are not aligned automatically. WheelColliders are physics simulations; they take care of all of the physics actions such as suspension and wheel friction.

The Unity documentation describes the WheelCollider as

…a special collider for grounded vehicles. It has built-in collision detection, wheel physics, and a slip-based tire friction model.

The physics and math behind the friction system are beyond the scope of this book, though thankfully, the Unity documentation has an extensive investigation of the subject on the WheelCollider page in the online help (http://docs.unity3d.com/Documentation/Components/class-WheelCollider.html).

The BaseVehicle.cs script uses four WheelColliders for its drive (in the example games, these are attached to empty gameObjects), and the actual orientation of wheel meshes is dealt with by a separate script called BaseWheelAlignment.cs and will be explained in full later on in the chapter.

BaseVehicle.cs looks like this:

using UnityEngine;
using System.Collections;
public class BaseVehicle : ExtendedCustomMonoBehavior
{
	public WheelCollider frontWheelLeft;
	public WheelCollider frontWheelRight;
	public WheelCollider rearWheelLeft;
	public WheelCollider rearWheelRight;
	public float steerMax = 30f;
	public float accelMax = 5000f;
	public float brakeMax = 5000f;
	public float steer = 0f;
	public float motor = 0f;
	public float brake = 0f;
	public float mySpeed;
	public bool isLocked;
	[System.NonSerialized]
	public Vector3 velo;
	[System.NonSerialized]
	public Vector3 flatVelo;
	public BasePlayerManager myPlayerController;
	[System.NonSerialized]
	public Keyboard_Input default_input;
	public AudioSource engineSoundSource;
	public virtual void Start ()
	{
		Init ();
	}
	public virtual void Init ()
	{
		// cache the usual suspects
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
		// add default keyboard input
		default_input= myGO.AddComponent<Keyboard_Input>();
		// cache a reference to the player controller
		myPlayerController= myGO.GetComponent<BasePlayerManager>();
		// call base class init
		myPlayerController.Init();
		// with this simple vehicle code, we set the center of mass 			// low to try to keep the car from toppling over
		myBody.centerOfMass= new Vector3(0,-4f,0);
		// see if we can find an engine sound source, if we need to
		if(engineSoundSource==null)
		{
			engineSoundSource= myGO.GetComponent<AudioSource>();
		}
	}
	public void SetUserInput(bool setInput)
	{
		canControl= setInput;	
	}
	public void SetLock(bool lockState)
	{
		isLocked = lockState;
	}
	public virtual void LateUpdate()
	{
		// we check for input in LateUpdate because Unity recommends			// this
		if(canControl)
			GetInput();
		// update the audio
		UpdateEngineAudio();
	}
	public virtual void FixedUpdate()
	{
		UpdatePhysics();
}
	
	public virtual void UpdateEngineAudio()
	{
		// this is just a 'made up' multiplier value applied to 				// mySpeed.
		 engineSoundSource.pitch= 0.5f + (Mathf.Abs(mySpeed) * 0.005f);
	}
		
	public virtual void UpdatePhysics()
	{
	CheckLock();
		// grab the velocity of the rigidbody and convert it into 				// flat velocity (remove the Y)
		velo= myBody.angularVelocity;
		// convert the velocity to local space so we can see how 				// fast we're moving forward (along the local z axis)
		velo= transform.InverseTransformDirection(myBody.velocity);
		flatVelo.x= velo.x;
		flatVelo.y= 0;
		flatVelo.z= velo.z;
		
		// work out our current forward speed
		mySpeed= velo.z;
		
		 // if we're moving slow, we reverse motorTorque and remove // brakeTorque so that the car will reverse
		if(mySpeed<2)
		{
			 // that is, if we're pressing down the brake key // (making brake>0)
			if(brake>0)
			{
				rearWheelLeft.motorTorque = -brakeMax * 						brake;
			rearWheelRight.motorTorque = -brakeMax * brake;
				rearWheelLeft.brakeTorque = 0;
			rearWheelRight.brakeTorque = 0;
			frontWheelLeft.steerAngle = steerMax * steer;
			frontWheelRight.steerAngle = steerMax * steer;
				// drop out of this function before applying 					// the 'regular' non-reversed values to the 						// wheels
				return;
			}
		}
		// apply regular movement values to the wheels
		rearWheelLeft.motorTorque = accelMax * motor;
	rearWheelRight.motorTorque = accelMax * motor;
	rearWheelLeft.brakeTorque = brakeMax * brake;
	rearWheelRight.brakeTorque = brakeMax * brake;
	frontWheelLeft.steerAngle = steerMax * steer;
	frontWheelRight.steerAngle = steerMax * steer;
	}
	public void CheckLock()
	{
	if (isLocked)
	{
		// control is locked out and we should be stopped
		steer = 0;
		brake = 0;
		motor = 0;
		// hold our rigidbody in place (but allow the Y to move so 				// the car may drop to the ground if it is not exactly 				// matched to the terrain)
		Vector3 tempVEC = myBody.velocity;
		tempVEC.x = 0;
		tempVEC.z = 0;
		myBody.velocity = tempVEC;
	}
	}
	public virtual void GetInput()
	{
		// calculate steering amount
		steer= Mathf.Clamp(default_input.GetHorizontal() , -1, 1);
		// how much accelerator?
		Motor= Mathf.Clamp(default_input.GetVertical() , 0, 1);
		// how much brake?
		Brake= -1 * Mathf.Clamp(default_input.GetVertical() , -1, 0);
	}
}

5.3.1 Script Breakdown

The class derives from ExtendedCustomMonoBehavior (as discussed in Chapter 4), which adds a few commonly used variables.

Each WheelCollider variable is named based on its intended position: frontWheelLeft, frontWheelRight, rearWheelLeft, and rearWheelRight. For the script to function correctly, WheelColliders need to be referenced (dragged and dropped) into the correct variables in the Unity Inspector window on the vehicle script:

using UnityEngine;
using System.Collections;
public class BaseVehicle : ExtendedCustomMonoBehavior
{
	public WheelCollider frontWheelLeft;
	public WheelCollider frontWheelRight;
	public WheelCollider rearWheelLeft;
	public WheelCollider rearWheelRight;

A player manager (BasePlayerManager.cs) is referenced by this script, but it is unused by the rest of the script and does nothing other than call its initialization function Init(). It is here to fit into the framework and for future development of the script:

	public BasePlayerManager myPlayerController;

The default input system is a keyboard script in which there is no need to serialize because it is only used internally:

	[System.NonSerialized]
	public Keyboard_Input default_input;

An AudioSource is required on the vehicle to make an engine noise. To set one up, add an AudioSource to the gameObject (Component → Audio → AudioSource) and reference an engine audio clip. On the AudioSource, check the boxes for Loop and Play On Awake:

	public AudioSource engineSoundSource;

The Start() function is called by the engine automatically. It calls the Init() function, which begins by caching references to commonly used objects:

	public virtual void Start ()
	{
		Init();
	}
	public virtual void Init ()
	{
		// cache the usual suspects
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
GameObject.AddComponent adds the default input controller to the gameObject:
		// add default keyboard input
		default_input= myGO.AddComponent<Keyboard_Input>();

A BasePlayerManager script should already be attached to the gameObject, as per the standard player setup for the framework, but just to be safe, a null check is made before calling the Init() function:

		// cache a reference to the player controller
		myPlayerController= myGO.GetComponent<BasePlayerManager>();
		// call base class init
		if(myPlayerController!=null)
			myPlayerController.Init();

The center of mass may be set on a rigidbody to change the way it behaves in the physics simulation. The farther the center of mass is from the center of the rigidbody, the more it will affect the physics behavior. Setting the center of mass down at −4, for example, will help a lot to keep the car stable. This is a method often employed to keep vehicles from toppling over in games, but it comes at a cost. If the car were to roll and flip over, it will roll in an unrealistic way because of the way that the center of mass will affect its movement. Look to the stabilizer bar simulation for a more realistic solution, but for now, this is as far as we go:

		 // with this simple vehicle code, we set the center of mass // low to try to keep the car from toppling over
		myBody.centerOfMass= new Vector3(0,-4f,0);

If no engine sound source has been set up via the Inspector window of the Unity editor, the script tries to find it with a call to GameObject.GetComponent(). The sound source will be stored in engineSoundSource and used solely for engine sound effects:

		// see if we can find an engine sound source, if we need to
		if(engineSoundSource==null)
		{
			engineSoundSource= myGO.GetComponent<AudioSource>();
		}
	}

As with all of the movement controllers, SetUserInput sets a flag to decide whether or not inputs should be used to drive the vehicle:

	public void SetUserInput(bool setInput)
	{
		canControl= setInput;	
	}

Unlike other movement controllers from this book so far, this one introduces a lock state. The lock state is for holding the vehicle in place without affecting its y-axis. This differs from just disabling user input as it adds physical constraints to the physics object to hold it still (its purpose being to hold the vehicle during the counting in at the start of a race or other similar scenario):

	public void SetLock(bool lockState)
	{
		isLocked = lockState;
	}

LateUpdate() starts by checking for input via the GetInput() function, when canControl is set to true. Next, UpdateEngineAudio() is called to update the pitch of the engine sound based on the engine speed:

	public virtual void LateUpdate()
	{
		// we check for input in LateUpdate because Unity recommends			// this
		if(canControl)
			GetInput();
		// update the audio
		UpdateEngineAudio();
	}

To keep everything tidy, all of the physics functionality is called from FixedUpdate(), but it actually resides in the UpdatePhysics() function:

	public virtual void FixedUpdate()
	{
		UpdatePhysics();
	}

Changing the pitch of the engine AudioSource object makes a convincing motor sound. As it may be possible for the variable mySpeed to be negative, Mathf.Abs is used, as well as an arbitrary multiplier to bring the number down to something that changes the pitch in a satisfactory manner. There’s no science here! The number was decided by trial and error, just adjusting its value until the motor sounded right:

	public virtual void UpdateEngineAudio()
	{
		// this is just a 'made up' multiplier value applied to 				// mySpeed.
		engineSoundSource.pitch= 0.5f + (Mathf.Abs(mySpeed) * 				0.005f);
	}

The real guts of the script may be found in the UpdatePhysics() function. CheckLock() is called first, which takes care of holding the vehicle in place when the lock is on (the variable is Locked set to true):

	public virtual void UpdatePhysics()
	{
		CheckLock();

velo is a variable holding the velocity of the car’s rigidbody:

		// grab the velocity of the rigidbody and convert it into 				// flat velocity (remove the Y)
		velo= myBody.angularVelocity;		

The next task is to find out the velocity in the local coordinate space, remove its y-axis, and use its value along the z-axis to put into the variable mySpeed:

	// convert the velocity to local space so we can see how fast we're			// moving forward (along the local z axis)
	velo= transform.InverseTransformDirection(myBody.velocity);
	flatVelo.x= velo.x;
	flatVelo.y= 0;
	flatVelo.z= velo.z;
	// work out our current forward speed
	mySpeed= velo.z;

mySpeed is then checked to see whether its value is less than 2. When the speed is slow and the brake key is held down, brakeTorque is no longer applied to the wheels; instead, a reverse amount of the maximum amount of brake torque (brakeMax) is applied to make the vehicle move backwards. Note that brakeMax is both the maximum amount of torque applied into the brakeTorque property of the WheelColliders during braking and the maximum amount of torque applied as motorTorque during reversing:

	// if we're moving slow, we reverse motorTorque and remove 				// brakeTorque so that the car will reverse
	if(mySpeed<2)
	{
		// that is, if we're pressing down the brake key (making 				// brake>0)
		if(brake>0)
		{
			rearWheelLeft.motorTorque = -brakeMax * brake;
		rearWheelRight.motorTorque = -brakeMax * brake;

In reverse mode, no brakeTorque will be applied to the WheelColliders so that the vehicle is free to move backward:

			rearWheelLeft.brakeTorque = 0;
		rearWheelRight.brakeTorque = 0;

When the vehicle is reversing, the steering is applied in reverse to switch over the steering when going backward, as a real car would:

		frontWheelLeft.steerAngle = steerMax * steer;
		frontWheelRight.steerAngle = steerMax * steer;

If the reverse conditions have been applied to the WheelColliders, this function drops out before the regular (forward) values are applied:

		 // drop out of this function before applying the 'regular' // non-reversed values to the wheels
		return;
	}
	}

In this script, driving the wheels of WheelColliders is a case of applying motorTorque and brakeTorque and setting the steerAngle:

	// apply regular movement values to the wheels
	rearWheelLeft.motorTorque = accelMax * motor;
	rearWheelRight.motorTorque = accelMax * motor;
	rearWheelLeft.brakeTorque = brakeMax * brake;
	rearWheelRight.brakeTorque = brakeMax * brake;
	frontWheelLeft.steerAngle = steerMax * steer;
	frontWheelRight.steerAngle = steerMax * steer;
	}

The CheckLock() function looks to see whether the Boolean variable isLocked is set to true; when it is, it freezes all inputs to zero and zeroes the x and z velocities of the vehicle’s rigidbody. The y velocity is left as is, so that gravity will still apply to the vehicle when it is being held in place. This is important at the start of a race when the vehicle is being held until the 3, 2, 1 counting in finishes. If the velocity along the y-axis were zeroed too, the car would float just above its resting place against the ground until the race started when it would drop down some as gravity is applied:

	public void CheckLock()
	{
		if (isLocked)
		{
			// control is locked out and we should be stopped
			steer = 0;
			brake = 0;
			motor = 0;
			 // hold our rigidbody in place (but allow the Y to // move so the car may drop to the ground if it is not // exactly matched to the terrain)
			Vector3 tempVEC = myBody.velocity;
			tempVEC.x = 0;
			tempVEC.z = 0;
			myBody.velocity = tempVEC;
		}
	}

steer, motor, and brake are float variables used for moving the vehicle around. GetInput() uses Mathf.Clamp to keep them to reasonable values as they are set by the default input system:

	public virtual void GetInput()
	{
		// calculate steering amount
		steer= Mathf.Clamp(default_input.GetHorizontal() , -1, 1);
		// how much accelerator?
	motor= Mathf.Clamp(default_input.GetVertical() , 0, 1);
		// how much brake?
		brake= -1 * Mathf.Clamp(default_input.GetVertical() , -1, 0);
	}
}

5.3.2 Wheel Alignment

WheelColliders will do a great job of acting like wheels, but if we want to actually see wheels, there is still more work to be done; each wheel mesh needs to be positioned to match that of its WheelCollider counterpart.

The script in full looks like this:

// based on work done in this forum post:
// http://forum.unity3d.com/threads/50643-How-to-make-a-physically-real-	// stable-car-with-WheelColliders
// by Edy.
using UnityEngine;
using System.Collections;
public class BaseWheelAlignment : MonoBehavior
{
	 // Define the variables used in the script, the Corresponding 	// collider is the wheel collider at the position of
	 // the visible wheel, the slip prefab is the prefab instantiated 	// when the wheels slide, the rotation value is the
	// value used to rotate the wheel around its axle.
	public WheelCollider CorrespondingCollider;
	public GameObject slipPrefab;
	public float slipAmountForTireSmoke= 50f;
	private float RotationValue = 0.0f;
	private Transform myTransform;
	private Quaternion zeroRotation;
	private Transform colliderTransform;
	private float suspensionDistance;
	void Start ()
	{
		// cache some commonly used things...
		myTransform= transform;
		zeroRotation= Quaternion.identity;
		colliderTransform= CorrespondingCollider.transform;
	}
	void Update ()
	{
		// define a hit point for the raycast collision
		RaycastHit hit;
		 // Find the collider's center point, you need to do this 	// because the center of the collider might not actually be
		 // the real position if the transform's off.
		Vector3 ColliderCenterPoint= 								colliderTransform.TransformPoint(CorrespondingCollider.				center);
		 // now cast a ray out from the wheel collider's center the // distance of the suspension, if it hit something, then use // the "hit" variable's data to find where the
		 // wheel hit, if it didn't, then set the wheel		// to be fully extended along the suspension.
		 if (Physics.Raycast(ColliderCenterPoint, -colliderTransform.up, out hit, 		CorrespondingCollider.suspensionDistance + CorrespondingCollider.radius)) {
			 myTransform.position= hit.point + (colliderTransform.up * 		CorrespondingCollider.radius);
		} else {
			 myTransform.position= ColliderCenterPoint - 		(colliderTransform.up * 		CorrespondingCollider.suspensionDistance);
		}
		 // now set the wheel rotation to the rotation of the 	// collider combined with a new rotation value. This new value
		 // is the rotation around the axle, and the rotation from 	// steering input.
		 myTransform.rotation= colliderTransform.rotation * Quaternion.Euler(RotationValue, 		CorrespondingCollider.steerAngle, 0);
		 // increase the rotation value by the rotation speed (in 	// degrees per second)
		 RotationValue+= CorrespondingCollider.rpm * (360 / 60) * Time.deltaTime;
		 // define a wheelhit object, this stores all of the data 	// from the wheel collider and will allow us to determine
		// the slip of the tire.
		WheelHit correspondingGroundHit= new WheelHit();
		 CorrespondingCollider.GetGroundHit(out correspondingGroundHit);
		 // if the slip of the tire is greater than 2.0f, and the 	// slip prefab exists, create an instance of it on the ground at
		 // a zero rotation.
		 if (Mathf.Abs(correspondingGroundHit.sidewaysSlip) > slipAmountForTireSmoke) {
			if (slipPrefab) {
				 SpawnController.Instance.Spawn(slipPrefab, correspondingGroundHit.point, zeroRotation);
			}
		}
	}
}

5.3.3 Script Breakdown

The BaseWheelAlignment.cs script may be used to align the wheel meshes to the wheel colliders, as well as to provide a little extra functionality in spawning tire smoke as the wheel is slipping. The smoke is optional, of course, but it’s here in case you need it.

Each wheel should have a BaseWheelAlignment script attached to it (i.e., the wheel mesh not the WheelCollider) and then a reference to the WheelCollider component provided to this script via the Unity editor Inspector window, into the variable named CorrespondingCollider. The wheels should be independent of the WheelColliders for them to move correctly; do not parent the wheel meshes to the colliders.

In the example game Metal Vehicle Doom, the vehicle looks like this in the hierarchy window:

Car

Body

Colliders (body)

WheelColliders

Wheel_FL

Wheel_FR

Wheel_RL

Wheel_RR

WheelMeshes

Wheel_FL

Wheel_FR

Wheel_RL

Wheel_RR

The script derives from MonoBehavior so that it can tap into the engine’s Update() function:

using UnityEngine;
using System.Collections;
public class BaseWheelAlignment : MonoBehavior
{

Skipping by the variable declarations, we move down to the Start() function. myTransform is a cached reference to the wheel transform, a default rotation value is stored in zeroRotation, and there’s also a cached reference to the transform, which has the WheelCollider attached to it, to be stored in the variable colliderTransform:

	void Start ()
	{
		// cache some commonly used things...
		myTransform= transform;
		zeroRotation= Quaternion.identity;
		colliderTransform= CorrespondingCollider.transform;
	}

This Update() function works by casting a ray down from the center of the WheelCollider with the intention of placing the wheel mesh up from the point of impact. If nothing is picked up by the raycast, the wheel mesh is simply positioned at a point down from the center of the WheelCollider instead:

	void Update ()
	{
		// define a hit point for the raycast collision
		RaycastHit hit;

The WheelCollider.center property returns a value from the WheelCollider’s local coordinate space, so Transform.TransformPoint is used to convert it into a world space coordinate that we can use to start the ray cast from:

		Vector3 ColliderCenterPoint= 								colliderTransform.TransformPoint(CorrespondingCollider.				center);

For a realistic representation of the structure of suspension, the raycast length should be limited to the length of the suspension and to the radius of the wheel. Starting out from the center of the WheelCollider, the ray will travel down the collider transform’s negative up vector (i.e., down, as the WheelCollider’s transform describes it):

		if (Physics.Raycast(ColliderCenterPoint, 						-colliderTransform.up, out hit, CorrespondingCollider.				suspensionDistance + CorrespondingCollider.radius)) {

When Physics.Raycast finds a hit, the position for the wheel is the hit point, plus the wheel collider transform’s up vector multiplied by the wheel radius. When the wheel meshes pivot point is at its center, this should position the wheel in the correct place on the ground:

		myTransform.position= hit.point + (colliderTransform.up * 				CorrespondingCollider.radius);
	} else {

When Physics.Raycast finds nothing, the wheel position is at its default of the suspension length down from the WheelCollider’s center point:

		 myTransform.position= ColliderCenterPoint - 			(colliderTransform.up * CorrespondingCollider.suspensionDistance);
	}

Now that the position is set for the wheel, the rotation needs to be calculated based on the WheelCollider’s rotation in the 3D space along with some extra code to produce a rotation angle from the WheelCollider’s revolutions per minute (RPM).

The wheel mesh transform’s rotation is put together as a result of the WheelCollider’s transform rotation multiplied by a new Euler angle formed with Quaternion.Euler. The new angle takes the value in the variable RotationValue as its x-axis, the value of the steerAngle property of the WheelCollider as its y-axis and a zero as its z-axis:

	myTransform.rotation= colliderTransform.rotation * 					Quaternion.Euler(RotationValue, CorrespondingCollider.steerAngle, 0);

RotationValue is a constantly flowing number; it is never reset once the game gets going. Instead, RotationValue is incremented by the WheelCollider’s RPM value (CorrespondingCollider.rpm) as it is converted into radians and multiplied by Time.deltaTime to keep the rotation time based.

	// increase the rotation value by the rotation speed (in degrees 			// per second)
	RotationValue+= CorrespondingCollider.rpm * (360 / 60) * 				Time.deltaTime;

To analyze detailed information from the WheelCollider and its contacts and relationships with the 3D world, Unity provides a structure called WheelHit.

correspondingGroundHit is a new WheelHit formed with the specific purpose of getting to the sidewaysSlip value of the WheelCollider. If its value falls outside a certain threshold, the wheels must be sliding enough to merit some tire smoke to be instantiated:

	// define a wheelhit object, this stores all of the data from the 			// wheel collider and will allow us to determine
	// the slip of the tire.
	WheelHit correspondingGroundHit= new WheelHit();

To fill correspondingGroundHit with the information from this wheel’s WheelCollider, the WheelCollider.GetGroundHit() function is used with correspondingGroundHit:

	CorrespondingCollider.GetGroundHit(out correspondingGroundHit);
	// if the slip of the tire is greater than 2.0f, and the slip 			// prefab exists, create an instance of it on the ground at
	// a zero rotation.
	if (Mathf.Abs(correspondingGroundHit.sidewaysSlip) > 				slipAmountForTireSmoke) {
		if (slipPrefab) {

Note that the slipPrefab will be instantiated every step (every update) that the sidewaysSlip value is above the value of slipAmountForTireSmoke. If your tire smoke particle effect is complex, this could be a big slowdown point. For more complex effects, consider staggering the effect based on a timer or simplifying the particle effect itself.

The tire smoke will be instantiated at the point of contact between the ground and the wheel, but its rotation is not matched to the wheel:

			SpawnController.Instance.Spawn(slipPrefab, correspondingGroundHit.point, zeroRotation);
		}
	}
	}
}

At this stage, the framework is loaded up for players and movement controllers. In the next chapter, it’s time to add some weaponry to the framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.176.194