Chapter 6

Weapon Systems

The weapon system, or more specifically at this point, the firing system, needs to be capable of dealing with either single or multiple weapons. To be as flexible as possible, we will build a weapon management system that will control individual weapon controllers. Those weapon controllers, in turn, will deal with spawning projectiles, and finally, projectiles will take care of themselves independently. Our weapon management system should function with a single weapon, or we should be able to add many different weapons and switch between them.

Our parent system should work independently of each specific weapon and fire out a generic message to the currently selected one. The selected weapon will deal with the call itself, and the overall weapon management system can continue unaffected.

The weapon controller deals with how weapons are stored or manipulated and passes on messages to the actual weapons themselves. It contains an array containing all weapons and a method to activate and deactivate them as required (switching between them).

Each weapon is a prefab with a script attached that derives from BaseWeaponScript.cs. The weapon should be self-contained except when it receives a call to Fire() in which it should do whatever it is supposed to do. BaseWeaponScript contains the basics for a weapon (ammunition, reload time, and the ability to be disabled or enabled).

6.1 Building the Scripts

For the weapon system, there are just two main scripts:

  1. BaseWeaponController.cs

    This is the framework for our weapon control system. It offers everything the system requires, and it is intended to be overridden for customization.

  2. BaseWeaponScript.cs

    This script takes care of individual weapon types. If you were to think of the BaseWeaponController as the arms of the player, the BaseWeaponScript would be the design of the weapon it is holding.

6.1.1 BaseWeaponController.cs

Here is the BaseWeaponController.cs script in its completed form:

using UnityEngine;
using System.Collections;
public class BaseWeaponController : MonoBehavior
{
	public GameObject[] weapons;
	public int selectedWeaponSlot;
	public int lastSelectedWeaponSlot;
	public Vector3 offsetWeaponSpawnPosition;
	public Transform forceParent;
	private ArrayList weaponSlots;
	private ArrayList weaponScripts;
	private BaseWeaponScript TEMPWeapon;
	private Vector3 TEMPvector3;
	private Quaternion TEMProtation;
	private GameObject TEMPgameObject;
	private Transform myTransform;
	private int ownerNum;
	public bool useForceVectorDirection;
	public Vector3 forceVector;
	private Vector3 theDir;
	public void Start ()
	{
		// default to the first weapon slot
		selectedWeaponSlot= 0;
		lastSelectedWeaponSlot= -1;
		// initialize weapon list ArrayList
		weaponSlots= new ArrayList();
		// initialize weapon scripts ArrayList
		weaponScripts= new ArrayList();
		 // cache a reference to the transform (looking up a // transform each step can be expensive, so this is important!)
		myTransform= transform;
		if(forceParent==null)
		{
			forceParent= myTransform;
		}
		 // rather than look up the transform position and rotation // of the player each iteration of the loop below,
		// we cache them first into temporary variables
		TEMPvector3= forceParent.position;
		TEMProtation= forceParent.rotation;
		 // we instantiate all of the weapons and hide them so that // we can activate and use them
		// when needed.
		for(int i=0; i<weapons.Length; i++)
		{
			// Instantiate the item from the weapons list
			 TEMPgameObject= (GameObject) Instantiate(weapons[i], TEMPvector3 + offsetWeaponSpawnPosition, TEMProtation);
			 // make this gameObject that our weapon controller // script is attached to, to be the parent of the	// weapon so that the weapon will move around
			// with the player
			 // NOTE: if you need projectiles to be on a // different layer from the main gameObject, set the // layer of the
			 // forceParent object to the layer you want // projectiles to be on
			TEMPgameObject.transform.parent= forceParent;
			TEMPgameObject.layer= forceParent.gameObject.layer;
			 TEMPgameObject.transform.position= forceParent.position;
			 TEMPgameObject.transform.rotation= forceParent.rotation;
			// store a reference to the gameObject in an ArrayList
			weaponSlots.Add(TEMPgameObject);
			 // grab a reference to the weapon script attached to // the weapon and store the reference in an ArrayList
			 TEMPWeapon= TEMPgameObject.GetComponent<BaseWeaponScript>();
			weaponScripts.Add(TEMPWeapon);
			// disable the weapon
			TEMPgameObject.SetActive(false);
		}
		// now we set the default selected weapon to visible
		SetWeaponSlot(0);
	}
	public void SetOwner(int aNum)
	{
		// used to identify the object firing, if required
		ownerNum= aNum;	
	}
	public virtual void SetWeaponSlot (int slotNum)
	{
		// if the selected weapon is already this one, drop out!
		if(slotNum==lastSelectedWeaponSlot)
			return;
		// disable the current weapon
		DisableCurrentWeapon();
		// set our current weapon to the one passed in
		selectedWeaponSlot= slotNum;
		// make sure sensible values are getting passed in
		if(selectedWeaponSlot<0)
			selectedWeaponSlot= weaponSlots.Count-1;
		 // make sure that the weapon slot isn’t higher than the // total number of weapons in our list
		if(selectedWeaponSlot>weaponSlots.Count-1)
			selectedWeaponSlot=weaponSlots.Count-1;
		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot= selectedWeaponSlot;
		// enable the newly selected weapon
		EnableCurrentWeapon();
	}
	public virtual void NextWeaponSlot (bool shouldLoop)
	{
		// disable the current weapon
		DisableCurrentWeapon();
		// next slot
		selectedWeaponSlot++;
		 // make sure that the slot isn’t higher than the total // number of weapons in our list
		if(selectedWeaponSlot==weaponScripts.Count)
		{
			if(shouldLoop)
			{
				selectedWeaponSlot= 0;
			} else {
				selectedWeaponSlot= weaponScripts.Count-1;
			}
		}
		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot=selectedWeaponSlot;
		// enable the newly selected weapon
		EnableCurrentWeapon();
	}
	public virtual void PrevWeaponSlot (bool shouldLoop)
	{
		// disable the current weapon
		DisableCurrentWeapon();
		// prev slot
		selectedWeaponSlot--;
		// make sure that the slot is a sensible number
		if(selectedWeaponSlot<0)
		{
			if(shouldLoop)
			{
				selectedWeaponSlot= weaponScripts.Count-1;
			} else {
				selectedWeaponSlot= 0;
			}
		}
		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot=selectedWeaponSlot;
		// enable the newly selected weapon
		EnableCurrentWeapon();
	}
	public virtual void DisableCurrentWeapon ()
	{
		if(weaponScripts.Count==0)
			return;
		// grab reference to currently selected weapon script
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];
		// now tell the script to disable itself
		TEMPWeapon.Disable();
		 // grab reference to the weapon’s gameObject and disable // that, too
		TEMPgameObject= (GameObject)weaponSlots[selectedWeaponSlot];
		TEMPgameObject.SetActive(false);
	}
	public virtual void EnableCurrentWeapon ()
	{
		if(weaponScripts.Count==0)
			return;
		// grab reference to currently selected weapon
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];
		// now tell the script to enable itself
		TEMPWeapon.Enable();
		 TEMPgameObject= (GameObject)weaponSlots[selectedWeaponSlot];
		TEMPgameObject.SetActive(true);
	}
	public virtual void Fire ()
	{
		if(weaponScripts==null)
			return;
		if(weaponScripts.Count==0)
			return;
		// find the weapon in the currently selected slot
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];
		theDir = myTransform.forward;
		if(useForceVectorDirection)
			theDir = forceVector;
		// fire the projectile
		TEMPWeapon.Fire(theDir, ownerNum);
	}
}

6.1.1.1 Script Breakdown

The BaseWeaponController class derives from MonoBehavior, utilizing the usual built-in Unity calls (Start(), Update(), FixedUpdate(), etc.):

public class BaseWeaponController : MonoBehavior
{

Skipping down, past the declarations, to the Start() function, default values are set for the currently selected weapon slot, and the last selected weapon and ArrayList arrays are initialized ready to hold different weapon(s). When the lastSelectedWeaponSlot variable is set to −1, it just lets the weapon system know that it needs to set the weapon (to avoid duplicate calls later on, whenever the weapon slot is set, it checks to make sure that the new weapon it is trying to set is different to the one currently set up—hence the −1 to make the values of selectedWeaponSlot versus lastSelectedWeaponSlot different on initialization):

		// default to the first weapon slot
		selectedWeaponSlot= 0;
		lastSelectedWeaponSlot= -1;
		// initialize weapon list ArrayList
		weaponSlots= new ArrayList();
		// initialize weapon scripts ArrayList
		weaponScripts= new ArrayList();

Transforms are cached into myTransform, and the variable forceParent is populated with myTransform if it has not been set in the Unity editor Inspector window. The idea here is that there may be occasions where the required parent transform of the weapon may not always be the transform to which this script is attached. The weapon’s parent transform may be set by dragging a reference into forceParent from within the Unity editor:

		myTransform= transform;
		if(forceParent==null)
		{
			forceParent= myTransform;
		}

The position and rotation of the parent transform will be used repeatedly in the weapons setup code. To avoid having to look them up over and over again, their values are saved in the variables TEMPvector3 and TEMProtation:

		TEMPvector3= forceParent.position;
		TEMProtation= forceParent.rotation;

When the game starts, the prefab references held by the ArrayList array are instantiated and positioned at the parent transform’s position.

The for loop uses weapons.Length to get the length of the weapons array and iterate through it:

		 // we instantiate all of the weapons and hide them so that
		 // we can activate and use them when needed.
		for(int i=0; i<weapons.Length; i++)
		{

The weapon at position i in the array is instantiated at the position of TEMPvector3 with the rotation from TEMProtation:

			// Instantiate the item from the weapons list
			 TEMPgameObject= (GameObject) Instantiate(weapons[i], TEMPvector3 + offsetWeaponSpawnPosition, TEMProtation);

The instantiated weapon is then parented to the transform held in the variable forceParent so that it inherits its position and rotation. In this game, the layer of the parent will also be used to identify where projectiles come from. We’ll look at this layer-based identification a little more as the function unfolds, but for now, just make a little note that the layer of the weapon’s parent will be copied down to the weapon. Individual weapons scripts will also copy the layer setting onto their projectiles:

			TEMPgameObject.transform.parent= forceParent;
			TEMPgameObject.layer= forceParent.gameObject.layer;
			TEMPgameObject.transform.position= TEMPvector3;
			TEMPgameObject.transform.rotation= TEMProtation;

These new objects are going to be the ones that the script manages, enabling and disabling them as required and calling on them to act as required. Each new weapon is added to a new ArrayList called weaponSlots:

			// store a reference to the gameObject in an ArrayList
			weaponSlots.Add(TEMPgameObject);

The BaseWeaponScript instance attached to each weapon in those slots is found (using GameObject.GetComponent()) and stored in another ArrayList named weaponScripts. By putting them in an array, it saves having to look them up every time:

			 // grab a reference to the weapon script attached to // the weapon and store the reference in an ArrayList
			 TEMPWeapon= TEMPgameObject.GetComponent<BaseWeaponScript>();
			weaponScripts.Add(TEMPWeapon);

The weapons need to start out disabled; otherwise, they will all appear on top of each other and all active. They are disabled here using the GameObject.SetActive() Unity function:

			// disable the weapon
			TEMPgameObject.SetActive(false);
		}

Now that all of the weapons are instantiated, set up, and hidden, the last thing needed for initialization is to set the default weapon slot to the first weapon (index of 0). To set the slot, use the SetWeaponSlot() function:

		// now we set the default selected weapon to visible
		SetWeaponSlot(0);
	}

When a projectile hits an enemy or collides with a player, the object being hit needs to know where the projectile came from in order for it to know how to react to the collision. In the example game Lazer Blast Survival, projectile identification is made simply by layer—all player projectiles are set to layer 9, and all enemy projectiles are on layer 17. Set the layer on the gun-mounting-point object (the object that will be the parent to the weapon), and this weapon script will use the same layer when assigning layers to the projectiles.

Another method for checking where projectiles have come from is to assign each player with a unique ID number and to use the SetOwner() function to tell the projectiles who owns each one. This method is employed in the Interstellar Paranoids and Tank Battle example games. The ID number is passed on to projectiles and stored in the script that each projectile has attached to it, the ProjectileController.cs script:

	public void SetOwner(int aNum)
	{
		// used to identify the object firing, if required
		ownerNum= aNum;
	}

The function SetWeaponSlot takes an integer to use as an index number to refer to the weapons stored in the ArrayList named weaponSlots. The weaponSlots ArrayList will be in the same order as weapons added to the original weapons array in the Unity editor Inspector window.

The SetWeaponSlot() function starts out by making sure that the weapon we are trying to set (the variable slotNum) is not the weapon that is already active (the variable lastSelectedWeaponSlot). This is done to avoid duplicate calls to activate the same already activated weapon:

	public virtual void SetWeaponSlot (int slotNum)
	{
		// if the selected weapon is already this one, drop out!
		if(slotNum==lastSelectedWeaponSlot)
			return;

As a new weapon is about to be made active, the current weapon is disabled by a call to the function DisableCurrentWeapon():

		// disable the current weapon
		DisableCurrentWeapon();

selectedWeaponSlot is an index used to store the currently active weapon slot. Here it is set to the incoming parameter value of slotNum:

		// set our current weapon to the one passed in
		selectedWeaponSlot= slotNum;

The new value of selectedWeaponSlot is then validated to make sure that it is not less than zero or more than the total number of occupied slots taking away one (since the weaponSlots array of weapons starts at zero, we take one off the total to find the last entry). During the validation, if the value of slotNum is found to be outside what is allowed, it will be set to the nearest valid number—that way, the function will not fail in setting the weapon because of a bad value in slotNum but will choose the wrong weapon instead:

		// make sure sensible values are getting passed in
		if(selectedWeaponSlot<0)
			selectedWeaponSlot= weaponSlots.Count-1;
		 // make sure that the weapon slot isn’t higher than the // total number of weapons in our list
		if(selectedWeaponSlot>weaponSlots.Count-1)
			selectedWeaponSlot=weaponSlots.Count-1;

Now that selectedWeaponSlot has been set, lastSelectedWeaponSlot needs updating to the latest slot number:

		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot= selectedWeaponSlot;

A call to EnableCurrentWeapon() will take the value of selectedWeaponSlot and activate the currently selected weapon:

		// enable the newly selected weapon
		EnableCurrentWeapon();
	}

The NextWeaponSlot() takes a Boolean parameter to tell the function whether or not to loop around from the last weapon to the first. If the shouldLoop parameter is false, and the current weapon is already set to the last available weapon, it will return the same weapon instead of looping around to zero and returning the weapon from the first slot:

	public virtual void NextWeaponSlot (bool shouldLoop)
	{

First, the current weapon is disabled in anticipation of setting a new one:

		// disable the current weapon
		DisableCurrentWeapon();

selectedWeaponSlot index number is incremented and validated to make sure that it now contains a valid index number (this is where the slot number will be looped back to zero if it goes over the total number of available weapons):

		// next slot
		selectedWeaponSlot++;
		 // make sure that the slot isn’t higher than the total // number of weapons in our list
		if(selectedWeaponSlot==weaponScripts.Count)
		{
			if(shouldLoop)
			{
				selectedWeaponSlot= 0;
			} else {
				selectedWeaponSlot= weaponScripts.Count-1;
			}
		}

The lastSelectedWeaponSlot gets updated before the new current weapon is enabled, completing the function:

		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot=selectedWeaponSlot;
		// enable the newly selected weapon
		EnableCurrentWeapon();
	}

PrevWeaponSlot works in almost exactly the same way as NextWeaponSlot(). It has a Boolean parameter to state whether or not to loop around if the current weapon slot drops below zero, and the most obvious difference is that the selectedWeaponSlot variable is decremented rather than incremented:

	public virtual void PrevWeaponSlot (bool shouldLoop)
	{
		// disable the current weapon
		DisableCurrentWeapon();
		// prev slot
		selectedWeaponSlot--;
		// make sure that the slot is a sensible number
		if(selectedWeaponSlot<0)
		{
			if(shouldLoop)
			{
				selectedWeaponSlot= weaponScripts.Count-1;
			} else {
				selectedWeaponSlot= 0;
			}
		}
		 // we store this selected slot to use to prevent duplicate // weapon slot setting
		lastSelectedWeaponSlot=selectedWeaponSlot;
		// enable the newly selected weapon
		EnableCurrentWeapon();
	}

Disabling the current weapon means that it will be both invisible and unable to fire:

	public virtual void DisableCurrentWeapon ()
	{

To disable the weapon, the script will need to talk to the BaseWeaponScript.cs attached to it. Since weaponScripts and weaponSlots ArrayLists were created in the same order, the selectedWeaponSlot can be used as the index for either array.

To make sure that there are scripts in the weaponScripts array, its Count property is checked before going any further:

		if(weaponScripts.Count==0)
			return;

A reference to the currently selected weapon slot’s weapon script (BaseWeaponScript.cs) is stored in the variable TEMPWeapon as it is retrieved from the weaponScripts ArrayList:

		// grab reference to currently selected weapon script
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];

To tell the weapon to disable, it’s a call to its Disable() function:

		// now tell the script to disable itself
		TEMPWeapon.Disable();

As well as disabling the actual weapon script, it now needs to be hidden from view. First, the variable TEMPgameObject receives a reference to the weapon’s gameObject from the weaponSlots ArrayList. The TEMPgameObject variable is then set to inactive by GameObject.SetActive():

		 // grab reference to the weapon’s gameObject and disable // that, too
		TEMPgameObject= (GameObject)weaponSlots[selectedWeaponSlot];
		TEMPgameObject.SetActive(false);
	}

Enabling the currently selected weapon is done by the EnableCurrentWeapon() function, which works almost exactly the same as DisableCurrentWeapon() except that it calls the Enable() function of the BaseWeaponScript instead of Disable():

	public virtual void EnableCurrentWeapon ()
	{
		if(weaponScripts.Count==0)
			return;
		// grab reference to currently selected weapon
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];
		// now tell the script to enable itself
		TEMPWeapon.Enable();
		 TEMPgameObject= (GameObject)weaponSlots[selectedWeaponSlot];
		TEMPgameObject.SetActive(true);
	}

By default, the Fire() function will launch projectiles along the transform’s forward axis, but there is support for firing along a fixed vector by setting the useForceVectorDirection to true.

The function takes no parameters, making it easy to call from any other script:

	public virtual void Fire ()
	{

The weaponScripts ArrayList is checked to make sure that it has been properly initialized and that it contains entries (a quick count check):

		if(weaponScripts==null)
			return;
		if(weaponScripts.Count==0)
			return;

TEMPWeapon gets a reference to the currently selected weapon’s script from the weaponScripts array:

		// find the weapon in the currently selected slot
		 TEMPWeapon= (BaseWeaponScript)weaponScripts[selectedWeaponSlot];

By default, the firing direction is along the transform’s forward vector:

		theDir = myTransform.forward;

When useForceVectorDirection is set to true, theDir is set to the Vector from the variable forceVector, which should be set in the Unity editor Inspector window on the gameObject:

		if(useForceVectorDirection)
			theDir = forceVector;

The ownerNum variable was mentioned earlier in this section, where it is set by the SetOwner() function. When the call to the currently selected weapon’s Fire() function goes out, it takes a Vector3 and the owner ID:

		// fire the projectile
		TEMPWeapon.Fire(theDir, ownerNum);
	}
}

6.1.2 BaseWeaponScript.cs

Here is the BaseWeaponController.cs script in its completed form:

using UnityEngine;
using System.Collections;
public class BaseWeaponScript : MonoBehavior
{
	[System.NonSerialized]
	public bool canFire;
	public int ammo= 100;
	public int maxAmmo= 100;
	public bool isInfiniteAmmo;
	public GameObject projectileGO;
	public Collider parentCollider;
	private Vector3 fireVector;
	[System.NonSerialized]
	public Transform myTransform;
	private int myLayer;
	public Vector3 spawnPosOffset;
	public float forwardOffset= 1.5f;
	public float reloadTime= 0.2f;
	public float projectileSpeed= 10f;
	public bool inheritVelocity;
	[System.NonSerialized]
	public Transform theProjectile;	
	private GameObject theProjectileGO;
	private bool isLoaded;
	private ProjectileController theProjectileController;
	public virtual void Start()
	{
		Init();
	}
	public virtual void Init()
	{
		// cache the transform
		myTransform= transform;
		 // cache the layer (we’ll set all projectiles to avoid this // layer in collisions so that things don’t shoot themselves!)
		myLayer= gameObject.layer;
		// load the weapon
		Reloaded();
	}
	public virtual void Enable()
	{
		// drop out if firing is disabled
		if(canFire==true)
			return;
		// enable weapon (do things like show the weapons mesh etc.)
		canFire=true;
	}
	public virtual void Disable()
	{
		// drop out if firing is disabled
		if(canFire==false)
			return;
		// hide weapon (do things like hide the weapons mesh etc.)
		canFire=false;
	}
	public virtual void Reloaded()
	{
		 // the 'isLoaded' var tells us if this weapon is loaded and // ready to fire
		isLoaded= true;
	}
	public virtual void SetCollider(Collider aCollider)
	{
		parentCollider= aCollider;
	}
	public virtual void Fire(Vector3 aDirection, int ownerID)
	{
		 // be sure to check canFire so that the weapon can be // enabled or disabled as required!
		if(!canFire)
			return;
		// if the weapon is not loaded, drop out
		if(!isLoaded)
			return;
		 // if we’re out of ammo and we do not have infinite ammo, // drop out...
		if(ammo<=0 && !isInfiniteAmmo)
			return;
		// decrease ammo
		ammo--;
		// generate the actual projectile
		FireProjectile(aDirection, ownerID);
		// we need to reload before we can fire again
		isLoaded= false;
		 // schedule a completion of reloading in <reloadTime> // seconds:
		CancelInvoke("Reloaded");
		Invoke("Reloaded", reloadTime);
	}
	 public virtual void FireProjectile(Vector3 fireDirection, int ownerID)
	{
		// make our first projectile
		theProjectile= MakeProjectile(ownerID);
		// direct the projectile toward the direction of fire
		 theProjectile.LookAt(theProjectile.position + fireDirection);
		// add some force to move our projectile
		 theProjectile.rigidbody.velocity= fireDirection * projectileSpeed;
	}
	public virtual Transform MakeProjectile(int ownerID)
	{		
		// create a projectile
		 theProjectile= SpawnController.Instance.Spawn(projectileGO, myTransform.position+spawnPosOffset + (myTransform.forward * forwardOffset), myTransform.rotation);
		theProjectileGO= theProjectile.gameObject;
		theProjectileGO.layer= myLayer;
		 // grab a ref to the projectile’s controller so we can pass // on some information about it
		 theProjectileController= theProjectileGO.GetComponent<ProjectileController>();
		// set owner ID so we know who sent it
		theProjectileController.SetOwnerType(ownerID);
		 Physics.IgnoreLayerCollision(myTransform.gameObject.layer, myLayer);
		 // NOTE: Make sure that the parentCollider is a collision // mesh which represents the firing object
		 // or a collision mesh likely to be hit by a projectile as // it is being fired from the vehicle.
		 // One limitation with this system is that it only reliably // supports a single collision mesh
		if(parentCollider!=null)
		{
			 // disable collision between 'us' and our projectile // so as not to hit ourselves with it!
			 Physics.IgnoreCollision(theProjectile.collider, parentCollider);
		}
		 // return this projectile in case we want to do something // else to it
		return theProjectile;
	}
}

6.1.2.1 Script Breakdown

The BaseWeaponScript class derives from MonoBehavior:

using UnityEngine;
using System.Collections;
public class BaseWeaponScript : MonoBehavior
{
	public virtual void Start()
	{
		Init();
	}

Init() begins by grabbing a reference to the transform and to the layer at which this gameObject with this script attached to is set. This will be used to disable collisions between the layer of the weapon and the actual projectile so that the projectile does not just explode as soon as it is instantiated:

	public virtual void Init()
	{
		// cache the transform
		myTransform= transform;
		 // cache the layer (we’ll set all projectiles to avoid this // layer in collisions so that things don’t shoot // themselves!)
		myLayer= gameObject.layer;
		// load the weapon
		Reloaded();
	}

The Enable() and Disable() functions set the Boolean variable canFire to true and false, respectively. This will be checked elsewhere in the code before allowing any projectile firing. Note that the visual representation of the weapon is not hidden here; that task is left to the slot control script to deal with rather than to the weapon itself:

	public virtual void Enable()
	{
		canFire=true;
	}
	public virtual void Disable()
	{
		canFire=false;
	}

When the weapon is fired, the assumption is made that it will not be loaded for a certain period of time (otherwise, you could, in theory, fire out thousands of projectiles each second). To keep track of when the weapon is in a loaded state, this script uses the Boolean variable isLoaded. A timed call to the Reloaded() function, after firing, will reset the weapon state again:

	public virtual void Reloaded()
	{
		 // the 'isLoaded' var tells us if this weapon is loaded and // ready to fire
		isLoaded= true;
	}

SetCollider() is used to set a collider to be ignored by a projectile. For example, when the script is applied to a player, it should ignore the player’s collider so as to prevent the newly created projectile from exploding instantly. It should be set, using the Unity editor Inspector window, to a collider that would likely destroy the projectile as it was spawned. If no parentCollider is set, the script will still work, but the projectile will not ignore any collider:

	public virtual void SetCollider(Collider aCollider)
	{
		parentCollider= aCollider;
	}

The Fire() function takes two parameters, a Vector3 to represent the direction of fire and an integer providing an ID number for its owner (which gets used primarily by collision code to know how to react):

	public virtual void Fire(Vector3 aDirection, int ownerID)
	{

The Boolean variable canFire needs to be true (the weapon is allowed to fire) and isLoaded needs to be true also (the weapon is ready to fire) before the function can progress to check the state of ammunition:

		 // be sure to check canFire so that the weapon can be // enabled or disabled as required!
		if(!canFire)
			return;
		// if the weapon is not loaded, drop out
		if(!isLoaded)
			return;

If the integer variable ammo is less than zero and the Boolean isInfiniteAmmo has not been set in the Unity editor Inspector window to true, then the function will drop out here, too:

		 // if we’re out of ammo and we do not have infinite ammo, // drop out...
		if(ammo<=0 && !isInfiniteAmmo)
			return;

Since all of the criteria have been met, the function goes ahead and decrements ammo in anticipation of the projectile about to be generated by the FireProjectile() function. The direction held by aDirection and the owner ID in the variable ownerID also get passed on to the FireProjectile() function as parameters:

		// decrease ammo
		ammo--;
		// generate the actual projectile
		FireProjectile(aDirection, ownerID);

This is where the Boolean variable isLoaded gets reset to false to delay firing for a reasonable amount of time (set by the value held in the variable reloadTime):

		// we need to reload before we can fire again
		isLoaded= false;

CancelInvoke is called to make sure that there is never more than one Invoke call to Reloaded() waiting to activate:

		 // schedule a completion of reloading in <reloadTime>	// seconds:
		CancelInvoke("Reloaded");
		Invoke("Reloaded", reloadTime);
	}

The FireProjectile() function is where the projectile gets instantiated. It was called by that last function, Fire():

	 public virtual void FireProjectile(Vector3 fireDirection, int ownerID)
	{

The function MakeProjectile will do the work in getting a physical projectile into the scene, but it returns a transform that this function can then use to set up with:

		// make our first projectile
		theProjectile= MakeProjectile(ownerID);

Now theProjectile contains a transform; the code uses the Transform.LookAt() function to align it along the target trajectory passed in via the parameter variable fireDirection. Since LookAt() requires a world position vector (as opposed to a direction vector), it takes the projectile’s current position from theProjectile.position and adds the fireDirection vector to it. This will adjust the new projectile’s rotation so that its z-axis is facing in the required direction of travel:

		// direct the projectile toward the direction of fire
		theProjectile.LookAt(theProjectile.position + fireDirection);

To send the projectile on its way, the projectile’s rigidbody has its velocity set to the required firing direction multiplied by projectilSpeed. Setting its velocity directly, rather than applying forces, makes its movement more predictable; how velocity is affected by applied force is strongly influenced by other properties of the rigidbody such as drag, gravity, and mass, whereas velocity will immediately set it in movement at the correct speed in the required direction:

		// add some force to move our projectile
		 theProjectile.rigidbody.velocity= fireDirection * projectileSpeed;
	}

MakeProjectile() handles the creation of the physical projectile, taking a parameter of the ownerID (to pass on to the projectile) and returning the newly instantiated projectile transform:

