Chapter 13

Dish: Making the Game Tank Battle

In 1974, Kee Games, a subsidiary of Atari, Inc., released an arcade game called Tank! It was a two-player game where players had to drive around a maze and destroy the other player’s tank. Although its graphics were simplistic black and white from a fixed top-down viewpoint, the game was extremely advanced for the time. Its control system was outstanding; it used two single-axis (up and down) joysticks to steer each vehicle as if each stick were controlling an individual track on each side of the tank.

Tank! required two players to play; there was no artificial intelligence. Perhaps this was because of the complexity of AI programming and the technological limitations of the time, or perhaps it was a design decision; it had no apparent impact on its success, and the game was a huge hit. It would be quite a while before a single-player tank game would make it into the arcades.

I personally recall the privilege of playing a tank game in an arcade in the late 1970s; I played a multiplayer top-down tank game, but it was so long ago now that I can’t be sure it was the original Tank! When I see screenshots, it looks very familiar, at least. Despite brushing with it at the arcades, the strongest memories of playing Tank! come from its later incarnation as a game for the Atari 2600 game console. Variations on Tank! came on a game cartridge called Combat, released in 1977, which included a collection of 27 variations on five game types, all tied together by the main theme in the title. Most of my time on the Atari 2600 was spent battling against my brother or my cousin, each of us trying to blast each other out of the games or beat each other’s high scores. Tank! was a huge hit with us, and I’m sure that Combat would have ended up being the most worn cartridge we owned.

Sometime in the early 1980s, something arrived at my local arcade that would change my perspective on video games forever. It was the game that really fired up my interest in virtual reality: Battlezone.

Battlezone features vector 3D graphics and one of the most amazing arcade cabinets any 80s kid could encounter. Instead of having to stand and look at the screen to play, Battlezone had a viewing goggle that looked rather like binoculars. Since it was a goggle, the player had to place his/her head to it and vision was taken up entirely by the game.

Battlezone was a single-player game, with enemies controlled by artificial intelligence. Just as with the original game Tank!, its control scheme used two single-axis joysticks to simulate control of individual tracks; push both sticks forward and the tank moved forward, pull one back and one forward then the tank would turn. The viewpoint was first person, from inside the tank. When a projectile hit the player’s vehicle, a vector-based screen glass cracking effect was displayed in front of the view as though the window of the tank had been hit. At that time, Battlezone was the closest thing possible to being inside a computer game. In the early 1980s, it was a truly mind-blowing experience; 3D vector graphics were new, and the submersion level offered by the game, its control system, and its goggle-based arcade cabinet were second to none.

The design for the example game Tank Battle plonks itself down somewhere between Tank! and Battlezone. The action takes place in a maze, with a number of AI-controlled tanks driving around the game world trying to blast each other into oblivion (see Figure 13.1). The camera view is third person, behind the tank, with a fixed turret firing projectiles forward.

Figure 13.1

Image of The example game, Tank Battle.

The example game, Tank Battle.

Its overall structure differs a little from other games in this book, as seen in Figure 13.2.

Figure 13.2

Image of Tank Battle game structure.

Tank Battle game structure.

This game may be found in the example games project, with its scenes in the Scenes/Tank Battle folder named menu_TB and game_TB.

Its prefabs are located in the Prefabs/Games/Tank Battle folder.

The most important thing to note about it is that there are several elements extremely close to those from the other example vehicle game in this book, Metal Vehicle Doom. The tanks are, in fact, the exact same car prefabs except for the fact that the tanks have invisible wheels and a different body model. They work exactly the same as the cars, with a different set of parameters set to make acceleration and steering more like a tank and less like a car.

Each player has a battle controller component attached to them and a global battle manager manages the overall race state; this is the exact same format as the race controllers and global race manager scripts from Metal Vehicle Doom.

13.1 Main Game Scene

Tank Battle has two scenes: a main menu and the main game battle arena scene.

The main menu consists of a single gameObject, and the main menu script is shown in Chapter 10.

The main game scene Hierarchy structure contains the following:

  • ARENA
    • (Models that make up the environment, including a directional light to brighten it up)
  • GameController
  • Main Camera
  • MusicController
  • Player_Startpoints
    • StartPoint_1 (1–10)
  • Players
  • SoundController
  • UI
    • Game Over Graphic
    • Get Ready Graphic

Note that the game controller dynamically creates players and enemies, when the game starts. References to several prefabs and gameObjects are set in the Unity Inspector window:

  • The game controller references these prefabs and objects:
    • Explosion prefab (a particle effect for explosions)
    • StartPoints array containing a reference to the Startpoint_X gameObjects in the scene
    • Player Prefab List array containing a reference to the Top_Down_Player prefab
  • The BaseSoundController.cs script attached to the Sound Controller gameObject has an array called Game Sounds, containing references to
    • shoot1
    • explode2
    • player_explode
    • powerup
    • spawner 1
  • The Spawn Controller gameObject has an instance of WaveSpawner.cs attached to it, with the following references in its Spawn Object Prefabs array:
    • Spawn_Structure1
    • Spawn_Structure2
    • Spawn_Structure3
  • The UI gameObject has a script named UI_LBS.cs attached to it. It refers to
    • GAMEOVER_GRAPHIC gameObject
    • GETREADY_GRAPHIC gameObject

13.2 Prefabs

The prefabs that make up the game are as follows:

  • PLAYERS
    • Tank
    • AI_Tank
  • PROJECTILES
    • Tank_StandardFire
  • WEAPONS
    • TankProjectileBlaster_Player
    • TankProjectileBlaster_Enemy

13.3 Ingredients

The game uses these ingredients:

  1. Game Controller—The game controller script for this game derives from BaseGameController.cs
  2. Battle Controller—Each player has a battle controller script attached to it, which holds information relevant to its battle stats (number of frags/number of times fragged, etc.). To be clear, a frag is a hit. When the player is destroyed, it counts as a frag.
  3. Global Battle Manager—The global battle manager talks to all of the player’s battle controllers and tracks global information on the state of the battle, such as player battle positions and whether or not the battle is still in progress.
  4. Vehicles (tanks):
    1. Main player
    2. AI opponents
  5. Scene Manager
  6. User Interface—The user interface for in-game will derive from the BaseUIDataManager script from Chapter 4, which adds data management. The main menu uses the main menu system outlined in Chapter 10.
  7. Sound Controller—The sound controller script from Chapter 8.
  8. Music Controller

13.4 Game Controller

The game controller script GameController_TB.cs looks like this:

using UnityEngine;
using System.Collections;
public class GameController_TB : BaseGameController
{
	public string mainMenuSceneName = "menu_TB";
 public int numberOfBattlers = 4;
	public int gameTime= 120;
 public Transform playerParent;
 public Transform [] startPoints;
 public Camera_Third_Person cameraScript;
 [System.NonSerialized]
 public GameObject playerGO1;
 private CarController_TB thePlayerScript;
 private CarController_TB focusPlayerScript;
 private ArrayList playerList;
	private ArrayList playerTransforms;
	
 private float aSpeed;
	
 public GUIText timerText;
 public GUIText posText;
	
	private bool isAhead;
 private CarController_TB car2;
 private int focusPlayerBattlePosition;
 public GameObject count3;
 public GameObject count2;
 public GameObject count1;
 public GUIText finalPositionText;
 public GameObject [] playerPrefabList;
 [System.NonSerialized]
 public static GameController_TB Instance;
	
	public Waypoints_Controller WaypointControllerForAI;
 // scale time here
 public float gameSpeed = 1;
	private bool didInit;
		
	private TimerClass theTimer;
	
 public GameController_TB ()
 {
   Instance = this;
}
 void Start ()
 {
  	Init();
}
 void Init ()
 {
		SpawnController.Instance.Restart();
		
   // in case we need to change the timescale, it gets set here
   Time.timeScale = gameSpeed;
		
		// tell battle manager to prepare for the battle
		GlobalBattleManager.Instance.InitNewBattle ();
		
  // initialize some temporary arrays we can use to set up the	// players
   Vector3 [] playerStarts = new Vector3 [numberOfBattlers];
   Quaternion [] playerRotations = new Quaternion [numberOfBattlers];
  // we are going to use the array full of start positions that must // be set in the editor, which means we always need to make sure
  // that there are enough start positions for the number of players
   for (int i = 0; i < numberOfBattlers; i++)
   {
    // grab position and rotation values from start position	// transforms set in the inspector
   playerStarts [i] = (Vector3) startPoints [i].position;
   playerRotations [i] = (Quaternion) startPoints [i].rotation;
  }
		
  SpawnController.Instance.SetUpPlayers(playerPrefabList, playerStarts, playerRotations, playerParent, numberOfBattlers);
		
		playerTransforms=new ArrayList();
		
		 // now let's grab references to each player's controller	// script
		 playerTransforms = SpawnController.Instance.GetAllSpawnedPlayers();
		
		playerList=new ArrayList();
		
		for (int i = 0; i < numberOfBattlers; i++)
   {
			Transform tempT= (Transform)playerTransforms[i];
			 CarController_TB tempController= tempT.GetComponent<CarController_TB>();
			
			playerList.Add(tempController);
						
			 BaseAIController tempAI=tempController.GetComponent<BaseAIController>();
			
			tempController.Init ();
			
			if(i>0)
			{
				 // grab a ref to the player's gameobject for // later use
   		 playerGO1 = SpawnController.Instance.GetPlayerGO(0);
				
				// tell AI to get the player!
				tempAI.SetChaseTarget(playerGO1.transform);
				
				// set AI mode to chase
				 tempAI.SetAIState(AIStates.AIState.steer_to_target);
			}
		}
				
		 // add an audio listener to the first car so that the audio // is based from the car rather than from the main camera
		playerGO1.AddComponent<AudioListener>();
		
		 // look at the main camera and see if it has an audio	// listener attached
		 AudioListener tempListener= Camera.main.GetComponent<AudioListener>();
		
		// if we found a listener, let's destroy it
		if(tempListener!=null)
			Destroy(tempListener);
  // grab a reference to the focussed player's car controller script, // so that we can do things like access its speed variable
  thePlayerScript = (CarController_TB) playerGO1.GetComponent<CarController_TB>();
  // assign this player the id of 0 - this is important. The id	// system is how we will know who is firing bullets!
   thePlayerScript.SetID(0);
   // set player control
   thePlayerScript.SetUserInput(true);
		
   // as this is the user, we want to focus on this for UI etc.
   focusPlayerScript = thePlayerScript;
   // tell the camera script to target this new player
   cameraScript.SetTarget(playerGO1.transform);
   // lock all the players on the spot until we're ready to go
   SetPlayerLocks(true);
   // start the game in 3 seconds from now
   Invoke("StartGame", 4);
		
		 // initialize a timer, but we won't start it right away. It // gets started in the FinishedCount() function after the // count-in
		theTimer = ScriptableObject.CreateInstance<TimerClass>();
		
   // update positions throughout the battle, but we don't need
  // to do this every frame, so just do it every half a second instead
   InvokeRepeating("UpdatePositions", 0f, 0.5f);
		
   // hide our count-in numbers
   HideCount();
   // schedule count-in messages
   Invoke("ShowCount3", 1);
   Invoke("ShowCount2", 2);
   Invoke("ShowCount1", 3);
   Invoke("FinishedCount", 4);
   // hide final position text
   finalPositionText.gameObject.SetActive(false);
   doneFinalMessage = false;
		
		didInit=true;
}
	
 void StartGame ()
 {
		// the SetPlayerLocks function tells all players to unlock
   SetPlayerLocks(false);
		
		// tell battle manager to start the battle!
		GlobalBattleManager.Instance.StartBattle();
}
	
   void UpdatePositions()
   {		
		// update the display
		UpdateBattlePositionText();
  }
 void UpdateBattlePositionText ()
 {
		 // get a string back from the timer to display on screen
   timerText.text = theTimer.GetFormattedTime();
		
		 // get the current player position scoreboard from the	// battle manager and show it via posText.text
		 posText.text = GlobalBattleManager.Instance.GetPositionListString();
		
		 // check the timer to see how much time we've been playing. // If it's more than gameTime, the game is over
		if(theTimer.GetTime() > gameTime)
		{
			// end the game
			BattleComplete();
		}
}
	
 void SetPlayerLocks (bool aState)
 {
   // tell all of the players to set their locks
   for (int i = 0; i < numberOfBattlers; i++)
   {
			 thePlayerScript = (CarController_TB) playerList [i];
   thePlayerScript.SetLock(aState);
  }
}
 private bool doneFinalMessage;
 public void BattleComplete ()
 {
		// tell battle manager we're done
		GlobalBattleManager.Instance.StopBattle();
		
		// stop the timer!
		theTimer.StopTimer();
		
		 // now display a message to tell the user the result of the // battle
   if (!doneFinalMessage)
   {
			 // get the final position for our local player	// (which is made first, so always has the id 1)
			 int finalPosition= GlobalBattleManager.Instance.GetPosition(1);
			
			if (finalPosition == 1)
				finalPositionText.text = "FINISHED 1st";
			if (finalPosition == 2)
				finalPositionText.text = "FINISHED 2nd";
			if (finalPosition == 3)
				finalPositionText.text = "FINISHED 3rd";
			if (finalPosition >= 4)
				finalPositionText.text = "GAME OVER";
			doneFinalMessage = true;
			finalPositionText.gameObject.SetActive(true);
			// drop out of the battle scene completely in 10 					// seconds...
			Invoke("FinishGame", 10);
  }
}
 void FinishGame ()
 {
   Application.LoadLevel(mainMenuSceneName);
}
 void ShowCount1 ()
 {
   count1.SetActive(true);
   count2.SetActive(false);
   count3.SetActive(false);
}
 void ShowCount2 ()
 {
   count1.SetActive(false);
   count2.SetActive(true);
   count3.SetActive(false);
}
 void ShowCount3 ()
 {
   count1.SetActive(false);
   count2.SetActive(false);
   count3.SetActive(true);
}
	
	void FinishedCount ()
	{
		HideCount ();
		
		// let the timer begin!
		theTimer.StartTimer();
	}
 void HideCount ()
 {
   count1.SetActive(false);
   count2.SetActive(false);
   count3.SetActive(false);
}
	
	public void PlayerHit(Transform whichPlayer)
	{
		// tell our sound controller to play an explosion sound
		 BaseSoundController.Instance.PlaySoundByIndex(1, whichPlayer.position);
		
		// call the explosion function!
		//Explode(whichPlayer.position);
	}
	
	public void PlayerBigHit(Transform whichPlayer)
	{
		// tell our sound controller to play an explosion sound
		 BaseSoundController.Instance.PlaySoundByIndex(2, whichPlayer.position);
		
		// call the explosion function!
		Explode(whichPlayer.position);
	}
	
	public void Explode (Vector3 aPosition)
	{		
		 // instantiate an explosion at the position passed into this // function
		 Instantiate(explosionPrefab,aPosition, Quaternion.identity);
	}	
}

13.4.1 Script Breakdown

GameController_TB.cs is very similar to the game controller used in the example game Metal Vehicle Doom. It derives from the BaseGameController class and uses a lot of the same logic, so rather than repeat everything here I will highlight the main differences and go through those, instead.

As per the other game control scripts in this book, it derives from BaseGameController:

using UnityEngine;
using System.Collections;
public class GameController_TB : BaseGameController
{

There are a few differences in the Init() function worth mentioning:

 void Init ()
 {
		SpawnController.Instance.Restart();
		
   // in case we need to change the timescale, it gets set here
   Time.timeScale = gameSpeed;

Unlike Metal Vehicle Doom, there is no lap counting in this script—the player’s position, in a leaderboard-style scoring system, is tracked by the battle controller and battle manager scripts. Here in the Init() function is where GlobalBattleManager gets its call to initialize:

		// tell battle manager to prepare for the battle
		GlobalBattleManager.Instance.InitNewBattle ();

Just like Metal Vehicle Doom, all of the players’ start positions and rotations are copied into new arrays ready to be fed to the SpawnController to set up:

  // initialize some temporary arrays we can use to set up the players
   Vector3 [] playerStarts = new Vector3 [numberOfBattlers];
   Quaternion [] playerRotations = new Quaternion [numberOfBattlers];
  // we are going to use the array full of start positions that must // be set in the editor, which means we always need to make sure
  // that there are enough start positions for the number of players
   for (int i = 0; i < numberOfBattlers; i++)
   {
    // grab position and rotation values from start position	// transforms set in the inspector
   playerStarts [i] = (Vector3) startPoints [i].position;
   playerRotations [i] = (Quaternion) startPoints [i].rotation;
  }

One important thing to note is that the first player listed in the playerPrefabList (set in the Unity editor Inspector window) should always be the user’s player prefab and not an AI—the script is based on the assumption that the first player will always be the one to focus on for UI, scoring, and camera following, etc.:

  SpawnController.Instance.SetUpPlayers(playerPrefabList, playerStarts, playerRotations, playerParent, numberOfBattlers);
		
		playerTransforms=new ArrayList();
		
		 // now let's grab references to each player's controller script
		 playerTransforms = SpawnController.Instance.GetAllSpawnedPlayers();
		
		playerList=new ArrayList();
		
		for (int i = 0; i < numberOfBattlers; i++)
   {
			Transform tempT= (Transform)playerTransforms[i];
			 CarController_TB tempController= 			tempT.GetComponent<CarController_TB>();
			
			playerList.Add(tempController);
						
			 BaseAIController tempAI=tempController.GetComponent<BaseAIController>();
			
			tempController.Init ();

The AI players in Tank Battle are going to need to know which gameObject the player is, to be able to chase and track it. We start the game with all of the tanks defaulting to look for the user. This behavior will, in fact, be overridden by the AI if it gets close enough to another AI opponent, as it will attack a close target regardless of whether or not it is the user.

Here the condition is to check to see whether the loop (i) is at a value greater than 0 so that we know this is not a user (remember that users are always the first in the playerPrefab array and will always be 0 index):

			if(i>0)
			{

The SpawnController.Instance.GetPlayerGO() function gives back the gameObject for a specific player based on its ID passed in as a parameter:

				 // grab a ref to the player's gameobject for // later use
   		playerGO1 = SpawnController.Instance.GetPlayerGO(0);

Next, the user’s transform is passed on to the AI player with a call to SetChaseTarget:

				// tell AI to get the player!
			tempAI.SetChaseTarget(playerGO1.transform);

In Metal Vehicle Doom, the default state for the AI was for it to follow waypoints in AIState.steer_to_waypoint. In this game, rather than path following, the enemy players need to go on the attack, and their first state is AIState.steer_to_target, which means that the AI provides steering inputs that should point its vehicle at its chase target:

				// set AI mode to chase
			 tempAI.SetAIState(AIStates.AIState.steer_to_target);
			}
		}

The AudioListener component is removed from the camera and added to the user’s tank, instead:

		 // add an audio listener to the first car so that the audio // is based from the car rather than from the main camera
		playerGO1.AddComponent<AudioListener>();
		
		 // look at the main camera and see if it has an audio	// listener attached
		 AudioListener tempListener= 					Camera.main.GetComponent<AudioListener>();
		
		// if we found a listener, let's destroy it
		if(tempListener!=null)
			Destroy(tempListener);
  // grab a reference to the focussed player's car controller script, // so that we can do things like access its speed variable
  thePlayerScript = (CarController_TB)			playerGO1.GetComponent<CarController_TB>();
  // assign this player the id of 0 - this is important. The id	// system is how we will know who is firing bullets!
   thePlayerScript.SetID(0);
   // set player control
   thePlayerScript.SetUserInput(true);
   // as this is the user, we want to focus on this for UI etc.
   focusPlayerScript = thePlayerScript;
   // tell the camera script to target this new player
   cameraScript.SetTarget(playerGO1.transform);
   // lock all the players on the spot until we're ready to go
   SetPlayerLocks(true);
   // start the game in 3 seconds from now
   Invoke("StartGame", 4);

The game is timed. The winner is the player who has the highest frag count when the timer finishes. A simple TimerClass script is used to manage the timing, which gets instanced here and a reference held by the variable theTimer:

		 // initialize a timer, but we won't start it right away. It // gets started in the FinishedCount() function after the 	// count-in
		theTimer = ScriptableObject.CreateInstance<TimerClass>();

Although this is not a battle, the game follows a similar format to Metal Vehicle Doom in terms of its position list: a leaderboard-like scoring system. To keep this up to date, a repeating Invoke calls UpdatePositions() every half second:

   // update positions throughout the battle, but we don't need
  // to do this every frame, so just do it every half a second instead
   InvokeRepeating("UpdatePositions", 0f, 0.5f);
   // hide our count-in numbers
   HideCount();
   // schedule count-in messages
   Invoke("ShowCount3", 1);
   Invoke("ShowCount2", 2);
   Invoke("ShowCount1", 3);

In the Metal Vehicle Doom Init() function, the final Invoke call was to HideCount(), but in this function, there is something else that needs to be done before starting the game that happens in the FinishedCount() function, so the call is made to FinishedCount() instead of HideCount():

   Invoke("FinishedCount", 4);
   // hide final position text
   finalPositionText.gameObject.SetActive(false);
   doneFinalMessage = false;
		didInit=true;
}

Further down in the script, UpdateBattlePositionText() updates GUIText components with the current time left on the timer and a list of players and their positions on the score board:

 void UpdateBattlePositionText ()
 {

The TimerClass script has a function called GetFormattedTime(), which returns a string containing the time formatted as mm:ss:ms. The timer GUIText component’s text is set to the function’s return value here:

		 // get a string back from the timer to display on screen
   timerText.text = theTimer.GetFormattedTime();

There is a function in the global battle manager called GetPositionListString() that will not only return a sorted score list but also return it as a formatted string with line breaks in it, ready to put right into a GUIText component:

		 // get the current player position scoreboard from the // battle manager and show it via posText.text
		 posText.text = GlobalBattleManager.Instance.GetPositionListString();

In all honesty, this may not be the best place for this to happen (since it’s supposed to be a UI-specific function), but it does serve to keep all of the timer logic in one function. Here the timer is checked to see whether it is time to end the game.

TimerClass.GetTime() returns an integer, which is compared to the variable gameTime here. gameTime is an integer representing the game length in seconds. When the timer hits gameTime, the BattleComplete() function shuts down the game:

		 // check the timer to see how much time we've been playing. // If it's more than gameTime, the game is over
		if(theTimer.GetTime() > gameTime)
		{
			// end the game
			BattleComplete();
		}
}

The BattleComplete() function of the Tank Battle’s game controller is the equivalent to Metal Vehicle Doom’s RaceComplete(). It tells the GlobalBattleManager to stop the battle, stops the timer, and composes a message to display via the GUIText component referenced in the variable finalPositionText:

 public void BattleComplete ()
 {
		// tell battle manager we're done
		GlobalBattleManager.Instance.StopBattle();
		
		// stop the timer!
		theTimer.StopTimer();
		
		 // now display a message to tell the user the result of the // battle
   if (!doneFinalMessage)
   {
			 // get the final position for our local player	// (which is made first, so always has the id 1)
			 int finalPosition= GlobalBattleManager.Instance.GetPosition(1);
			
			if (finalPosition == 1)
				finalPositionText.text = "FINISHED 1st";
			if (finalPosition == 2)
				finalPositionText.text = "FINISHED 2nd";
			if (finalPosition == 3)
				finalPositionText.text = "FINISHED 3rd";
			if (finalPosition >= 4)
				finalPositionText.text = "GAME OVER";
			doneFinalMessage = true;
			
			finalPositionText.gameObject.SetActive(true);

Now that the final position message is on the screen, a call to FinishGame will load the main menu scene:

			// drop out of the scene completely in 10 seconds...
			Invoke("FinishGame", 10);
  }
}
 void FinishGame ()
 {
   Application.LoadLevel(mainMenuSceneName);
}

The timer is started at the end of the countdown, with a call to StartTimer():

