banner



Introduction To Game Design Prototyping And Development 31

Iterative Code Development When prototyping on your own, this kind of announcement test is something that you will do often to test whether the code you've written is working properly. I find that it is much better to do small tests along the way like this than to work on code for hours only to find at the end that something is causing a bug. Testing incrementally makes things a lot easier to debug because you know that you've only made slight changes since the last test that worked, so it's easier to find the place where you added a bug.

Another key element of this approach is using the debugger. Throughout the authoring of this book, any time I ran into something that worked a little differently than I expected, I used the debugger to understand what was happening. If you don't remember how to use the MonoDevelop debugger, I highly recommend rereading Chapter 24, "Debugging."

Using the debugger effectively is often the difference between solving your code problems and just staring at pages of code blankly for several hours. Try putting a debug breakpoint into the OnTriggerEnter() method you just modified and watching how code is called and variables change. The recursive calling of Utils.FindTaggedParent() in particular should be interesting.

Iterative code development has the same strengths as the iterative process of design, and it is the key to the agile development methodology discussed in Chapter 27, "The Agile Mentality."


Next, modify the OnTriggerEnter() method of the Hero class to make a collision with an enemy decrease the player's shield by 1 and destroy the Enemy that was hit. It's also very important to make sure that the same parent GameObject doesn't trigger the Shield collider twice (which could happen with very fast-moving objects if two child colliders of one object hit the Shield trigger in the same frame).