	public virtual Transform MakeProjectile(int ownerID)
	{

The SpawnController script from Chapter 4 is used to instantiate the projectile through its Spawn() function. For convenience, SpawnController.Instance.Spawn() takes the exact same parameters as Unity’s Instantiate function: the prefab reference to spawn, a Vector3 position, and a rotation for the spawned object.

The projectile to spawn is held in the variable projectileGO (typed as a GameObject); the position is found by taking myTransform’s position (the position of the weapon) and adding to it the spawnPosOffset vector (a Vector3 intended to be set in the Unity editor Inspector window). The rotation is taken from the weapon’s rotation myTransform.rotation, but the projectile will normally be created from the Fire() function and a new rotation applied to it after the projectile is returned:

		// create a projectile
		 theProjectile= SpawnController.Instance.Spawn(projectileGO, myTransform.position+spawnPosOffset + (myTransform.forward * forwardOffset), myTransform.rotation);

To set the projectile’s layer, access to its gameObject is required since there is no way to set the layer of a transform. theProjectileGO holds a quick temporary reference to theProjectile.gameObject, and the next line sets its layer to the value held by myLayer:

		theProjectileGO= theProjectile.gameObject;
		theProjectileGO.layer= myLayer;

The projectile needs to know about the owner ID so that it can be identified during a collision. Before the script can tell the projectile about it, it needs to use GameObject.GetComponent() to find the ProjectileController.cs script instance

		 // grab a ref to the projectile’s controller so we can pass // on some information about it
		 theProjectileController= theProjectileGO.GetComponent<ProjectileController>();

With the instance reference in theProjectileController, the next line tells the projectile’s script about its owner ID:

		// set owner ID so we know who sent it
		theProjectileController.SetOwnerType(ownerID);

In an attempt to prevent projectiles from colliding either with the parent object or with other projectiles spawned closely together, Physics.IgnoreLayerCollision() is used.

Physics.IgnoreLayerCollision() takes two layer numbers (both integers) referring to which layers should ignore colliding with each other. The layer numbers come from the Tags and Layers manager accessible via the menu Edit → Project Settings → Tags and Layers or the Unity editor Inspector window by clicking on the layer dropdown and clicking on the Add layer button. This is applied globally, which means that once two layers are on Unity’s ignore list, everything placed on those two layers will no longer register collisions.

In this case, the first layer is the weapon’s layer from myTransform.gameObject.layer and the second taken from the variable myLayer, which was set in the Init() function of this script:

	Physics.IgnoreLayerCollision(myTransform.gameObject.layer, myLayer);

A better solution to avoiding collisions between the projectile and any parent collider is to set the parentCollider reference in the Unity editor Inspector window. This part of the code checks that parentCollider is not null; then as long as it contains something, Physics.When a collider is accessible to this script via parentCollider (as set in the Unity editor Inspector window), IgnoreCollision is used to tell the engine to ignore collisions between the two specific colliders.

Whereas Physics.IgnoreLayerCollision() worked to disable all collisions between objects on two layers, the Physics.IgnoreCollision() function stops collision events on specified colliders:

		if(parentCollider!=null)
		{
			 // disable collision between 'us' and our projectile // so as not to hit ourselves with it!
			 Physics.IgnoreCollision(theProjectile.collider, parentCollider);
		}

The newly created projectile needs to be returned to the function calling (especially when this is called from the Fire() function shown earlier in this section), and the last bit of code from this script does just that:

		 // return this projectile in case we want to do something // else to it
		return theProjectile;
	}	
}

Further on in this book, when the example games are examined, these base scripts will come up again and we will see how they can be applied to real game situations. In Chapter 9, we look at adding artificial intelligence (AI) to the game and how the weapon controller can easily be tied into it and controlled by an AI player in a game.

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

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