	void FinishedCount ()
	{
		HideCount ();
		// let the timer begin!
		theTimer.StartTimer();
	}

13.5 Battle Controller

The battle controller is Tank Battle’s equivalent to Metal Vehicle Doom’s race controller. It is applied to all players to be tracked as members of the battle, and the global battle manager will talk to these script components to keep an overall picture of the battle (managing the score board, etc.). The full script looks like this:

using UnityEngine;
using System.Collections;
public class BattleController : MonoBehavior
{
 private bool isFinished;
	private Vector3 myPosition;
	private Transform myTransform;
	public int howmany_frags;
	public int howMany_fraggedOthers;
	public bool battleRunning;
	private bool doneInit;
	 // we default myID to -1 so that we will know if the script hasn't // finished initializing when another script tries to GetID
	private int myID =-1;
	public BattleController ()
	{
		myID= GlobalBattleManager.Instance.GetUniqueID(this);
		Debug.Log ("ID assigned is "+myID);
	}
	public void Init()
	{
		myTransform= transform;
		doneInit= true;
	}
	
	public int GetID ()
	{
		return myID;	
	}
	
	public void Fragged ()
	{
		howmany_frags++;
	}
	
	public void FraggedOther ()
	{
		howMany_fraggedOthers++;
	}
	
	public void GameFinished ()
	{
		isFinished=true;
		battleRunning=false;
		
		// find out which position we finished in
		 int finalBattlePosition= 			GlobalBattleManager.Instance.GetPosition(myID);
		
		// tell our car controller about the battle ending
		 gameObject.SendMessageUpwards("PlayerFinishedBattle", finalBattlePosition, SendMessageOptions.DontRequireReceiver);
	}
	public void BattleStart ()
	{
		isFinished=false;
		battleRunning=true;
	}
	public bool GetIsFinished ()
	{
		return isFinished;
	}
	
	public void UpdateBattleState(bool aState)
	{
		battleRunning= aState;
	}
		
	public void OnTriggerEnter(Collider other)
	{
	}	
}

13.5.1 Script Breakdown

BattleController is very similar in format to the RaceController class from Metal Vehicle Doom. This script derives from MonoBehavior so that it can tap into the system functions called automatically by Unity:

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

When the player is hit by another player’s projectile, it is deemed as a frag, and the function Fragged() is called to register it:

	public void Fragged ()
	{
		howmany_frags++;
	}

When the player’s projectile hits another player, it is a frag, and the function FraggedOther() is called on the player with the projectile’s battle controller:

	public void FraggedOther ()
	{
		howMany_fraggedOthers++;
	}

Just as the RaceController from Metal Vehicle Doom had the RaceFinished() function, the battle controller has its equivalent that will be called by the game controller when the timer reaches its limit:

	public void GameFinished ()
	{
		isFinished=true;
		battleRunning=false;
		
		// find out which position we finished in
		 int finalBattlePosition= GlobalBattleManager.Instance.GetPosition(myID);
		
		// tell our car controller about the battle ending
		 gameObject.SendMessageUpwards("PlayerFinishedBattle", finalBattlePosition, SendMessageOptions.DontRequireReceiver);
	}

The Boolean variable battleRunning is used to tell whether or not the battle is still active. When BattleStart() is called, it is set to true and isFinished set to false:

	public void BattleStart ()
	{
		isFinished=false;
		battleRunning=true;
	}

UpdateBattleState() is this script’s equivalent to UpdateRaceState() in the Metal Vehicle Doom game. It sets battleRunning to whatever Boolean value is passed in as a parameter:

	public void UpdateBattleState(bool aState)
	{
		battleRunning= aState;
	}

13.6 Global Battle Manager

The battle controller script manages data for each player and registers itself with the global battle manager as it instantiates. The global battle manager takes care of managing the bigger picture, dealing with comparing players to calculate score board positions and keeping tabs on the global race state.

The GlobalBattleManager.cs script in full:

using UnityEngine;
using System.Collections;
public class GlobalBattleManager : ScriptableObject
{
	private int currentID;
	
	private Hashtable battleControllers;
	private Hashtable battlePositions;
	private Hashtable battleFinished;
	private Hashtable sortedPositions;
	
	private int numberOfBattlers;
	
	private int myPos;
	private bool isAhead;
	private BattleController tempRC;
	private BattleController focusPlayerScript;
	private bool battleRunning;
	
	private static GlobalBattleManager instance;
	public static GlobalBattleManager Instance
	{
		get
		{
			if (instance == null)
			{
				 instance = 			ScriptableObject.CreateInstance <GlobalBattleManager>();
			}
			return instance;
		}
	}
	public void OnApplicationQuit ()
	{
		instance = null;
	}
	public void InitNewBattle()
	{
		// initialize our hashtables ready for putting objects into
		battlePositions= new Hashtable();
		battleFinished= new Hashtable();
		battleControllers= new Hashtable();
		sortedPositions= new Hashtable();
		
		currentID=0;
		numberOfBattlers=0;
	}
	
	public int GetUniqueID(BattleController theBattleController)
	{
		 // whenever an id is requested, we increment the ID counter. // this value never gets reset, so it should always
		 // return a unique id (NOTE: these are unique to each battle)
		currentID++;
		
		// now set up some default data for this new player
		if(battlePositions==null)
			InitNewBattle();
		
		// this player will be in last position
		battlePositions.Add(currentID, battlePositions.Count + 1);
		
		 // store a reference to the battle controller, to talk to later
		battleControllers.Add (currentID, theBattleController);
		
		// default finished state
		battleFinished[currentID]=false;
		
		 // increment our battler counter so that we don't have to do // any counting or lookup whenever we need it
		numberOfBattlers++;
		
		// pass this id back out for the battle controller to use
		return currentID;
	}
	
	public void SetBattlePosition(int anID, int aPos)
	{
		if(battlePositions.ContainsKey(anID))
		{
			 // we already have an entry in the battle positions // table, so let's modify it
			battlePositions[anID]=aPos;
		} else {
			 // we have no data for this player yet, so let's add // it to the battlePositions hashtable
			battlePositions.Add(anID,aPos);	
		}
	}
	
	public int GetBattlePosition(int anID)
	{
		 // just returns the entry for this ID in the battlePositions // hashtable
		return (int)battlePositions[anID];
	}
	
	private string posList;
	private int whichPos;
	
	public string GetPositionListString()
	{
		 // this function builds a string containing a list of	// players in order of their scoring positions
		 // we step through each battler and check its		// positions and build a hash table that can be		// accessed by using the position as an index
 		for (int b = 1; b <= numberOfBattlers; b++)
   {
			whichPos= GetPosition(b);
			
			tempRC = (BattleController) battleControllers [b];
			
			sortedPositions[whichPos]= tempRC.GetID();
		}
		
		if(sortedPositions.Count<numberOfBattlers)
			return "";
		
		posList="";
		
		 // now we have a populated sortedPositions hash table, let's // iterate through it and build the string
		for (int b = 1; b <= numberOfBattlers; b++)
   {
			whichPos= (int)sortedPositions[b];
			 posList=posList+b.ToString()+". PLAYER "+whichPos+"
";
		}
		
		return posList;
	}
	
	public int GetPosition (int ofWhichID)
 {
		 // first, let's make sure that we are ready to go (the	// hashtables may not have been set up yet, so it's
		// best to be safe and check this first)
		if(battleControllers==null)
		{
			 Debug.Log ("GetPosition battleControllers is NULL!");
			return -1;
		}
		
		if(battleControllers.ContainsKey(ofWhichID)==false)
		{
			 Debug.Log ("GetPosition says no battle controller found for id "+ofWhichID);
			return -1;
		}
		
		 // first, we need to find the player that we're trying to	// calculate the position of
		 focusPlayerScript= (BattleController) battleControllers[ofWhichID];
		
		 // start with the assumption that the player is in last	// place and work up
   myPos = numberOfBattlers;
		for (int b = 1; b <= numberOfBattlers; b++)
   {
   // assume that we are behind this player
   isAhead = false;
		
			 // grab a temporary reference to the 'other' player // we want to check against
   tempRC = (BattleController) battleControllers [b];
			
			 // if car 2 happens to be null (deleted for example) // here's a little safety to skip this iteration in // the loop
			if(tempRC==null)
				continue;
				
   if (focusPlayerScript.GetID() != tempRC.GetID())
   {// <-- make sure we're not trying to compare same objects!
				
				 // check to see if this player has fragged	// more
				 // if(focusPlayerScript.howMany_fraggedOthers	// > tempRC.howMany_fraggedOthers)
					// isAhead=true;
				 // we check here to see if the frag count is // the same and if so we use the id to sort // them instead
				 if(focusPlayerScript.howMany_fraggedOthers == tempRC.howMany_fraggedOthers && focusPlayerScript.GetID() > tempRC.GetID())
					isAhead=true;
				
				 // alternative version just for fun... counts // fragged times too
				 // check to see if this player has fragged more
				 if((focusPlayerScript.howMany_fraggedOthers - focusPlayerScript.howmany_frags) > (tempRC.howMany_fraggedOthers - focusPlayerScript.howmany_frags))
					isAhead=true;
				
				if (isAhead)
    {
					myPos--;
   }
			}
		}
		
		return myPos;
}
	