public class Hero : MonoBehaviour {
...
void Update() {
...
}

// This variable holds a reference to the last triggering GameObject
public GameObject lastTriggerGo = null; // 1

void OnTriggerEnter(Collider other) {
...
if (go != null) {
// Make sure it's not the same triggering go as last time
if (go == lastTriggerGo) { // 2
return;
}
lastTriggerGo = go; // 3

if (go.tag == "Enemy") {
// If the shield was triggered by an enemy
// Decrease the level of the shield by 1
shieldLevel--;
// Destroy the enemy
Destroy(go); // 4
} else {
print("Triggered: "+go.name); // Move this line here!
}
} else {
...
}

1. This field holds a reference to the last GameObject that triggered Shield collider. It is initially set to null. Though we usually declare fields at the top of the class, they can actually be declared anywhere throughout the class, as we have done with this line.

2. If lastTriggerGo is the same as go (the current triggering GameObject), this collision is ignored as a duplicate, which can happen if two children GameObjects of the same Enemy trigger the Shield collider at the same time (that is, in the same single frame).

3. Assign go to lastTriggerGo so that it is updated the next time OnTriggerEnter() is called.

4. go, the enemy GameObject, is destroyed by hitting the shield. Because the actual GameObject go that we're testing is the Enemy GameObject found by Utils.FindTaggedParent(), this will delete the entire Enemy (and by extension, all of its children), and not just one of the Enemy's child GameObjects.

Play the scene and try running into some ships. After running into more than a few, you may notice a strange shield behavior. The shield will loop back around to full strength after being completely drained. What do you think is causing this? Try selecting _Hero in the Hierarchy while playing the scene to see what's happening to the shieldLevel field.

Because there is no bottom limit to shieldLevel, it continues past 0 into negative territory. The Shield C# script then translates this into negative x offset values for Mat Shield, and because the material's texture is set to loop, it looks like the shield is returning to full strength.

To fix this, we will convert shieldLevel to a property that insulates and limits a new private field named _shieldLevel. The shieldLevel property will watch the value of the _shieldLevel field and make sure that _shieldLevel never gets above 4 and that the ship is destroyed if _shieldLevel ever drops below 0. An insulated field like _shieldLevel should be set to private because it does not need to be accessed by other classes; however, in Unity, private fields are not viewable in the Inspector. To remedy this, the line [SerializeField] is added above the declaration of _shieldLevel to instruct Unity to show it in the Inspector even though it is a private field. Properties are never visible in the Inspector, even if they're public.

First, change the name of the public variable shieldLevel to _shieldLevel near the top of the Hero class, set it to private, and add the [SerializeField] line:

// Ship status information
[SerializeField]
private float _shieldLevel = 1; // Add the underscore!

Next, add the shieldLevel property to the end of the Hero class.

public class Hero : MonoBehaviour {

...

void OnTriggerEnter(Collider other) {
...
}

public float shieldLevel {
get {
return( _shieldLevel ); // 1
}
set {
_shieldLevel = Mathf.Min( value, 4 ); // 2
// If the shield is going to be set to less than zero
if (value < 0) { // 3
Destroy(this.gameObject);
}
}
}
}

1. The get clause just returns the value of _shieldLevel.

2. Mathf.Min() ensures that _shieldLevel is never set to a number higher than 4.

3. If the value passed into the set clause is less than 0, _Hero is destroyed.

Restarting the Game

From your testing, you can see that the game gets exceedingly boring once _Hero has been destroyed. We'll now modify both the Hero and Main classes to call a method when _Hero is destroyed that waits for 2 seconds and then restarts the game.

Add a gameRestartDelay field to the top of the Hero class:

static public Hero S; // Singleton

public float gameRestartDelay = 2f;

// These fields control the movement of the ship

Then add the following lines to the shieldLevel property definition in the Hero class:

if (value < 0) {
Destroy(this.gameObject);
// Tell Main.S to restart the game after a delay
Main.S.DelayedRestart( gameRestartDelay );
}

Finally, add the following methods to the Main class to make this work.

public class Main : MonoBehaviour {
...

public void SpawnEnemy() {
...
}

public void DelayedRestart( float delay ) {
// Invoke the Restart() method in delay seconds
Invoke("Restart", delay);
}

public void Restart() {
// Reload _Scene_0 to restart the game
Application.LoadLevel("_Scene_0");
}

}

Now, once the player ship has been destroyed, the game waits a couple of seconds and then restarts by reloading the scene.

Shooting (Finally)

Now that the enemy ships can hurt the player, it's time to give _Hero a way to fight back.

Artwork

Create an empty GameObject, name it Weapon, and give it the following structure and children:

Image

Remove the Collider component from both Barrel and Collar by selecting them individually and then right-clicking on the name of the Box Collider component and choosing Remove Component from the pop-up menu. You can also click the gear to the right of the Box Collider name to get the same menu.

Now, create a new material named Mat Collar. Drag this material on to Collar to assign it. In the Inspector, choose ProtoTools > UnlitAlpha from the Shader pop-up menu. The Collar should now be a bright white (see Figure 30.9).

Now, create a new C# script named Weapon and drag it onto the Weapon GameObject in the Hierarchy. Then drag the Weapon GameObject into the _Prefabs folder in the Project pane to make it a prefab. Make the Weapon instance in the Hierarchy a child of _Hero and set its position to [0,2,0]. This should place the Weapon on the nose of the _Hero ship, as is shown in Figure 30.9.

Image

Figure 30.9 Weapon with the Collar selected and proper material and shader selected

Save your scene! Are you remembering to save constantly?

Next, create a cube named ProjectileHero in the Hierarchy as follows:

Image

Set both the tag and layer of ProjectileHero to ProjectileHero. Create a new material named Mat Projectile, give it the ProtoTools > UnlitAlpha shader, and assign it to the ProjectileHero GameObject. Add a Rigidbody component to the ProjectileHero GameObject with the settings shown inFigure 30.10. (The transform.position of ProjectileHero doesn't actually matter because it will be a prefab that is positioned via code.) Create a new C# script named Projectile and drag it onto ProjectileHero. We'll edit the script later.

Image

Figure 30.10 ProjectileHero with the proper settings showing the large Size.z of the Box Collider

In the Box Collider component of the ProjectileHero GameObject, set Size.z to 10. This will make sure that the projectile is able to hit anything that is slightly off of the z=0 plane.

Save your scene.

Drag ProjectileHero into the _Prefabs folder in the Project pane to make it a prefab and delete the instance remaining in the Hierarchy.

Save your scene. As I've said, you want to save as often as you can.

The Serializable WeaponDefinition Class

Open the Weapon script in MonoDevelop and enter the following code:

using UnityEngine;
using System.Collections;

// This is an enum of the various possible weapon types
// It also includes a "shield" type to allow a shield power-up
// Items marked [NI] below are Not Implemented in this book
public enum WeaponType {
none, // The default / no weapon
blaster, // A simple blaster
spread, // Two shots simultaneously
phaser, // Shots that move in waves [NI]
missile, // Homing missiles [NI]
laser, // Damage over time [NI]
shield // Raise shieldLevel
}

// The WeaponDefinition class allows you to set the properties
// of a specific weapon in the Inspector. Main has an array
// of WeaponDefinitions that makes this possible.
// [System.Serializable] tells Unity to try to view WeaponDefinition
// in the Inspector pane. It doesn't work for everything, but it
// will work for simple classes like this!
[System.Serializable]
public class WeaponDefinition {
public WeaponType type = WeaponType.none;
public string letter; // The letter to show on the power-up
public Color color = Color.white; // Color of Collar & power-up
public GameObject projectilePrefab; // Prefab for projectiles
public Color projectileColor = Color.white;
public float damageOnHit = 0; // Amount of damage caused
public float continuousDamage = 0; // Damage per second (Laser)
public float delayBetweenShots = 0;
public float velocity = 20; // Speed of projectiles
}

// Note: Weapon prefabs, colors, and so on. are set in the class Main.

public class Weapon : MonoBehaviour {
// The Weapon class will be filled in later.
}

As described in the code comments, the enum WeaponType defines all the possible weapon types and power-up types. WeaponDefinition is a class that combines a WeaponType with several other fields that will be useful for defining each weapon. Add the following code to theMain class:

public class Main : MonoBehaviour {
...
public float enemySpawnPadding = 1.5f; // Padding for position
public WeaponDefinition[] weaponDefinitions;

public bool ________________;

public WeaponType[] activeWeaponTypes;
public float enemySpawnRate; // Delay between Enemies

void Awake() {...}

void Start() {
activeWeaponTypes = new WeaponType[weaponDefinitions.Length];
for ( int i=0; i<weaponDefinitions.Length; i++ ) {
activeWeaponTypes[i] = weaponDefinitions[i].type;
}
}
...
}

Save this and then select _MainCamera in the Hierarchy. You should now see a weaponDefinitions array in the Main (Script) component Inspector. Click the disclosure triangle next to it and set the Size of the array to 3. Enter settings for the three WeaponDefinitions as shown in Figure 30.11. The colors don't have to be exactly right, but it is important that the alpha value of each color is set to fully opaque (which appears as a white bar beneath the color swatch).

Image

Figure 30.11 Settings for the WeaponDefinitions of blaster, spread, and shield on Main


Warning

Colors Sometimes Default to an Invisible Alpha When you create a serializable class like WeaponDefinition that includes color fields, the alpha values of those colors will default to 0 (i.e., invisible). To fix this, make sure that the white bar under each of your color definitions is actually white (and not black). If you click on the color itself, you will be presented with four values to set (R, G, B, and A). Make sure that A is set to 255 (i.e., fully opaque) or your shots will be invisible.

If you are using OS X and have chosen to use the OS X color picker in Unity instead of the default one, the A value is set by the Opacity slider at the bottom of the color picker window (which should be set to 100% for these colors).


A Generic Dictionary for WeaponDefinitions

Now, open the Main script in MonoDevelop and enter the following bold code. This code uses a Dictionary, which is another type of generic collection like List. Dictionaries have a key type and value type, and the key is used to retrieve the value. Here, the Dictionary has the enumWeaponType as the key and the class WeaponDefinition as the value. We will create the static public W_DEFS dictionary to hold the WeaponDefinition information that we just entered into the array in the Main (Script) Inspector. Unfortunately, Dictionaries do not appear in the Inspector, or we would have just used one from the start. Instead, the W_DEFS Dictionary is defined in the Awake() method of Main and then used by the static function Main.GetWeaponDefinition().

public class Main : MonoBehaviour {
static public Main S;
static public Dictionary<WeaponType, WeaponDefinition> W_DEFS;
...
void Awake() {
...
Invoke( "SpawnEnemy", enemySpawnRate );

// A generic Dictionary with WeaponType as the key
W_DEFS = new Dictionary<WeaponType, WeaponDefinition>();
foreach( WeaponDefinition def in weaponDefinitions ) {
W_DEFS[def.type] = def;
}
}

static public WeaponDefinition GetWeaponDefinition( WeaponType wt ) {
// Check to make sure that the key exists in the Dictionary
// Attempting to retrieve a key that didn't exist, would throw an error,
// so the following if statement is important.
if (W_DEFS.ContainsKey(wt)) {
return( W_DEFS[wt]);
}
// This will return a definition for WeaponType.none,
// which means it has failed to find the WeaponDefinition
return( new WeaponDefinition() );
}

void Start() {...}
}

Now, open the Projectile class in MonoDevelop and enter this code:

using UnityEngine;
using System.Collections;

public class Projectile : MonoBehaviour {
[SerializeField)
private WeaponType _type;

// This public property masks the field _type & takes action when it is set
public WeaponType type {
get {
return( _type );
}
set {
SetType( value );
}
}

void Awake() {
// Test to see whether this has passed off screen every 2 seconds
InvokeRepeating( "CheckOffscreen", 2f, 2f );
}

public void SetType( WeaponType eType ) {
// Set the _type
_type = eType;
WeaponDefinition def = Main.GetWeaponDefinition( _type );
renderer.material.color = def.projectileColor;
}

void CheckOffscreen() {
if ( Utils.ScreenBoundsCheck( collider.bounds, BoundsTest.offScreen ) != Vector3.zero ) {
Destroy( this.gameObject );
}
}

}

Whenever the type property of this Projectile is set, SetType() will be called, and the Projectile will automatically set its own color based on the WeaponDefinitions in Main.

Using a Function Delegate to Fire

Before continuing, read the "Function Delegates" section of Appendix B.

In this game prototype, the Hero class will have a function delegate fireDelegate that is called to fire all weapons, and each Weapon attached to it will add its individual Fire() target method to fireDelegate.

Add the following bold code to the Hero class:

public class Hero : MonoBehaviour {
...
public Bounds bounds;

// Declare a new delegate type WeaponFireDelegate
public delegate void WeaponFireDelegate();
// Create a WeaponFireDelegate field named fireDelegate.
public WeaponFireDelegate fireDelegate;

void Awake() {
...
}

void Update () {
...
// Rotate the ship to make it feel more dynamic
transform.rotation = Quaternion.Euler(yAxis*pitchMult,xAxis*rollMult,0);

// Use the fireDelegate to fire Weapons
// First, make sure the Axis("Jump") button is pressed
// Then ensure that fireDelegate isn't null to avoid an error
if (Input.GetAxis("Jump") == 1 && fireDelegate != null) { // 1
fireDelegate();
}
}
...
}

1. If fireDelegate is called when it has no methods assigned to it, it will throw an error. To avoid this, fireDelegate != null is tested to see whether it is null before calling it.

Open the Weapon C# script in MonoDevelop and add the following code:

public class Weapon : MonoBehaviour {
static public Transform PROJECTILE_ANCHOR;

public bool ____________________;
[SerializeField]
private WeaponType _type = WeaponType.none;

public WeaponDefinition def;
public GameObject collar;
public float lastShot; // Time last shot was fired

void Start() {
collar = transform.Find("Collar").gameObject;
// Call SetType() properly for the default _type
SetType( _type );

if (PROJECTILE_ANCHOR == null) {
GameObject go = new GameObject("_Projectile_Anchor");
PROJECTILE_ANCHOR = go.transform;
}
// Find the fireDelegate of the parent
GameObject parentGO = transform.parent.gameObject;
if (parentGO.tag == "Hero") {
Hero.S.fireDelegate += Fire;
}
}

public WeaponType type {
get { return( _type ); }
set { SetType( value ); }
}

public void SetType( WeaponType wt ) {
_type = wt;
if (type == WeaponType.none) {
this.gameObject.SetActive(false);
return;
} else {
this.gameObject.SetActive(true);
}
def = Main.GetWeaponDefinition(_type);
collar.renderer.material.color = def.color;
lastShot = 0; // You can always fire immediately after _type is set.
}

public void Fire() {
// If this.gameObject is inactive, return
if (!gameObject.activeInHierarchy) return;
// If it hasn't been enough time between shots, return
if (Time.time - lastShot < def.delayBetweenShots) {
return;
}
Projectile p;
switch (type) {
case WeaponType.blaster:
p = MakeProjectile();
p.rigidbody.velocity = Vector3.up * def.velocity;
break;

case WeaponType.spread:
p = MakeProjectile();
p.rigidbody.velocity = Vector3.up * def.velocity;
p = MakeProjectile();
p.rigidbody.velocity = new Vector3( -.2f, 0.9f, 0 ) * def.velocity;
p = MakeProjectile();
p.rigidbody.velocity = new Vector3( .2f, 0.9f, 0 ) * def.velocity;
break;

}
}

public Projectile MakeProjectile() {
GameObject go = Instantiate( def.projectilePrefab ) as GameObject;
if ( transform.parent.gameObject.tag == "Hero" ) {
go.tag = "ProjectileHero";
go.layer = LayerMask.NameToLayer("ProjectileHero");
} else {
go.tag = "ProjectileEnemy";
go.layer = LayerMask.NameToLayer("ProjectileEnemy");
}
go.transform.position = collar.transform.position;
go.transform.parent = PROJECTILE_ANCHOR;
Projectile p = go.GetComponent<Projectile>();
p.type = type;
lastShot = Time.time;
return( p );

}
}

Most of this code should make sense to you. Note that the various kinds of projectiles and weapons are handled with a switch statement inside of the Fire() method.

Now, it's important to make projectiles actually damage enemies. Open the Enemy C# script in MonoDevelop and add the following OnCollisionEnter() method:

public class Enemy : MonoBehaviour {
...
void CheckOffscreen() {
...
}

void OnCollisionEnter( Collision coll ) {
GameObject other = coll.gameObject;
switch (other.tag) {
case "ProjectileHero":
Projectile p = other.GetComponent<Projectile>();
// Enemies don't take damage unless they're onscreen
// This stops the player from shooting them before they are visible
bounds.center = transform.position + boundsCenterOffset;
if (bounds.extents == Vector3.zero || Utils.ScreenBoundsCheck(bounds, BoundsTest.offScreen) != Vector3.zero) {
Destroy(other);
break;
}
// Hurt this Enemy
// Get the damage amount from the Projectile.type & Main.W_DEFS
health -= Main.W_DEFS[p.type].damageOnHit;
if (health <= 0) {
// Destroy this Enemy
Destroy(this.gameObject);
}
Destroy(other);
break;
}
}
}

Now when you play the scene, it is possible to destroy an Enemy, but each one takes 10 shots to take down, and it's difficult to tell that they're being damaged. We will add code that makes an Enemy blink red for a couple of frames every time it is hit, but to do so, we're going to need to have access to all the materials of all the children of each Enemy. This sounds like something that may be useful in later prototypes, so we will add it to the Utils script. Open the Utils script in MonoDevelop and add the following static method to achieve this:

public class Utils : MonoBehaviour {

//============================ Bounds Functions ==============================\\
...

//============================ Transform Functions ===========================\\

...
public static GameObject FindTaggedParent(Transform t) {
return( FindTaggedParent( t.gameObject ) );
}

}

//=========================== Materials Functions ============================\\

// Returns a list of all Materials on this GameObject or its children
static public Material[] GetAllMaterials( GameObject go ) {
List<Material> mats = new List<Material>();
if (go.renderer != null) {
mats.Add(go.renderer.material);
}
foreach( Transform t in go.transform ) {
mats.AddRange( GetAllMaterials( t.gameObject ) );
}
return( mats.ToArray() );
}
}

Now, add the following bold code to the Enemy class:

public class Enemy : MonoBehaviour {
...
public int score = 100; // Points earned for destroying this

public int showDamageForFrames = 2; // # frames to show damage

public bool ________________;

public Color[] originalColors;
public Material[] materials;// All the Materials of this & its children
public int remainingDamageFrames = 0; // Damage frames left

public Bounds bounds; // The Bounds of this and its children

void Awake() {
materials = Utils.GetAllMaterials( gameObject );
originalColors = new Color[materials.Length];
for (int i=0; i<materials.Length; i++) {
originalColors[i] = materials[i].color;
}
InvokeRepeating( "CheckOffscreen", 0f, 2f );
}

// Update is called once per frame
void Update() {
Move();
if (remainingDamageFrames>0) {
remainingDamageFrames--;
if (remainingDamageFrames == 0) {
UnShowDamage();
}
}
}

void OnCollisionEnter( Collision coll ) {
GameObject other = coll.gameObject;
switch (other.tag) {
case "ProjectileHero":
...
// Hurt this Enemy
ShowDamage();
// Get the damage amount from the Projectile.type & Main.W_DEFS
...
break;
}
}

void ShowDamage() {
foreach (Material m in materials) {
m.color = Color.red;
}
remainingDamageFrames = showDamageForFrames;
}
void UnShowDamage() {
for ( int i=0; i<materials.Length; i++ ) {
materials[i].color = originalColors[i];
}
}

}

Now, when an Enemy is struck by a projectile from the _Hero, it will turn entirely red for showDamageForFrames frames by setting the color of all materials to red and setting remainingDamageFrames to showDamageForFrames. Each update, if remainingDamageFramesis greater than 0, it is decremented until it reaches 0, at which time, the enemy ship and children revert to their original colors.

Now it's possible to see that the player is damaging the ship, but it still takes many hits to destroy one. Let's make some power-ups that will increase the power and number of the player's weapons.

Adding Power-Ups

At this point, there will be three power-ups in the game:

Image blaster [B]: If the player weapon type is not blaster, switches to blaster and resets to 1 gun. If the player weapon type is already blaster, increases the number of guns.

Image spread [S]: If the player weapon type is not spread, switches to spread and resets to 1 gun. If the player weapon type is already spread, increases the number of guns.

Image shield [O]: Increases the player's shieldLevel by 1.

Artwork for Power-Ups

The power-ups will be made of a letter rendered as 3D text with a spinning cube behind it. (You can see some of them in Figure 30.1 at the beginning of the chapter.) To make the power-ups, complete these steps:

1. Create a new 3D text (GameObject > Create Other > 3D Text from the menu bar). Name it PowerUp and give it the following settings:

Image

2. Create a cube that is a child of PowerUp as described in the preceding settings.

3. Select the PowerUp.

4. Set the Text Mesh component properties of PowerUp to those shown in Figure 30.12.

5. Add a Rigidbody component to PowerUp (Component > Physics > Rigidbody) and set it as shown in Figure 30.12.

6. Set both the tag and the layer of PowerUp to PowerUp. When asked, click Yes, change children.

Next, you will create a custom material for the PowerUp cube, as follows:

1. Create a new Material named Mat PowerUp.

2. Drag it on to the cube that is a child of PowerUp.

3. Select the cube that is a child of PowerUp.

4. Set the Shader of Mat PowerUp to ProtoTools > UnlitAlpha.

5. Click the Select button at the bottom right of the texture for Mat PowerUp and choose the texture named PowerUp from the Assets tab.

6. Set the main color of Mat PowerUp to cyan (a light blue that is RGBA:[ 0,255,255,255 ]), and you can see how the PowerUp will look when colored.

7. Set the Box Collider of cube to be a trigger (check the box next to Is Trigger).

Double-check that all the settings for PowerUp and its child Cube match those in Figure 30.12 and save your scene.

Image

Figure 30.12 Settings for PowerUp and its child Cube prior to attaching any scripts

PowerUp Code

Now create a new C# script named PowerUp and assign it to the PowerUp GameObject in the Hierarchy. Open the PowerUp script in MonoDevelop and enter the following code:

using UnityEngine;
using System.Collections;

public class PowerUp : MonoBehaviour {
// This is an unusual but handy use of Vector2s. x holds a min value
// and y a max value for a Random.Range() that will be called later
public Vector2 rotMinMax = new Vector2(15,90);
public Vector2 driftMinMax = new Vector2(.25f,2);
public float lifeTime = 6f; // Seconds the PowerUp exists
public float fadeTime = 4f; // Seconds it will then fade
public bool ________________;
public WeaponType type; // The type of the PowerUp
public GameObject cube; // Reference to the Cube child
public TextMesh letter; // Reference to the TextMesh
public Vector3 rotPerSecond; // Euler rotation speed
public float birthTime;

void Awake() {
// Find the Cube reference
cube = transform.Find("Cube").gameObject;
// Find the TextMesh
letter = GetComponent<TextMesh>();

// Set a random velocity
Vector3 vel = Random.onUnitSphere; // Get Random XYZ velocity
// Random.onUnitSphere gives you a vector point that is somewhere on
// the surface of the sphere with a radius of 1m around the origin
vel.z = 0; // Flatten the vel to the XY plane
vel.Normalize(); // Make the length of the vel 1
// Normalizing a Vector3 makes it length 1m
vel *= Random.Range(driftMinMax.x, driftMinMax.y);
// Above sets the velocity length to something between the x and y
// values of driftMinMax
rigidbody.velocity = vel;

// Set the rotation of this GameObject to R:[0,0,0]
transform.rotation = Quaternion.identity;
// Quaternion.identity is equal to no rotation.

// Set up the rotPerSecond for the Cube child using rotMinMax x & y
rotPerSecond = new Vector3( Random.Range(rotMinMax.x,rotMinMax.y),
Random.Range(rotMinMax.x,rotMinMax.y),
Random.Range(rotMinMax.x,rotMinMax.y) );

// CheckOffscreen() every 2 seconds
InvokeRepeating( "CheckOffscreen", 2f, 2f );

birthTime = Time.time;
}

void Update () {
// Manually rotate the Cube child every Update()
// Multiplying it by Time.time causes the rotation to be time-based
cube.transform.rotation = Quaternion.Euler( rotPerSecond*Time.time );

// Fade out the PowerUp over time
// Given the default values, a PowerUp will exist for 10 seconds
// and then fade out over 4 seconds.
float u = (Time.time - (birthTime+lifeTime)) / fadeTime;
// For lifeTime seconds, u will be <= 0. Then it will transition to 1
// over fadeTime seconds.
// If u >= 1, destroy this PowerUp
if (u >= 1) {
Destroy( this.gameObject );
return;
}
// Use u to determine the alpha value of the Cube & Letter
if (u>0) {
Color c = cube.renderer.material.color;
c.a = 1f-u;
cube.renderer.material.color = c;
// Fade the Letter too, just not as much
c = letter.color;
c.a = 1f - (u*0.5f);
letter.color = c;
}
}

// This SetType() differs from those on Weapon and Projectile
public void SetType( WeaponType wt ) {
// Grab the WeaponDefinition from Main
WeaponDefinition def = Main.GetWeaponDefinition( wt );
// Set the color of the Cube child
cube.renderer.material.color = def.color;
//letter.color = def.color; // We could colorize the letter too
letter.text = def.letter; // Set the letter that is shown
type = wt; // Finally actually set the type
}

public void AbsorbedBy( GameObject target ) {
// This function is called by the Hero class when a PowerUp is collected
// We could tween into the target and shrink in size,
// but for now, just destroy this.gameObject
Destroy( this.gameObject );
}

void CheckOffscreen() {
// If the PowerUp has drifted entirely off screen...
if ( Utils.ScreenBoundsCheck( cube.collider.bounds, BoundsTest.offScreen) != Vector3.zero ) {
// ...then destroy this GameObject
Destroy( this.gameObject );
}
}
}

When you press Play, you should see the power-up drifting and rotating. If you fly _Hero into the power-up, you will get the console message "Triggered: Cube," letting you know that the Trigger Collider on the PowerUp cube is working properly.

Drag the PowerUp GameObject from the Hierarchy into the _Prefabs folder in the Project pane to make it into a prefab. Delete the remaining PowerUp instance from the Hierarchy.

Now, make the following changes to the Hero C# script to enable the Hero to collide with and collect power-ups:

public class Hero : MonoBehaviour {
...
private float _shieldLevel = 1;

// Weapon fields
public Weapon[] weapons;

public bool ____________________________;

void Awake() {
S = this; // Set the Singleton
bounds = Utils.CombineBoundsOfChildren(this.gameObject);

// Reset the weapons to start _Hero with 1 blaster
ClearWeapons();
weapons[0].SetType(WeaponType.blaster);
}

void OnTriggerEnter(Collider other) {
...
if (go != null) {
...

if (go.tag == "Enemy") {
// If the shield was triggered by an enemy
// Decrease the level of the shield by 1
shieldLevel--;
// Destroy the enemy
Destroy(go);
} else if (go.tag == "PowerUp") {
// If the shield was triggerd by a PowerUp
AbsorbPowerUp(go);
} else {
print("Triggered: "+go.name); // Move this line here!
}
}
...
}

public void AbsorbPowerUp( GameObject go ) {
PowerUp pu = go.GetComponent<PowerUp>();
switch (pu.type) {
case WeaponType.shield: // If it's the shield
shieldLevel++;
break;

default: // If it's any Weapon PowerUp
// Check the current weapon type
if (pu.type == weapons[0].type) {
// then increase the number of weapons of this type
Weapon w = GetEmptyWeaponSlot(); // Find an available weapon
if (w != null) {
// Set it to pu.type
w.SetType(pu.type);
}
} else {
// If this is a different weapon
ClearWeapons();
weapons[0].SetType(pu.type);
}
break;
}
pu.AbsorbedBy( this.gameObject );
}

Weapon GetEmptyWeaponSlot() {
for (int i=0; i<weapons.Length; i++) {
if ( weapons[i].type == WeaponType.none ) {
return( weapons[i] );
}
}
return( null );
}

void ClearWeapons() {
foreach (Weapon w in weapons) {
w.SetType(WeaponType.none);
}
}

}

Now that the code is set up, you need to make a couple of changes to _Hero in Unity. Open the disclosure triangle next to the GameObject _Hero in the Hierarchy. Select the Weapon child of _Hero. Press Command-D (or Control+D on Windows) four times to make four duplicates of Weapon. These should all still be children of _Hero. Rename the five weapons Weapon_0 through Weapon_4 and configure their transforms as follows:

Image

Next, select _Hero and open the disclosure triangle for the weapons field in the Hero (Script) component Inspector. Set the Size of weapons to 5 and assign Weapon_0 through Weapon_4 to the five Weapon slots in order (either by dragging them in from the Hierarchy or by clicking the target to the right of the Weapon slot and selecting each Weapon_# from the Scene tab). Figure 30.13 shows the final setup.

Image

Figure 30.13 The _Hero ship showing five Weapons as children and assigned to the weapons field

Resolving Race Conditions in Code

Now, when you try to play the scene as we've created it, you may encounter an error message in the Console pane. It's also possible that you will not get this error. In this code, I've tried to intentionally create a race condition to show you how to resolve them. A race condition occurs when one piece of code must be executed before another piece of code, but it's possible that they will execute in the wrong order. The two pieces of code end up racing against each other. The thing about race conditions is that they're unpredictable, so you might not get the error that I tried to create. Regardless, please read this section. Race conditions are an important kind of error that you should understand. The error you may encounter is as follows:

NullReferenceException: Object reference not set to an instance of an object Main.GetWeaponDefinition (WeaponType wt) (at Assets/__Scripts/Main.cs:38) Weapon.SetType (WeaponType wt) (at Assets/__Scripts/Weapon.cs:77) Hero.Awake () (at Assets/__Scripts/Hero.cs:35)

If you double-click the error message, it should take you to line 38 of Main.cs. (Your line number might differ slightly.) Line 38 is:

if (W_DEFS.ContainsKey(wt)) {

Let's use the debugger to learn more about what's causing the error. (Please do this even if you're not getting the error.) Add a breakpoint next to this line in Main.cs and attach the debugger to Unity (by clicking the Play icon in the top-left corner of the MonoDevelop window or selecting Run > Attach to Process from the MonoDevelop menu bar). If you need a refresher on the debugger, reread Chapter 24. Unfortunately, in this case, you will need to have the debugger attached when the scene first starts playing, so the trick described in Chapter 24 where you start the game paused and then attach the debugger later won't work for these bugs.

When you run the project (in Unity) with the debugger attached, it will freeze on your line 38 breakpoint immediately before executing that line. We know that something's wrong with this line, and as a NullReferenceException, we know that the code is trying to access some variable that isn't yet defined. Let's look at each variable and see what's happening.

1. Open the Watch panel in MonoDevelop ( View > Debug Windows > Watch from the menu bar; there should be already a check mark next to it, and selecting it again will bring the Watch panel to the front).

2. The two variables used in this line are W_DEFS (a static variable of the Main class) and wt (a local variable of the method GetWeaponDefinition()).

3. Type each of these into a line of the Watch window, and you'll be able to see their individual values.

4. As expected, W_DEFS isn't defined (its value is null). (That is, if you're experiencing the race condition error on your machine.) But we know that W_DEFS is properly defined in Main.Awake(). You can see the code that does so just a few lines above. The only way that W_DEFScould not be defined is if Main.Awake() hasn't run yet.

This is the race condition. Main.Awake() defines W_DEF, and Hero.Awake() is trying to use that value. We know that Awake() is called on each GameObject as it comes into being, but it is unclear in what order they are called. I believe that it probably happens in the order that the objects are listed in the Hierarchy, but I'm not certain of that. It's possible that your Awake() methods may be called in a different order than mine.

This is the major problem with race conditions. The two Awake() functions are racing against each other. When one is called first, your code works fine, but when the other is called first, everything breaks. Regardless of whether your code happens to be working, this is an issue that you need to resolve, because even on the same computer, the two Awake() functions could be called in different orders from one time to the next..

This is one reason that there are both Awake() and Start() methods in Unity. Awake() is called immediately when a GameObject is instantiated, while Start() is called immediately before the first Update() that the GameObject ever receives. This can be a difference of several milliseconds, which for a computer program is a very long time. If you have a number of objects in your scene, you can be guaranteed that Awake() will be called on all of them before Start() is called on any of them. Awake() will always happen before Start().

Knowing this, take a look back at the original error. If you look at the Call Stack pane in MonoDevelop (View > Debug Windows > Call Stack from the menu bar), it looks like Hero.Awake() on line 35 called Weapon.SetType(), which in turn calledMain.GetWeaponDefinition(). To start fixing this issue, we will choose to delay the call from Hero.Awake() by moving it into Hero.Start(). Make the following changes to the Hero C# script. You should click the Stop sign in the MonoDevelop debugger (or select Run > Stop from the menu bar) as well as stop playback in Unity before changing the Hero script code:

public class Hero : MonoBehaviour {
...

void Awake() {
S = this; // Set the Singleton
bounds = Utils.CombineBoundsOfChildren(this.gameObject);
}

void Start() {
// Reset the weapons to start _Hero with 1 blaster
ClearWeapons();
weapons[0].SetType(WeaponType.blaster);
}

...
}

However after doing so, playing the project will expose yet another race condition error!

UnassignedReferenceException: The variable collar of Weapon has not been assigned. You probably need to assign the collar variable of the Weapon script in the Inspector. Weapon.SetType (WeaponType wt) (at Assets/__Scripts/Weapon.cs:78) Hero.Start () (at Assets/__Scripts/Hero.cs:38)

Attach the MonoDevelop debugger to the Unity process again to get more information on this error. Place a breakpoint on line 78 of Weapon.cs and then press Play in Unity. Because the Start() functions are called at different times, I sometimes saw the code first stop on line 38 of Hero.cs (where the breakpoint still remains from the previous debug) and sometimes saw it first stop on line 78 of Weapon. This is happening because both Hero.Start() and Weapon.Start() call Weapon.SetType(). If Weapon.Start() happens to be called before Hero.Start(), this is fine, but if Hero.Start() is called first, we get an error due to the race condition. The issue here is that all Weapons need to define Weapon.collar before Hero.Start() is run. To resolve this, move the definition of collar from the Start() method to an Awake() method in the the Weapon C# script.

void Awake() {
collar = transform.Find("Collar").gameObject;
}

void Start() {
// Call SetType() properly for the default _type
SetType( _type );

...
}

Now, the race conditions should finally be resolved. Weapon.Awake() will define collar before either Weapon.Start() or Hero.Start() are called. Also, Main.Awake() will set the value of Main.W_DEFS before Hero.Start() is called. Race conditions are a common error for new game developers to step into, and it's important to be able to recognize when you may be encountering one. This is why I have lead you into this one and shown you how to discover and resolve the problem.

Making Enemies Drop Power-Ups

Getting back to the power-ups. Let's make enemies have the potential to drop a random power-up when they are destroyed. This gives the player a lot more incentive to try to destroy enemies rather than just avoid them, and it gives the player a path to improving her ship.

Add the following code to the Enemy and Main C# scripts:

public class Enemy : MonoBehaviour {
...
public int showDamageForFrames = 2; // # frames to show damage
public float powerUpDropChance = 1f; // Chance to drop a power-up

public bool ________________;
...
void OnCollisionEnter( Collision coll ) {
...
case "ProjectileHero":
...
if (health <= 0) {
// Tell the Main singleton that this ship has been destroyed
Main.S.ShipDestroyed( this );
// Destroy this Enemy
Destroy(this.gameObject);
}
...
}
}
...
}

public class Main : MonoBehaviour {
...
public WeaponDefinition[] weaponDefinitions;
public GameObject prefabPowerUp;
public WeaponType[] powerUpFrequency = new WeaponType[] {
WeaponType.blaster, WeaponType.blaster,
WeaponType.spread,
WeaponType.shield };

public bool ________________;
...

public void ShipDestroyed( Enemy e ) {
// Potentially generate a PowerUp
if (Random.value <= e.powerUpDropChance) {
// Random.value generates a value between 0 & 1 (though never == 1)
// If the e.powerUpDropChance is 0.50f, a PowerUp will be generated
// 50% of the time. For testing, it's now set to 1f.

// Choose which PowerUp to pick
// Pick one from the possibilities in powerUpFrequency
int ndx = Random.Range(0,powerUpFrequency.Length);
WeaponType puType = powerUpFrequency[ndx];

// Spawn a PowerUp
GameObject go = Instantiate( prefabPowerUp ) as GameObject;
PowerUp pu = go.GetComponent<PowerUp>();
// Set it to the proper WeaponType
pu.SetType( puType );

// Set it to the position of the destroyed ship
pu.transform.position = e.transform.position;
}
}

}

Before this code will work, you need to select _MainCamera in the Unity Hierarchy and set the prefabPowerUp field of the Main Script component to be the PowerUp prefab in the _Prefabs folder of the Project pane. powerUpFrequency should already be set in the Inspector, but just in case, Figure 30.14 shows the correct settings. Note that enums appear in the Unity Inspector as pop-up menus.

Image

Figure 30.14 prefabPowerUp and powerUpFrequency on the Main (Script) component of _MainCamera

Now play the scene and destroy some enemies. They should drop power-ups that will now improve your ship!

You should notice over time that the blaster [B] power-up is more common than spread [S] or shield [O]. This is because there are two occurrences of blaster in powerUpFrequency and only one each of spread and shield. By adjusting the relative numbers of occurrences of each of these in powerUpFrequency, you can determine the chance that each will be chosen relative to the others. This same trick could also be used to set the frequency of different types of enemies spawning by assigning some enemies to the prefabEnemies array more times than other enemy types.

Programming Other Enemies

Now that the core elements of the game are each working, it's time to expand the different offerings of enemies. Create new C# scripts named Enemy_1, Enemy_2, Enemy_3, and Enemy_4 and assign them each to their respective Enemy_# prefab in the Project pane.

Enemy_1

Open Enemy_1 scripts in MonoDevelop and enter the following code:

using UnityEngine;
using System.Collections;

// Enemy_1 extends the Enemy class
public class Enemy_1 : Enemy {
// Because Enemy_1 extends Enemy, the _____ bool won't work // 1
// the same way in the Inspector pane. :/

// # seconds for a full sine wave
public float waveFrequency = 2;
// sine wave width in meters
public float waveWidth = 4;
public float waveRotY = 45;

private float x0 = -12345; // The initial x value of pos
private float birthTime;

void Start() {
// Set x0 to the initial x position of Enemy_1
// This works fine because the position will have already
// been set by Main.SpawnEnemy() before Start() runs
// (though Awake() would have been too early!).
// This is also good because there is no Start() method
// on Enemy.
x0 = pos.x;

birthTime = Time.time;
}

// Override the Move function on Enemy
public override void Move() { // 2
// Because pos is a property, you can't directly set pos.x
// so get the pos as an editable Vector3
Vector3 tempPos = pos;
// theta adjusts based on time
float age = Time.time - birthTime;
float theta = Mathf.PI * 2 * age / waveFrequency;
float sin = Mathf.Sin(theta);
tempPos.x = x0 + waveWidth * sin;
pos = tempPos;

// rotate a bit about y
Vector3 rot = new Vector3(0, sin*waveRotY, 0);
this.transform.rotation = Quaternion.Euler(rot);

// base.Move() still handles the movement down in y
base.Move(); // 3
}

}

1. The bool ________________ that is used to divide the elements you should set in the Inspector from those you should not won't work that way in these subclasses of Enemy because when the Inspector sees a subclass like this, it will first list all the public fields of the superclass and then all the public fields of the subclass. This will place waveFrequency, waveWidth, and waveRotY below the line, even though you should feel free to manipulate them in the Inspector.

2. Because the method Move() is marked as a virtual method in the Enemy superclass, we are able to override it here and replace it with another function.

3. base.Move() calls the Move() function on the superclass Enemy.

Back in Unity, select _MainCamera in the Hierarchy and change Element 0 of prefabEnemies from Enemy_0 to Enemy_1 (which is the Enemy_1 prefab in the Project pane) in the Main (Script) component. Now, when you press Play, the Enemy_1 ship will appear instead of Enemy_0, and it will move in a wave.


Tip

Sphere Colliders Only Scale Uniformly You might have noticed that the collision with Enemy_1 actually occurs before the projectile reaches the wing. If you select Enemy_1 in the Project pane and drag an instance into the scene, you will see that the green collider spheres around Enemy_1 don't scale to match the flat ellipse of the wing. This isn't a huge problem, but it is something to be aware of. A Sphere Collider will scale with the largest single component of scale in the transform. (In this case, because wing has a Scale.x of 6, the Sphere Collider scales up to that.)

If you want, you can try other types of colliders to see whether one of them will scale to match the wing more accurately. A Box Collider will scale nonuniformly. You can also approximate one direction being much longer than the others with a Capsule Collider. A Mesh Collider will match the scaling most exactly, but Mesh Colliders are much slower than other types. This shouldn't be a problem on a modern high-performance PC, but Mesh Colliders are often too slow for mobile platforms like iOS or Android.

If you choose to give Enemy_1 a Box Collider or Mesh Collider, then when it rotates about the y axis, it will move the edges of the wing out of the XY (that is, z=0) plane. This is why the ProjectileHero prefab has a Box Collider Size.z of 10 (to make sure that it can hit the wingtips of Enemy_1 even if they are not in the XY plane).


Preparing for the Other Enemies

The remaining enemies make use of linear interpolation, an important development concept that is described in Appendix B. You saw a very simple interpolation in Chapter 29, "Prototype 2: Mission Demolition," but these will be a bit more interesting. Take a moment to read the "Interpolation" section of Appendix B, before tackling the remaining enemies.

Enemy_2

Enemy_2 will move via a linear interpolation that is heavily eased by a sine wave. It will rush in from the side of the screen, slow, reverse direction for a bit, slow, and then fly off the screen along its initial velocity. Only two points will be used in this interpolation, but the u value will be drastically curved by a sine wave. The easing function for the u value of Enemy_2 will be along the lines of

u = u + 0.6 * Sin(2π * u)

This is one of the easing functions depicted in the "Interpolation" section of Appendix B.

Open the Enemy_2 C# script and enter the following code. After you have the code working, you're welcome to adjust the easing curve and see how it affects the motion.

using UnityEngine;
using System.Collections;

public class Enemy_2 : Enemy {
// Enemy_2 uses a Sin wave to modify a 2-point linear interpolation
public Vector3[] points;
public float birthTime;
public float lifeTime = 10;
// Determines how much the Sine wave will affect movement
public float sinEccentricity = 0.6f;

void Start () {
// Initialize the points
points = new Vector3[2];

// Find Utils.camBounds
Vector3 cbMin = Utils.camBounds.min;
Vector3 cbMax = Utils.camBounds.max;

Vector3 v = Vector3.zero;
// Pick any point on the left side of the screen
v.x = cbMin.x - Main.S.enemySpawnPadding;
v.y = Random.Range( cbMin.y, cbMax.y );
points[0] = v;

// Pick any point on the right side of the screen
v = Vector3.zero;
v.x = cbMax.x + Main.S.enemySpawnPadding;
v.y = Random.Range( cbMin.y, cbMax.y );
points[1] = v;

// Possibly swap sides
if (Random.value < 0.5f) {
// Setting the .x of each point to its negative will move it to the
// other side of the screen
points[0].x *= -1;
points[1].x *= -1;
}

// Set the birthTime to the current time
birthTime = Time.time;
}

public override void Move() {
// Bézier curves work based on a u value between 0 & 1
float u = (Time.time - birthTime) / lifeTime;

// If u>1, then it has been longer than lifeTime since birthTime
if (u > 1) {
// This Enemy_2 has finished its life
Destroy( this.gameObject );
return;
}

// Adjust u by adding an easing curve based on a Sine wave
u = u + sinEccentricity*(Mathf.Sin(u*Mathf.PI*2));

// Interpolate the two linear interpolation points
pos = (1-u)*points[0] + u*points[1];
}
}

Swap the Enemy_2 prefab into the Element 0 slot of Main.S.prefabEnemies using the _MainCamera Inspector and press Play. As you can see the easing function causes each Enemy_2 to have very smooth movement that waves between the points it has selected on either side of the screen.

Enemy_3

Enemy_3 will use a Bézier curve to swoop down from above, slow, and fly back up off the top of the screen. For this example, we will use a simple version of the three-point Bézier curve function. In the "Interpolation" section of Appendix B you can find a recursive version of the Bézier curve function that can use any number of points (not just three).

Open the Enemy_3 script and enter the following code:

using UnityEngine;
using System.Collections;

// Enemy_3 extends Enemy
public class Enemy_3 : Enemy {

// Enemy_3 will move following a Bezier curve, which is a linear
// interpolation between more than two points.
public Vector3[] points;
public float birthTime;
public float lifeTime = 10;

// Again, Start works well because it is not used by Enemy
void Start () {
points = new Vector3[3]; // Initialize points

// The start position has already been set by Main.SpawnEnemy()
points[0] = pos;

// Set xMin and xMax the same way that Main.SpawnEnemy() does
float xMin = Utils.camBounds.min.x+Main.S.enemySpawnPadding;
float xMax = Utils.camBounds.max.x-Main.S.enemySpawnPadding;

Vector3 v;
// Pick a random middle position in the bottom half of the screen
v = Vector3.zero;
v.x = Random.Range( xMin, xMax );
v.y = Random.Range( Utils.camBounds.min.y, 0 );
points[1] = v;

// Pick a random final position above the top of the screen
v = Vector3.zero;
v.y = pos.y;
v.x = Random.Range( xMin, xMax );
points[2] = v;

// Set the birthTime to the current time
birthTime = Time.time;
}

public override void Move() {
// Bezier curves work based on a u value between 0 & 1
float u = (Time.time - birthTime) / lifeTime;

if (u > 1) {
// This Enemy_3 has finished its life
Destroy( this.gameObject );
return;
}

// Interpolate the three Bezier curve points
Vector3 p01, p12;
p01 = (1-u)*points[0] + u*points[1];
p12 = (1-u)*points[1] + u*points[2];
pos = (1-u)*p01 + u*p12;

}
}

Now try swapping Enemy_3 into the Element 0 of prefabEnemies on _MainCamera. These have a very different movement than the previous enemies. After playing for a bit, you'll notice a couple of things about Bézier curves:

1. Even though the midpoint is at or below the bottom of the screen, no Enemy_3 ever gets that far down. That is because a Bézier curve touches both the start and end points but is only influenced by the midpoint.

2. Enemy_3 slows down a lot in the middle of the curve. This is also a feature of Bézier curves. If you want, you can correct this by adding the following bold line to the Enemy_3 Move() method just before the curve points are interpolated. This will add easing to the Enemy_3 movement that will speed up the middle of the curve to make the movement feel more consistent:

Vector3 p01, p12;
u = u - 0.2f*Mathf.Sin(u*Mathf.PI*2);
p01 = (1-u)*points[0] + u*points[1];

Enemy_4

As somewhat of a boss type, Enemy_4 will have more health than other Enemy types and will have destructible parts (rather than all the parts being destroyed at the same time). It will also stay on screen, moving from one position to another, until the player destroys it completely.

Collider Modifications

Before getting into code issues, you need to make a few adjustments to the colliders of Enemy_4. First, drag an instance of Enemy_4 into the Hierarchy and make sure that it's positioned away from other GameObjects in the scene.

Open the disclosure triangle next to Enemy_4 in the Hierarchy and select Enemy_4.Fuselage. Replace the Sphere Collider with a Capsule Collider by selecting Component > Physics > Capsule Collider from the menu bar. If Unity asks you, choose to replace the Sphere Collider with the Capsule Collider, if it doesn't ask you, you will need to manually remove the Sphere Collider. Set the Capsule Collider as follows in the Fuselage Inspector:

Image

Feel free to play with the values somewhat to see how they affect things. As you can see, the Capsule Collider is a much better approximation of Fuselage than the Sphere Collider was.

Now, select Wing_L in the Hierarchy and replace its Sphere Collider with a Capsule Collider as well. The settings for this collider are as follows:

Image

The Direction setting chooses which is the long axis of the capsule. This is determined in local coordinates, so the Capsule Collider height of 5 along the X-axis matches the Transform scale of 5 in the X dimension. The radius of 0.1 states that the radius should be 1/10th of the height (5 * 1/10th = 0.5, which is the Z Scale dimension). You can see that the capsule does not perfectly match the wing, but it is a much better approximation than a sphere.

Select Wing_R, replace its collider with a Capsule Collider, and give that collider the same settings as used on Wing_L. Once these changes have been made, click the Prefab > Apply button at the top of the Inspector pane to commit these changes to the Enemy_4 prefab in the Project pane. To double-check that this worked successfully, drag a second instance of the Enemy_4 prefab into the Hierarchy pane and check to make sure that the colliders all look correct. Once this is done, delete both instances of Enemy_4 from the Hierarchy pane.

This same Capsule Collider strategy could also be applied to Enemy_3 if you want.

Movement of Enemy_4

Enemy_4 will start in the standard position off the top of the screen, pick a random point on screen, and move to it over time using a linear interpolation. Each time Enemy_4 reaches the end of an interpolation, it will pick a new point and start moving toward it. Open the Enemy_4 script and input this code:

using UnityEngine;
using System.Collections;

public class Enemy_4 : Enemy {
// Enemy_4 will start offscreen and then pick a random point on screen to
// move to. Once it has arrived, it will pick another random point and
// continue until the player has shot it down.

public Vector3[] points; // Stores the p0 & p1 for interpolation
public float timeStart; // Birth time for this Enemy_4
public float duration = 4; // Duration of movement

void Start () {
points = new Vector3[2];
// There is already an initial position chosen by Main.SpawnEnemy()
// so add it to points as the initial p0 & p1
points[0] = pos;
points[1] = pos;

InitMovement();
}

void InitMovement() {
// Pick a new point to move to that is on screen
Vector3 p1 = Vector3.zero;
float esp = Main.S.enemySpawnPadding;
Bounds cBounds = Utils.camBounds;
p1.x = Random.Range(cBounds.min.x + esp, cBounds.max.x - esp);
p1.y = Random.Range(cBounds.min.y + esp, cBounds.max.y - esp);

points[0] = points[1]; // Shift points[1] to points[0]
points[1] = p1; // Add p1 as points[1]

// Reset the time
timeStart = Time.time;
}

public override void Move () {
// This completely overrides Enemy.Move() with a linear interpolation

float u = (Time.time-timeStart)/duration;
if (u>=1) { // if u >=1...
InitMovement(); // ...then initialize movement to a new point
u=0;
}

u = 1 - Mathf.Pow( 1-u, 2 ); // Apply Ease Out easing to u

pos = (1-u)*points[0] + u*points[1]; // Simple linear interpolation
}
}

Swap the Enemy_4 prefab into the Element 0 slot of Main.S.prefabEnemies using the _MainCamera Inspector and save your scene. Did you remember to save after altering the colliders?

Press Play. You can see that the spawned Enemy_4s stay on screen until you destroy them. However, they're currently just as simple to take down as any of the other enemies. Now we'll break the Enemy_4 ship into four different parts with the central Cockpit protected by the others.

Open the Enemy_4 C# script and start by adding a new serializable class named Part to the top of Enemy_4.cs. Also be sure to add a Part[] array to the Enemy_4 class named parts.

using UnityEngine;
using System.Collections;

// Part is another serializable data storage class just like WeaponDefinition
[System.Serializable]
public class Part {
// These three fields need to be defined in the Inspector pane
public string name; // The name of this part
public float health; // The amount of health this part has
public string[] protectedBy; // The other parts that protect this

// These two fields are set automatically in Start().
// Caching like this makes it faster and easier to find these later
public GameObject go; // The GameObject of this part
public Material mat; // The Material to show damage
}

public class Enemy_4 : Enemy {
...
public float duration = 4; // Duration of movement

public Part[] parts; // The array of ship Parts

void Start() {
...
}
...
}

The Part class will store individual information about the four parts of Enemy_4: Cockpit, Fuselage, Wing_L, and Wing_R.

Switch back to Unity and do the following:

1. Select the Enemy_4 prefab in the Project pane.

2. Expand the disclosure triangle next to parts in the Inspector > Enemy_4 (Script).

3. Enter the settings shown in Figure 30.15. The GameObject go and Material mat of each Part will be set automatically by code.

Image

Figure 30.15 The settings for the Parts array of Enemy_4

As you can see in Figure 30.15, each part has 10 health, and there is a tree of protection. Cockpit is protected by Fuselage, and Fuselage is protected by both Wing_L and Wing_R. Now, switch back to MonoDevelop and make the following additions to the Enemy_4 class to make this protection work:

public class Enemy_4 : Enemy {
...
void Start () {
...
InitMovement();

// Cache GameObject & Material of each Part in parts
Transform t;
foreach(Part prt in parts) {
t = transform.Find(prt.name);
if (t != null) {
prt.go = t.gameObject;
prt.mat = prt.go.renderer.material;
}
}
}

...

public override void Move() {
...
}

// This will override the OnCollisionEnter that is part of Enemy.cs
// Because of the way that MonoBehaviour declares common Unity functions
// like OnCollisionEnter(), the override keyword is not necessary.
void OnCollisionEnter( Collision coll ) {
GameObject other = coll.gameObject;
switch (other.tag) {
case "ProjectileHero":
Projectile p = other.GetComponent<Projectile>();
// Enemies don't take damage unless they're on screen
// This stops the player from shooting them before they are visible
bounds.center = transform.position + boundsCenterOffset;
if (bounds.extents == Vector3.zero || Utils.ScreenBoundsCheck(bounds, BoundsTest.offScreen) != Vector3.zero) {
Destroy(other);
break;
}

// Hurt this Enemy
// Find the GameObject that was hit
// The Collision coll has contacts[], an array of ContactPoints
// Because there was a collision, we're guaranteed that there is at
// least a contacts[0], and ContactPoints have a reference to
// thisCollider, which will be the collider for the part of the
// Enemy_4 that was hit.
GameObject goHit = coll.contacts[0].thisCollider.gameObject;
Part prtHit = FindPart(goHit);
if (prtHit == null) { // If prtHit wasn't found
// ...then it's usually because, very rarely, thisCollider on
// contacts[0] will be the ProjectileHero instead of the ship
// part. If so, just look for otherCollider instead
goHit = coll.contacts[0].otherCollider.gameObject;
prtHit = FindPart(goHit);
}
// Check whether this part is still protected
if (prtHit.protectedBy != null) {
foreach( string s in prtHit.protectedBy ) {
// If one of the protecting parts hasn't been destroyed...
if (!Destroyed(s)) {
// ...then don't damage this part yet
Destroy(other); // Destroy the ProjectileHero
return; // return before causing damage
}
}
}
// It's not protected, so make it take damage
// Get the damage amount from the Projectile.type & Main.W_DEFS
prtHit.health -= Main.W_DEFS[p.type].damageOnHit;
// Show damage on the part
ShowLocalizedDamage(prtHit.mat);
if (prtHit.health <= 0) {
// Instead of Destroying this enemy, disable the damaged part
prtHit.go.SetActive(false);
}
// Check to see if the whole ship is destroyed
bool allDestroyed = true; // Assume it is destroyed
foreach( Part prt in parts ) {
if (!Destroyed(prt)) { // If a part still exists
allDestroyed = false; // ...change allDestroyed to false
break; // and break out of the foreach loop
}
}
if (allDestroyed) { // If it IS completely destroyed
// Tell the Main singleton that this ship has been destroyed
Main.S.ShipDestroyed( this );
// Destroy this Enemy
Destroy(this.gameObject);
}
Destroy(other); // Destroy the ProjectileHero
break;
}
}

// These two functions find a Part in this.parts by name or GameObject
Part FindPart(string n) {
foreach( Part prt in parts ) {
if (prt.name == n) {
return( prt );
}
}
return( null );
}
Part FindPart(GameObject go) {
foreach( Part prt in parts ) {
if (prt.go == go) {
return( prt );
}
}
return( null );
}

// These functions return true if the Part has been destroyed
bool Destroyed(GameObject go) {
return( Destroyed( FindPart(go) ) );
}
bool Destroyed(string n) {
return( Destroyed( FindPart(n) ) );
}
bool Destroyed(Part prt) {
if (prt == null) { // If no real Part was passed in
return(true); // Return true (meaning yes, it was destroyed)
}
// Returns the result of the comparison: prt.health <= 0
// If prt.health is 0 or less, returns true (yes, it was destroyed)
return (prt.health <= 0);
}

// This changes the color of just one Part to red instead of the whole ship
void ShowLocalizedDamage(Material m) {
m.color = Color.red;
remainingDamageFrames = showDamageForFrames;
}
}

Now when you play the scene, you should be overwhelmed by many Enemy_4s, each of which has two wings that protect the fuselage and a fuselage that protects the cockpit. If you want more of a chance against these, you can change the value of Main (Script).enemySpawn PerSecond on the _MainCamera to something lower, which will give you more time between Enemy_4 spawns (though it will also delay the initial spawn).

Adding Particle Effects and Background

After all of that coding, here are a couple of things you can do just for fun to make the game look a little better.

Starfield Background

Create a two-layer starfield background to make things look more like outer space.

Create a quad in the Hierarchy (GameObject > Create Other > Quad). Name it StarfieldBG.

Image

This will place StarfieldBG in the center of the camera's view and fill the view entirely. Now, create a new material named Mat Starfield and set its shader to ProtoTools > UnlitAlpha. Set the texture of Mat Starfield to the Space Texture2D that you imported at the beginning of this tutorial. Now drag Mat Starfield onto StarfieldBG, and you should see a starfield behind your _Hero ship.

Select Mat Starfield in the Project pane and duplicate it (Command-D on Mac or Control+D on PC). Name the new material Mat Starfield Transparent. Select Space_Transparent as the texture for this new material.

Select StarfieldBG in the Hierarchy and duplicate it. Name the duplicate StarfieldFG_0. Drag the Mat Starfield Transparent material onto StarfieldFG_0 and set its transform.

Image

Now if you drag StarfieldFG_0 around a bit, you'll see that it moves some stars in the foreground past stars in the background, creating a nifty parallax scrolling effect. Now duplicate Starfield_FG_0 and name the duplicate Starfield_FG_1. You will need two copies of the foreground for the scrolling trick that we're going to employ.

Create a new C# script named Parallax and edit it in MonoDevelop.

using UnityEngine;
using System.Collections;

public class Parallax : MonoBehaviour {

public GameObject poi; // The player ship
public GameObject[] panels; // The scrolling foregrounds
public float scrollSpeed = -30f;
// motionMult controls how much panels react to player movement
public float motionMult = 0.25f;

private float panelHt; // Height of each panel
private float depth; // Depth of panels (that is, pos.z)

// Use this for initialization
void Start () {
panelHt = panels[0].transform.localScale.y;
depth = panels[0].transform.position.z;

// Set initial positions of panels
panels[0].transform.position = new Vector3(0,0,depth);
panels[1].transform.position = new Vector3(0,panelHt,depth);
}

// Update is called once per frame
void Update () {
float tY, tX=0;
tY= Time.time * scrollSpeed % panelHt + (panelHt*0.5f);

if (poi != null) {
tX = -poi.transform.position.x * motionMult;
}

// Position panels[0]
panels[0].transform.position = new Vector3(tX, tY, depth);
// Then position panels[1] where needed to make a continuous starfield
if (tY >= 0) {
panels[1].transform.position = new Vector3(tX, tY-panelHt, depth);
} else {
panels[1].transform.position = new Vector3(tX, tY+panelHt, depth);
}
}
}

Save the script, return to Unity, and assign the script to _MainCamera. Select _MainCamera in the Hierarchy and find the Parallax (Script) component in the Inspector. There, set the poi to _Hero and add StarfieldFG_0 and StarfieldFG_1 to the panels array. Now press Play, and you should see the starfield moving in response to the player.

And of course, remember to save your scene.

Summary

This was a long chapter, but it introduced a lot of important concepts that I hope will help you with your own game projects in the future. Over the years, I have made extensive use of linear interpolation and Bézier curves to make the motion in my games and other projects smooth and refined. Just a simple easing function can make the movement of an object look graceful, excited, or lethargic, which is a powerful when you're trying to balance and tune the feel of a game.

In the next chapter, we move on to a very different kind of game: a solitaire card game (actually, my favorite solitaire card game). The next chapter demonstrates how to read information from an XML file to construct an entire deck of cards out of just a few art assets and also how to use XML to lay out the game itself. And, at the end, you'll have a fun digital card game to play.

Next Steps

From your experience in the previous tutorials, you already understand how to do many of the things listed in this section. These are just some recommendations on what you can do if you want to keep going with this prototype.

Tune Variables

As you have learned in both paper and digital games, tuning of numbers is critically important and has a significant effect on experience. The following is a list of variables you should consider tuning to change the feel of the game:

Image _Hero: Change how movement feels

Image Adjust the speed.

Image Modify the gravity and sensitivity of the horizontal and vertical axes in the InputManager.

Image Weapons: Differentiate weapons more

Image Spread: The spread gun could shoot five projectiles instead of just three but have a much longer delayBetweenShots.

Image Blaster: The blaster could fire more rapidly (smaller delayBetweenShots) but do less damage with each shot (reduced damageOnHit).

Image Power-ups: Adjust drop rate

Image Each Enemy class has a powerUpDropChance field that can be set to any number between 0 (never drop a power-up) to 1 (always drop a power-up). These were set to 1 for testing, but you can adjust them to whatever you want.

Image It's also possible now for multiple Projectiles to hit an Enemy on the same turn that the Enemy's health drops to 0. This will cause multiple PowerUps to be spawned. Try to change the code to stop this from happening.

Add Additional Elements

While this prototype has so far shown five kinds of enemies and two kinds of weapons, there are infinite possibilities for either open to you:

Image Weapons: Add additional weapons

Image Phaser: Shoots two projectiles that move in a sine wave pattern (similar to the movement of Enemy_1).

Image Laser: Instead of doing all of its damage at once, the laser does continuous damage over time.

Image Missiles: Missiles could have a lock-on mechanic and have a very slow fire-rate but would track enemies and always hit. Perhaps missiles could be a different kind of weapon with limited ammunition that were fired using a different button (that is, not the space bar).

Image Swivel Gun: Like the blaster but actually shoots at the nearest enemy. However, the gun is very weak.

Image Enemies: Add additional enemies. There are countless kinds of enemies that could be created for this game.

Image Add additional enemy abilities

Image Allow some enemies to shoot.

Image Some enemies could track and follow the player, possibly acting like missiles homing in on the player.

Image Add level progression

Image Make specific, timed waves instead of the randomized infinite attack in the existing prototype. This could be accomplished using a [System.Serializable] Wave class as defined here:

[System.Serializable]
public class Wave {
float delayBeforeWave=1; // secs to delay after the prev wave
GameObject[] ships; // array of ships in this wave
// Delay the next wave until this wave is completely killed?
bool delayNextWaveUntilThisWaveIsDead=false;
}

Image Add a Level class to contain the Wave[] array:

[System.Serializable]
public class Level {
Wave[] waves; // Holder for waves
float timeLimit=-1; // If -1, there is no time limit
string name = ""; // If the name is left blank (i.e., ""),
// the name could appear as "Level #1"
}

However, this will cause issues because even if Level is serializable, the Wave[] array won't appear properly because the Unity Inspector won't allow nested serializable classes. This means that you should probably try something like an XML document to define levels and waves which can then be read into Level and Wave classes. XML is covered in the "XML" section of Appendix B and is used in the next prototype, Prospector Solitaire.

Image Add more game structure and GUI (graphical user interface) elements:

Image Give the player a score and a specific number of lives (both of these were covered in Chapter 29).

Image Add difficulty settings.

Image Track high scores (as covered in the Apple Picker and Mission Demolition prototypes).

Image Create a title screen scene that welcomes the player to the game and allows her to choose the difficulty setting. This could also show high scores.



Introduction To Game Design Prototyping And Development 31

Source: https://apprize.best/c/game_1/30.html

Posted by: espositohessity.blogspot.com

0 Response to "Introduction To Game Design Prototyping And Development 31"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel