• No results found

5.2 Humanoid Character

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;

135 6.1 Building the Scripts

[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 );

137 6.1 Building the Scripts

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<Projec

tileController>();

// 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 projec- tile 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

139 6.1 Building the Scripts

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 colli- sion 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 pro- gress 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 rea- sonable 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() func- tion to align it along the target trajectory passed in via the parameter variable fireDirec- tion. 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 fireDirec- tion 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,

141 6.1 Building the Scripts

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.rota- tion, but the projectile will normally be created from the Fire() function and a new rota- tion 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<Projec

tileController>();

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

// set owner ID so we know who sent it

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.

143

7.1 Waypoint System

Both the racing game Motor Vehicle Doom and the shoot ’em up Interstellar Paranoids will need waypoints for the AI players. In Motor Vehicle Doom, we use waypoints for the AI and also for the main player for the following reasons:

1. To check that the main player is heading in the right direction. The car controller code will track the player’s position on the track (based on which waypoint has been passed) and check its forward vector to make sure that it is facing the next waypoint along the track. If the player’s forward vector is not within a certain tol- erance angle, the game will display a wrong way message and eventually respawn the car facing the right way.

2. To enable the respawning system to find a “safe” place along the track to respawn the player as well as to use its rotation to point the respawned car in the right direction along the track.

3. To find out how far the vehicle has traveled around the track, which is used to compare the other players progress amounts to calculate race positions.

Interstellar Paranoids uses waypoints for moving all of the enemies. Enemies are simple

path followers that move along a path firing until they reach the end, where they are destroyed.

7

Recipe: Waypoints

Manager

The Waypoints_Controller.cs script is very similar to the one from my last book, Game

Development for iOS with Unity3D (also published by CRC Press). The waypoints controller

here has been converted over to C# from its original JavaScript form:

public class Waypoints_Controller : MonoBehavior {

[ExecuteInEditMode]

// this script simply gives us a visual path to make it easier to edit // our waypoints

private ArrayList transforms; // arraylist for easy access to

// transforms

private Vector3 firstPoint; // store our first waypoint so we can

// loop the path

private float distance; // used to calculate distance between

// points

private Transform TEMPtrans; // a temporary holder for a transform private int TEMPindex; // a temporary holder for an index number private int totalTransforms;

private Vector3 diff; private float curDistance; private Transform closest; private Vector3 currentPos; private Vector3 lastPos; private Transform pointT; public bool closed=true; public bool shouldReverse; void Start()

{

// make sure that when this script starts that // we have grabbed the transforms for each waypoint GetTransforms();

}

void OnDrawGizmos() {

// we only want to draw the waypoints when we're editing, // not when we are playing the game

if( Application.isPlaying ) return;

GetTransforms();

// make sure that we have more than one transform in the // list, otherwise we can't draw lines between them if (totalTransforms < 2)

return;

// draw our path first, we grab the position of the very // first waypoint so that our line has a start point TEMPtrans = (Transform)transforms[0];

lastPos = TEMPtrans.position;

// we point each waypoint at the next, so that we can use // this rotation data to find out when the player is going

145 7.1 Waypoint System

// the wrong way or to position the player after a reset // facing the correct direction. So first we need to hold a // reference to the transform we are going to point

pointT = (Transform)transforms[0];

// also, as this is the first point we store it to use for // closing the path later

firstPoint = lastPos;

// now we loop through all of the waypoints drawing lines // between them

for (int i = 1; i < transforms.Count; i++) { TEMPtrans = (Transform)transforms[i]; if(TEMPtrans==null) { GetTransforms(); return; }

// grab the current waypoint position

currentPos = TEMPtrans.position;

Gizmos.color=Color.green; Gizmos.DrawSphere(currentPos,2);

// draw the line between the last waypoint and this one Gizmos.color=Color.red;

Gizmos.DrawLine(lastPos, currentPos);

// point our last transform at the latest position pointT.LookAt(currentPos);

// update our 'last' waypoint to become this one as we // move on to find the next...

lastPos = currentPos;

// update the pointing transform pointT=(Transform)transforms[i]; }

// close the path if(closed) { Gizmos.color=Color.red; Gizmos.DrawLine(currentPos, firstPoint); } }

public void SetReverseMode(bool rev) {

shouldReverse=rev; }

public void GetTransforms() {

// we store all of the waypoints transforms in an ArrayList, // which is initialised here (we always need to do this // before we can use ArrayLists)

// now we go through any transforms 'under' this transform, // so all of the child objects that act as our waypoints get // put into our arraylist

foreach(Transform t in transform) {

// add this transform to our arraylist transforms.Add(t);

}

totalTransforms=(int)transforms.Count; }

public int FindNearestWaypoint ( Vector3 fromPos, float maxRange) {

// make sure that we have populated the transforms // list, if not, populate it

if(transforms==null) GetTransforms();

// the distance variable is just used to hold the // 'current' distance when we are comparing, so that // we can find the closest distance = Mathf.Infinity; // Iterate through them and find the closest one for(int i = 0; i < transforms.Count; i++)

{

// grab a reference to a transform

TEMPtrans = (Transform)transforms[i];

// calculate the distance between the current // transform and the passed in transform’s // position vector

diff = (TEMPtrans.position - fromPos);

curDistance = diff.sqrMagnitude;

// now compare distances - making sure that // we are not closer than the closest object // (whose distance is held by the variable

// (distance)

if ( curDistance < distance ) {

if( Mathf.Abs( TEMPtrans.position.y -

fromPos.y ) < maxRange ) {

// set our current 'winner'

// (closest transform) to the // transform we just found

closest = TEMPtrans;

// store the index of this

// waypoint TEMPindex=i;

// set our 'winning' distance

// to the distance we just // found

distance = curDistance;

} }

147 7.1 Waypoint System

// now we make sure that we did actually find // something, then return it

if(closest) {

// return the waypoint we found in this test

return TEMPindex;

} else {

// no waypoint was found, so return -1 (this // should be accounted for at the other end!)

return -1;

} }

// this function has the addition of a check to avoid finding the // same transform as one passed in. We use this to make sure that // when we are looking for the nearest waypoint we don’t find the // same one as we just passed

public int FindNearestWaypoint ( Vector3 fromPos , Transform exceptThis, float maxRange)

{

// make sure that we have populated the transforms // list, if not, populate it

if(transforms==null) GetTransforms();

// the distance variable is just used to hold the // 'current' distance when we are comparing, so that we can find the closest distance = Mathf.Infinity;

// Iterate through them and find the closest one for(int i = 0; i < transforms.Count; i++)

{

// grab a reference to a transform

TEMPtrans = (Transform)transforms[i];

// calculate the distance between the current // transform and the passed in transform’s // position vector

diff = (TEMPtrans.position - fromPos);

curDistance = diff.sqrMagnitude;

// now compare distances - making sure that // we are not

if ( curDistance < distance && TEMPtrans != exceptThis )

{

if( Mathf.Abs( TEMPtrans.position.y -

fromPos.y ) < maxRange ) {

// set our current 'winner'

// (closest transform) to the // transform we just found

closest = TEMPtrans;

// store the index of this

// waypoint TEMPindex=i;

// set our 'winning' distance // to the distance we just

// found

distance = curDistance;

} }

}

// now we make sure that we did actually find // something, then return it

if(closest) {

// return the waypoint we found in this test

return TEMPindex;

} else {

// no waypoint was found, so return -1 (this // should be accounted for at the other end!)

return -1;

} }

public Transform GetWaypoint(int index) {

if( shouldReverse ) {

// send back the reverse index'd waypoint