	public void StartBattle()
	{
		battleRunning=true;
		UpdateBattleStates();
	}
	
	public void StopBattle()
	{
		 // we don't want to keep calling everyone to tell them about // the battle being over if we've already done it once, so // check first battleRunning is true
		if(battleRunning==true)
		{
			// set a flag to stop repeat calls etc.
			battleRunning=false;
		
			 // tell everyone about the update to the state of	// battleRunning
			UpdateBattleStates();
			
			 // tell all players that we're done, by sending a	// message to each gameObject with a battle		// controller attached to it
			 // to call the PlayerFinishedBattle() function in	// the car control script
			for (int b = 1; b <= numberOfBattlers; b++)
	   {
				 tempRC = (BattleController) battleControllers [b];
				 tempRC.gameObject.SendMessage("PlayerFinishedBattle",SendMessageOptions.DontRequireReceiver);
			}
		}
	}
	
	public void RegisterFrag(int whichID)
	{
		 focusPlayerScript= (BattleController) battleControllers[whichID];
		focusPlayerScript.FraggedOther();
	}
	
	void UpdateBattleStates()
	{
		for (int b = 1; b <= numberOfBattlers; b++)
		{
			tempRC = (BattleController) battleControllers [b];
			tempRC.UpdateBattleState(battleRunning);
		}
	}
}

13.6.1 Script Breakdown

This global battle manager is, again, very similar to the global race manager from the Metal Vehicle Doom game we looked at in Chapter 12. For that reason, I will highlight and run through the main differences rather than repeat everything. If the reasoning or workings of anything in this script are unclear, take a look back at Chapter 12 to the global race manager.

GlobalBattleManager derives from ScriptableObject:

using UnityEngine;
using System.Collections;
public class GlobalBattleManager : ScriptableObject
{

The global battle manager starts with the code to make sure that only one instance of it exists:

	public static GlobalBattleManager Instance
	{
		get
		{
			if (instance == null)
			{
				 instance = ScriptableObject.CreateInstance <GlobalBattleManager>();
			}
			return instance;
		}
	}

InitNewBattle() initializes the Hashtables (and a couple of other variables) ready for the game. These are almost the same Hashtables as those found in the global race manager from Chapter 12, except that the word race has been replaced with battle. They do serve the same function: battlePositions refers to positions on the score board, the battleFinished table holds each player’s game state, battleControllers is used to store the player’s battle controller components and a new Hashtable, sortedPositions, is used to provide a formatted and sorted score board for the UI:

	public void InitNewBattle()
	{
		// initialize our hashtables ready for putting objects into
		battlePositions= new Hashtable();
		battleFinished= new Hashtable();
		battleControllers= new Hashtable();
		sortedPositions= new Hashtable();
		
		currentID=0;
		numberOfBattlers=0;
	}

Skipping down past a few functions in the original script now, we can see that the function GetPositionListString() provides the formatted, sorted list of players in order of their race positions, for the game controller to display as a text object.

GetPositionListString() will return a string:

	public string GetPositionListString()
	{

The sortedPositions Hashtable is constructed here by looping through all of the players. Each player’s score board position is used as a key in sortedPositions, adding the value of the player’s battleController:

		 // this function builds a string containing a list of	// players in order of their scoring positions
		 // now we step through each battler and check their		// positions to determine whether or not
 		for (int b = 1; b <= numberOfBattlers; b++)
   {
			whichPos= GetPosition(b);
			
			tempRC = (BattleController) battleControllers [b];
			
			sortedPositions[whichPos]= tempRC.GetID();
		}

In case something has gone wrong with building the sortedPositions Hashtable, a quick count check ensures that some kind of string will get returned regardless. If there has been a problem (which, in theory, should not happen), an empty string is returned:

		if(sortedPositions.Count<numberOfBattlers)
			return "";
		
		posList="";

We can now access the sortedPositions Hashtable by its score board position key. To build out the sorted string, the loop b goes through to the value of numberOfBattlers, each time adding the value of sortedPositions[b] (the player number as per the original playerPrefabs array on game controller) to posList, as a string with a little extra formatting:

		 // now we have a populated sortedPositions hash table, let's // iterate through it and build the string
		for (int b = 1; b <= numberOfBattlers; b++)
   {
			whichPos= (int)sortedPositions[b];
			 posList=posList+b.ToString()+". PLAYER "+whichPos+"
";
		}
		
		return posList;
	}

GetPosition() will look at all of the players and compare scores to find the score board position of the player identified by the ID passed in as a parameter:

	public int GetPosition (int ofWhichID)
 {

The function starts out with a few safety checks to make sure everything is set up correctly and that the ID passed in is a valid one:

		 // first, let's make sure that we are ready to go (the	// hashtables may not have been set up yet, so it's
		// best to be safe and check this first)
		if(battleControllers==null)
		{
			 Debug.Log ("GetPosition battleControllers is NULL!");
			return -1;
		}
		
		if(battleControllers.ContainsKey(ofWhichID)==false)
		{
			 Debug.Log ("GetPosition says no battle controller found for id "+ofWhichID);
			return -1;
		}

Next, the code grabs a reference to the BattleController component attached to the player we want to find out about and keeps it in focusPlayerScript:

		 // first, we need to find the player that we're trying to	// calculate the position of
		 focusPlayerScript= (BattleController) battleControllers[ofWhichID];

This code is very similar to the race position checking from the global race manager, except there is no waypoint, lap, or distance checking; instead, it is all based on frag numbers. The script begins with the assumption that the player is in last place, as the logic works backward in terms of calculating the score rank:

		myPos = numberOfBattlers;
		for (int b = 1; b <= numberOfBattlers; b++)
   {
   // assume that we are behind this player
   isAhead = false;

Step 1 is to get a reference to the current battleController instance we need to talk to, which is stored in the variable tempRC:

			 // grab a temporary reference to the 'other' player // we want to check against
   tempRC = (BattleController) battleControllers [b];

A quick null check is made to make sure the game does not crash if a player were to get prematurely deleted during play or as the game ends:

			 // if player 2 happens to be null (deleted for 	// example) here's a little safety to skip this 	// iteration in the loop
			if(tempRC==null)
				continue;

Comparing the player we want to look at with its own battleController would be pointless, so the script compares the IDs of each battleController before any sort of score checking takes place:

   if (focusPlayerScript.GetID() != tempRC.GetID())
   {

focusPlayerScript.howMany_fraggedOthers contains a count of how many other players this player has fragged.

There is code in this script for three different systems of a player moving up in rank. The first one only takes into account the number of frags. Whichever player has fragged the most others gets higher in the ranking. This is commented out in the example game, as it uses the second ranking system further on in the script:

		 // if(focusPlayerScript.howMany_fraggedOthers > 		// tempRC.howMany_fraggedOthers)
	// isAhead=true;

When both players have fragged the same amount, ranking will be based on ID. Higher ID will come first on the score board:

				 // we check here to see if the frag count is // the same and if so we use the id to sort // them instead
				 if(focusPlayerScript.howMany_fraggedOthers == tempRC.howMany_fraggedOthers && focusPlayerScript.GetID() > tempRC.GetID())
					isAhead=true;

The third and final condition both takes the number of frags and subtracts it from the number of times this player has been fragged:

	 if((focusPlayerScript.howMany_fraggedOthers - focusPlayerScript.howmany_frags) > (tempRC.howMany_fraggedOthers - focusPlayerScript.howmany_frags))
		isAhead=true;

If one of the conditions has set isAhead to true, the myPos variable is decremented, which moves the player up one in the rankings. The loop then either completes or goes through this process again:

				if (isAhead)
    {
					myPos--;
   }
			}
		}
		
		return myPos;
}

The game controller will call StopBattle() when the game is over. StopBattle() sets battleRunning to false, then calls out UpdateBattleStates() to tell all of the battle controllers about the change:

	public void StopBattle()
	{
		 // we don't want to keep calling everyone to tell them about // the battle being over if we've already done it once, so // check first battleRunning is true
		if(battleRunning==true)
		{
			// set a flag to stop repeat calls etc.
			battleRunning=false;
		
			 // tell everyone about the update to the state of	// battleRunning
			UpdateBattleStates();

Next, PlayerFinishedBattle() is called for all of the battle controllers, telling them to shut down their battle:

			 // tell all players that we're done, by sending a	// message to each gameObject with a battle controller // attached to it to call the PlayerFinishedBattle()
			 // function in the car control script
			 for (int b = 1; b <= numberOfBattlers; b++)
	   {
				 tempRC = (BattleController) battleControllers [b];
				 tempRC.gameObject.SendMessage("PlayerFinishedBattle",SendMessageOptions.DontRequireReceiver);
			}
		}
	}

When the player script (CarController_TB.cs) detects a collision with a projectile, it calls GlobalBattleManager.Instance.RegisterFrag() to tell this script about the hit. When this script gets the call, it uses the BattleController for the ID passed in, to tell the other player (who sent the projectile) about the hit:

	public void RegisterFrag(int whichID)
	{
		 focusPlayerScript= (BattleController) battleControllers[whichID];
		focusPlayerScript.FraggedOther();
	}

13.7 Players

The CarController_TB.cs script is attached to all tanks. It derives from BaseVehicle and bears more than a passing resemblance to the controller script from the example game Metal Vehicle Doom in Chapter 12. The full player script looks like this:

using UnityEngine;
using System.Collections;
public class CarController_TB : BaseVehicle
{
	 public BaseWeaponController weaponControl; // note that we don't // use the standard slot // system here!
	
	public bool canFire;
	public bool isRespawning;
	public bool isAIControlled;
	public bool isInvulnerable;
	
	public BaseAIController AIController;
	
	public int startHealthAmount= 50;
	
	public BasePlayerManager myPlayerManager;
	public BaseUserManager myDataManager;
	
	public float turnTorqueHelper= 90;
	public float TurnTorqueHelperMaxSpeed= 30;
	
	public float catchUpAccelMax= 8000;
	public float originalAccelMax= 5000;
	public float resetTime= 4;
	
	public LayerMask respawnLayerMask;
	
	private BaseArmedEnemy gunControl;
	private BattleController battleControl;
	
	private int racerID;
	private int myBattlePosition;
	private bool isGameRunning;
	private float resetTimer;
	private bool canRespawn;
	private bool canPlayFireSound;
	public float timeBetweenFireSounds= 0.25f;
	private bool isFinished;
	
	private Vector3 respawnPoint;
	private Vector3 respawnRotation;
	
	public Material treadMaterial;
	public GameObject shieldMesh;
	public float respawnInvunerabilityTime= 5;
	
	public override void Start ()
	{
		 // we are overriding the Start function of BaseVehicle	// because we do not want to initialize from here!
		 // Game controller will call Init when it is ready
		
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
	}
	public override void Init ()
	{
		Debug.Log ("CarController_TB Init called.");
		
		// cache the usual suspects
		myBody= rigidbody;
		myGO= gameObject;
		myTransform= transform;
		
		// allow respawning from the start
		canRespawn=true;
		
		 // save our accelMax value for later use, in case we need to // change it to do AI catch up
		originalAccelMax= accelMax;
		
		// add default keyboard input if we don't already have one
		default_input= myGO.GetComponent<Keyboard_Input>();
		
		if(default_input==null)
			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,-6.5f,0);
		
		// see if we can find an engine sound source, if we need to
		if(engineSoundSource==null)
		{
			engineSoundSource= myGO.GetComponent<AudioSource>();
		}
		
		AddBattleController();
		
		// get a ref to the weapon controller
		weaponControl= myGO.GetComponent<BaseWeaponController>();
		
		 // if a player manager is not set in the editor, let's try // to find one
		if(myPlayerManager==null)
			 myPlayerManager= 					myGO.GetComponent<BasePlayerManager>();
		
		// cache ref to data manager
		myDataManager= myPlayerManager.DataManager;
		
		// set default data
		myDataManager.SetName("Player");
		myDataManager.SetHealth(startHealthAmount);
				
		if(isAIControlled)
		{
			// set our name to an AI player
			myDataManager.SetName("AIPlayer");
			
			// set up AI
			InitAI();
		}
		
		isGameRunning= true;
		
		// store respawn point
		respawnPoint= myTransform.position;
		respawnRotation= myTransform.eulerAngles;
		MakeVulnerable();
		
		// grab volume from sound controller for our engine sound
		audio.volume= BaseSoundController.Instance.volume;
	}	
	
	void InitAI()
	{
		// cache a reference to the AI controller
		AIController= myGO.GetComponent<BaseAIController>();
		
		 // check to see if we found an AI controller component, if // not we add one here
		if(AIController==null)
			AIController= myGO.AddComponent<BaseAIController>();
		
		// initialize the AI controller
		AIController.Init();
		
		// tell the AI controller to go into waypoint steering mode
		AIController.SetAIState(AIStates.AIState.steer_to_waypoint);
		
		// disable our default input method
		default_input.enabled=false;
		
		// add an AI weapon controller
		gunControl= myGO.GetComponent<BaseArmedEnemy>();
		
		 // if we don't already have a gun controller, let's add one to	// stop things breaking but warn about it so that it may be fixed
		if(gunControl==null)
		{
			gunControl= myGO.AddComponent<BaseArmedEnemy>();
			 Debug.LogWarning("WARNING! Trying to initialize car without a BaseArmedEnemy component attached. Player cannot fire!");
		}
		
		// tell gun controller to do 'look and destroy'
		 gunControl.currentState= 				AIAttackStates.AIAttackState.look_and_destroy;
	}
		
	void AddBattleController()
	{			
		if(myGO==null)
			myGO=gameObject;
		
		// add a battle controller script to our object
		battleControl= myGO.AddComponent<BattleController>();
		
		 // grab an ID from the battle control script, so we don't	// have to look it up whenever we communicate with
		// the global battle controller
		racerID= battleControl.GetID();
		
		 // we are going to use the same id for this player and	// its weapons
		id=racerID;
		
		 // tell weapon controller this id so we can add it to	// projectiles. This way, when a projectile hits something
		// we can look up its id and trace it back to this player
		weaponControl.SetOwner(id);
		
		 // set up a repeating invoke to update our position, rather // than calling it every single tick or update
		InvokeRepeating("UpdateBattlePosition",0, 0.5f);
	}
	
	public void UpdateBattlePosition()
	{
		 // grab our score board position from the global battle	// manager
		 myBattlePosition= 				GlobalBattleManager.Instance.GetPosition(racerID);
	}
	public override void UpdatePhysics ()
	{		
		if(canControl)
 		base.UpdatePhysics();
		
		if(isFinished)
			myBody.velocity *= 0.99f;
			
		 // if we are moving slow, apply some extra force to turn the // car quickly so we can do donuts (!) - note that since there // is no 'ground detect' it will apply it in the air, too (bad!)
		if(mySpeed < TurnTorqueHelperMaxSpeed)
		{
			 myBody.AddTorque(new Vector3(0, steer * myBody.mass * turnTorqueHelper * motor, 0));
		}
	}
	
	private float timedif;
	
	public override void LateUpdate()
	{		
		// get the state of the battle from the battle controller
		isGameRunning= battleControl.battleRunning;
		
		if(isGameRunning)
		{
			 // check to see if we've crashed and are upside	// down/wrong(!)
			Check_If_Car_Is_Flipped();
			
			 // make sure that gunControl has been told it can fire
			if(isAIControlled)
				gunControl.canControl=true;
			
			 // we check for input in LateUpdate because Unity	// recommends this
			if(isAIControlled)
			{
				GetAIInput();
			} else {
				GetInput();
			}
		} else {
			if(isAIControlled)
			{
				 // since the battle is not running, we'll	// tell the AI gunControl not to fire yet
				gunControl.canControl=false;
			}
		}
				
		// see if our car is supposed to be held in place
		CheckLock();
				
		// update the audio
		UpdateEngineAudio();
		
		// finally, update the tread scrolling texture
		if(treadMaterial!=null)
			 treadMaterial.SetTextureOffset ("_MainTex", new Vector2(0, treadMaterial.mainTextureOffset.y + (mySpeed * -0.005f)));
	}
	
	public override 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);
		
		 if(default_input.GetRespawn() && !isRespawning && canRespawn)
		{
			isRespawning=true;
			Respawn();
			canRespawn=false;
			Invoke ("resetRespawn",2);
		}
		
		// fire if we need to
		if(default_input.GetFire() && canFire)
		{
			// tell weapon controller to deal with firing
			weaponControl.Fire();
			
			if(canPlayFireSound)
			{
				canPlayFireSound=false;
				 Invoke ("ResetFireSoundDelay", timeBetweenFireSounds);
			}
		}
	}
	
	void ResetFireSoundDelay()
	{
		canPlayFireSound=true;
	}
	
	void resetRespawn()
	{
		canRespawn=true;	
	}
	
	public void GetAIInput ()
	{
		// calculate steering amount
		steer= Mathf.Clamp(AIController.GetHorizontal(), -1, 1);
		
		// how much accelerator?
   motor= Mathf.Clamp(AIController.GetVertical() , -1, 1);
	}
 public void SetAIInput (bool aiFlag)
 {
   isAIControlled = aiFlag;
}
	
	private ProjectileController aProj;
	private GameObject tempGO;
	
	void OnCollisionEnter(Collision collider)
	{
		 // when something collides with our ship, we check its layer // to see if it is a projectile (Note: remember when you add // projectiles, set the layers correctly!) by default, we're // using layer 17 for projectiles fired by enemies and layer // 9 for projectiles fired by the main player but all we 	// need to know here is that it *is* a projectile of any type
		
		 if(!isRespawning && !isInvulnerable) // make sure no 	// respawning or 	// invulnerability is 	// happening
		{
			 // temp ref to this collider's gameobject so that we // don't need to keep looking it up
			tempGO= collider.gameObject;
			
			 // do a quick layer check to make sure that these	// are in fact projectiles
			if(tempGO.layer==17 || tempGO.layer==9)
			{
				// grab a ref to the projectile's controller
				 aProj= tempGO.GetComponent<ProjectileController>();
			
				 // quick check to make sure that this		// projectile was not launched by this player
				if(aProj.ownerType_id != id)
				{
					 // tell the hit function about this // collision, passing in the		// gameobject so that we can
					 // get to its projectile controller // script and find out more about	// where it came from
					Hit();
					 // tell our battle controller that we // got hit
					battleControl.Fragged();
					
					 // tell the global battle controller // who fragged us
		 GlobalBattleManager.Instance.RegisterFrag		(aProj.ownerType_id);
				}
			}
		}
		
	}
	
	public void OnTriggerEnter(Collider other)
	{
	
		 // check to see if the trigger uses any of the layers where // we want to automatically respawn the player on impact
		int objLayerMask = (1 << other.gameObject.layer);
		if ((respawnLayerMask.value & objLayerMask) > 0)
		{
			Respawn();
		}
	}
	
	void Hit()
	{	
		// reduce lives by one
		myDataManager.ReduceHealth(1);
		
		if(myDataManager.GetHealth()<1) // <- destroyed
		{
			isRespawning=true;
			
			 // blow up! (apply a force to affect everything in // the vicinity and let's get the model spinning	// too, with angular velocity)
			 myBody.AddExplosionForce(myBody.mass * 2000f, myBody.position, 100);
			 myBody.angularVelocity=new Vector3(Random.Range (-100,100), Random.Range (-100,100), Random.Range (-100,100));
			
			// tell game controller to do a nice big explosion
			 GameController_TB.Instance.PlayerBigHit(myTransform);
			
			// respawn
			Invoke("Respawn",4f);
			
			// reset health to full
			myDataManager.SetHealth(startHealthAmount);
		} else {
			
			 // tell game controller to do small scale hit if we // still have some health left
			GameController_TB.Instance.PlayerHit(myTransform);
		}
	}
	void Respawn()
	{
		// reset the 'we are respawning' variable
		isRespawning= false;
		
		 // reset our velocities so that we don't reposition a	// spinning vehicle
		myBody.velocity=Vector3.zero;
		myBody.angularVelocity=Vector3.zero;
		
		// get the waypoint to respawn at from the battle controller
		tempVEC= respawnPoint;
		
		 // cast a ray down from the waypoint to try to find the ground
		RaycastHit hit;
		 if(Physics.Raycast(tempVEC + (Vector3.up * 300), 	-Vector3.up, out hit)){
			tempVEC.y=hit.point.y+15;
		}
		
		 // reposition the player at tempVEC (the waypoint position // with a corrected y value via raycast) and also we set
		 // the player rotation to the waypoint's rotation so that we // are facing in the right direction after respawning
		myTransform.eulerAngles= respawnRotation;
		myTransform.position= tempVEC;
		
		// we need to be invulnerable for a little while
		MakeInvulnerable();
		
		Invoke ("MakeVulnerable", respawnInvunerabilityTime);
		
		// revert to the first weapon
		weaponControl.SetWeaponSlot(0);
		
		 // show the current weapon (since it was hidden when the	// ship explosion was shown)
		weaponControl.EnableCurrentWeapon();
	}
	
	void MakeInvulnerable()
	{
		isInvulnerable=true;
		shieldMesh.SetActive(true);
	}
	
	void MakeVulnerable()
	{
		isInvulnerable=false;
		shieldMesh.SetActive(false);
	}
	public void PlayerFinishedBattle()
	{
		Debug.Log ("PlayerFinished() called!");
		
		// disable this vehicle
		isAIControlled= false;
		canControl= false;
		canFire= false;
		 // if we have an AI controller, let's take away its control // now that the battle is over
		if(AIController!=null)
			AIController.canControl= false;
		motor= 0;
		steer= 0;
		isFinished=true;
	}
	
	void Check_If_Car_Is_Flipped()
	{
		 if((myTransform.localEulerAngles.z > 80 && 		myTransform.localEulerAngles.z < 280) || 		(myTransform.localEulerAngles.x > 80 && 		myTransform.localEulerAngles.x < 280)){
			resetTimer += Time.deltaTime;
		} else {
			resetTimer = 0;
		}
		
		if(resetTimer > resetTime)
			Respawn();
	}
}

13.7.1 Script Breakdown

The CarController_TB.cs script is similar in format to the CarController_MVD.cs script from Metal Vehicle Doom. There are several main differences between the two scripts:

  1. The way that respawning is dealt with.
  2. This version has a battle controller not a race controller.
  3. WeaponControl for this script is a BaseWeapon class type, rather than the slot weapon system. This decision was made so that Metal Vehicle Doom is open to receive switchable weapons in a slot system, whereas this game only requires a single turret with no switching.
  4. There is no waypoint or wrong-way direction checking.
  5. When the player is respawned, it uses the position and rotation of its original start point.
  6. A shield mesh is displayed, a sphere that wraps around the player, when it is invulnerable.

Besides the differences highlighted at the start of this section, it is extremely similar to the car controller script from Chapter 12.

Note that the steering for this game is dealt with by the BaseAIController script’s TurnTowardTarget() function to patrol and find targets. It does not use any horizontal inputs. As it uses the TurnTowardTarget() function to turn, the turning speed of the tanks is set by the value of the variable modelRotateSpeed on the AIController.

13.8 AI Chasing with SetAIChaseTargetBasedOnTag.cs

The AI system is programmed to patrol and then chase a specific target object around. The tanks in this game require this behavior to be more dynamic, with some kind of target-finding system that can pick out targets for itself. The script SetAIChaseTargetBasedOnTag acts as a component to be added to an AI object, which will use Unity’s tag system to find all objects of a specified tag to check their distance and pick the nearest one as a target for the AIController component to use.

The script:

using UnityEngine;
using System.Collections;
using AIStates;
public class SetAIChaseTargetBasedOnTag : MonoBehavior
{
	public BaseAIController AIControlComponent;
	public string defaultTagFilter= "enemy";
	public bool checkForWalls;
	public LayerMask raycastWallLayerMask;
	public float chaseDistance;
	private GameObject myGO;
	private Transform myTransform;
	private GameObject tempGO;
	private RaycastHit hit;
	private Vector3 tempDirVec;
	private bool foundTarget;
	public float visionHeightOffset= 1f;
	void Start ()
	{
		 // first, let's try to get the AI control script		// automatically
		AIControlComponent= GetComponent<BaseAIController>();
		
		myGO= gameObject;
		myTransform= transform;
		
		// quick null check, to warn us if something goes wrong
		if(AIControlComponent == null)
		{
			 Debug.LogWarning("SetAIChaseTargetBasedOnTag cannot find BaseAIController");	
		}
		InvokeRepeating("LookForChaseTarget", 1.5f, 1.5f);
	}
	
	void LookForChaseTarget ()
	{
		// null check
		if(AIControlComponent == null)
			return;
		
		 GameObject[] gos = GameObject.FindGameObjectsWithTag(defaultTagFilter);
	 // Iterate through them
	 foreach (GameObject go in gos)
	 {
			 if(go!= myGO) // make sure we're not comparing 	// ourselves to ourselves
			{
				 float aDist = 				Vector3.Distance(myGO.transform.position, go.transform.position);
				if(checkForWalls)
				{
					// wall check required
					if(CanSee(go.transform)==true)
					{
			 AIControlComponent.SetChaseTarget(go.transform);
						foundTarget= true;
					}
				} else {
					 // no wall check required! go ahead // and find something to chase!
					if(aDist< chaseDistance)
					{
						 // tell our AI controller to // chase this target
			 AIControlComponent.SetChaseTarget(go.transform);
						foundTarget= true;
					}
				}
			}
		}
		if(foundTarget==false)
		{
			// clear target
			AIControlComponent.SetChaseTarget(null);
			// change AI state
			 AIControlComponent.SetAIState(AIState.moving_looking_for_target);
		}
	}
	public bool CanSee(Transform aTarget)
	{
		 // first, let's get a vector to use for raycasting by	 // subtracting the target position from our AI position
		 tempDirVec= Vector3.Normalize(aTarget.position - myTransform.position);
		
		 // let's have a debug line to check the distance between the // two manually, in case you run into trouble!
		Debug.DrawLine(myTransform.position, aTarget.position);
		
		 // cast a ray from our AI, out toward the target passed in // (use the tempDirVec magnitude as the distance to cast)
		 if(Physics.Raycast(myTransform.position + 			(visionHeightOffset * myTransform.up), tempDirVec, out hit, chaseDistance))
		{
			// check to see if we hit the target
			if(hit.transform.gameObject == aTarget.gameObject)
			{
				return true;
			}
		}
		
		// nothing found, so return false
		return false;
	}
}

13.8.1 Script Breakdown

This script will utilize some of the functionality provided by MonoBehavior, which is where it derives from:

using UnityEngine;
using System.Collections;

The AIStates namespace is used by this script to make accessing the AIStates enumeration list easier:

using AIStates;
public class SetAIChaseTargetBasedOnTag : MonoBehavior
{

Start() begins by getting a reference to the AI controller component. It does this with Unity’s GetComponent() function, employed here as a keyword, meaning that it will look for the component on the gameObject this script is attached to:

	void Start ()
	{
		 // first, let's try to get the AI control script		// automatically
		AIControlComponent= GetComponent<BaseAIController>();

Next, the regular gameObject and transform references are cached, and a quick null check is made to make sure that we have an AI controller:

		myGO= gameObject;
		myTransform= transform;
		
		// quick null check, to warn us if something goes wrong
		if(AIControlComponent == null)
		{
			 Debug.LogWarning("SetAIChaseTargetBasedOnTag cannot find BaseAIController");	
		}

The main part of this script is within LookForChaseTarget(), but it is not something we need to do every step or frame of the game running. Instead, InvokeRepeating schedules a repeated call to it every 1.5 s:

		InvokeRepeating("LookForChaseTarget", 1.5f, 1.5f);
	}

This script will not work if it has not been able to find an AI controller script (BaseAIController.cs) attached to the same gameObject. LookForChaseTarget() does a quick null check to get started:

	void LookForChaseTarget ()
	{
		// null check
		if(AIControlComponent == null)
			return;

To find another chase target, references to all of the potential gameObjects need to be retrieved and put into an array. GameObject.FindGameObjectsWithTag() returns an array of all gameObjects in the scene whose tag matches the one passed in as a parameter. The defaultTagFilter variable is actually just a string containing the word “enemy,” which corresponds to the tag with the same name set in the Unity editor:

		 GameObject[] gos = GameObject.FindGameObjectsWithTag(defaultTagFilter);

A foreach loop now steps through each gameObject in the array named gos, which holds all of the tagged objects returned by the call to FindGameObjectsWithTag():

	 // Iterate through them
	 foreach (GameObject go in gos)
	 {

Of course, it makes no sense for this gameObject to ever chase itself, and to avoid this, there is a quick check to see whether go!=myGO:

			if(go!= myGO) // make sure we're not comparing // ourselves to ourselves
			{

To see whether this is a viable target, the process is a simple distance check with Vector3.Distance() between myGO’s transform position (this player) and go’s transform.position (the most recent gameObject picked up by the foreach loop):

				 float aDist = 				Vector3.Distance(myGO.transform.position, go.transform.position);
				if(checkForWalls)
				{
					// wall check required
					if(CanSee(go.transform)==true)
					{
						 AIControlComponent.SetChaseTarget(go.transform);
						foundTarget= true;
					}
				} else {

When no wall check is required, aDist is compared to chaseDistance to see whether the target set by the loop is close enough to be chased:

					 // no wall check required! go ahead // and find something to chase!
					if(aDist< chaseDistance)
					{

The AI controller now needs to target the current gameObject from the foreach loop, which is passed to it via BaseAIController.SetChaseTarget():

						 // tell our AI controller to // chase this target
						 AIControlComponent.SetChaseTarget(go.transform);

We need to track whether or not a target has been found, so that if no suitable target turns up, we can adapt the AI behavior:

						foundTarget= true;
					}
				}
			}
		}

Here, if the variable foundTarget is false, we know that no target was found by the previous distance checks. If a target has not been found, the current chase target needs to be cleared out and the AI controller state switched back to its moving_looking_for_target stage:

		if(foundTarget==false)
		{
			// clear target
			AIControlComponent.SetChaseTarget(null);

With the chase target set to null, we set the AIState to AIState.moving_looking_for_target to make sure that the AI will be moving even though the target has been set to null:

			// change AI state
			 AIControlComponent.SetAIState(AIState.moving_looking_for_target);
		}
	}

CanSee() is almost the exact same code as the CanSee() function built into the BaseAIController.cs script. Take a look at the breakdown in Chapter 9 for a full description on how this function works:

	public bool CanSee(Transform aTarget)
	{
		 // first, let's get a vector to use for raycasting by	// subtracting the target position from our AI position
		 tempDirVec= Vector3.Normalize(aTarget.position - myTransform.position);
		
		 // let's have a debug line to check the distance between the // two manually, in case you run into trouble!
		Debug.DrawLine(myTransform.position, aTarget.position);
		
		 // cast a ray from our AI, out toward the target passed in 	// (use the tempDirVec magnitude as the distance to cast)
		 if(Physics.Raycast(myTransform.position + 			(visionHeightOffset * myTransform.up), tempDirVec, out hit, chaseDistance))
		{
			// check to see if we hit the target
			if(hit.transform.gameObject == aTarget.gameObject)
			{
				return true;
			}
		}
		
		// nothing found, so return false
		return false;
	}
}
..................Content has been hidden....................

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