Customizable Navigation Bar

Sunday, April 3, 2011

Building A Game In Unity Part 6:The Boss and Some Very Basic GUI

This is part 6 of the Building a Game In Unity tutorial series. This tutorial is going to cover how you would go about scripting a very simple Boss and some basic GUI usage.

So one thing that almost all games have, is the boss battle. Whether you want it to be epic, or just a quick, fun  new mechanic for your game boss battles take a fair amount of time to tweak and get perfect. This boss script is very simple, and will more or less give you something to start with.

First things first, create a new C# script and call it Boss.cs. It's should also extend Enemy, since most of the functionality that we added from the previous tutorial is also used here.


using UnityEngine;
using System.Collections;

/*Extend enemy so that we get all of the added functionality from the last tutorial*/
      
public class Boss : Enemy
{

        /*We need to keep track of where the parent of this object is in world space. In Unity, animations are played in local space, so if there is no parent to the object that is playing the animation, that object will snap to what ever coordinates the animation is being played at. For example, if the animation starts at (0,0,0) and the player is at (100, 100, 100) the player will snap to (0,0,0) instead of playing the animation from it's current position*/

         private Transform parent;

        /*We get the starting point of the ship in screen coordinates because this is where we want our ship to have it's resting position at*/

         private Vector3 startingPoint = new Vector3(Screen.width * 0.5f, Screen.height * 0.9f, 60);


        /*We also should keep a reference to the main camera for movement and animation purposes*/

         Camera mainCamera;

        /*Let's make some public variables that will hold the prefabs of the bullets that will be shot from the boss*/

         public Projectile basicShot;

         public Projectile rocket;

         public Projectile trackingRocket;

        /*Since we have multiple bullet types, we should create a number to reference what type of bullet we are firing. Using a number is just cheaper than checking the type*/

         private int mFiringMode;


        /*Now that that is over with, let's make an array of weapons and animation clips to store all of the weapons and animation that this object is using, just for easier reference and enabling/disabling*/

         private Weapon[] mBossWeapons;

         private AnimationClip[] mBossAnimations;


        /*We also want to create some public textures so that we can throw in the texture for what the health bar is going to look like from the editor*/

         public Texture healthBar;

         public Texture healthBarOutline;


        /*It's also cheaper to cache the health bar location instead of creating a new Rect every frame/multiple times per frame*/

         private Rect mHealthBarLocation;


        /*First thing we want to do is fill in the reference to the main camera, set the reference to this object's parent object and set a longer lifespan for this object*/

         void Awake()

         {

                 mainCamera = Camera.mainCamera;



                  if(mainCamera == null)
                 {
                          Debug.LogError("No Camera with tag mainCamera");
                          Debug.Break();
                 }

                  parent = transform.parent;
                if(parent == null)
                 {
                          Debug.LogError("Error: Boss needs to be nested inside of gameobject for animations to play correctly");
                          Debug.Break();
                 }

                mLifeSpan = 300.0f;

         }


         void Start ()
         {



        /*Now that that is over with, we should check to make sure that we put some sort of object into the public projectile variables that we created, because sometimes we forget to do things like that, and it just screws stuff up*/


                  if(basicShot == null)
                 {

                         if((basicShot = (Resources.Load("Projectiles/Basic Shot") as GameObject).GetComponent<Projectile>()) == null)

                          {
                                  Debug.Log("No prefab found of type basicshot");
                                   Debug.Break();
                          }
                  }

                  if(rocket == null)
                  {
                          if((rocket = (Resources.Load("Projectiles/Rocket") as GameObject).GetComponent<Projectile>()) == null)
                          {
                                  Debug.Log("No prefab found of type rocket");
                                  Debug.Break();
                          }
                 }


                  if(trackingRocket == null)
                  {
                          if((trackingRocket = (Resources.Load("Projectiles/Tracking Rocket") as GameObject).GetComponent<Projectile>()) == null)
                          {
                                  Debug.Log("No prefab found of type tracking rocket");
                                  Debug.Break();
                          }
                  }

                /*If our animation component doesn't exist on this object, let's create one and set some default animation clips to it. The path I am using will definitely be different from the one you are using*/

                  if(animation == null)
                  {
                          gameObject.AddComponent<Animation>();

                          Object[] animationArray = Resources.LoadAll("Enemies/Boss/Animations");
                         mBossAnimations = new AnimationClip[animationArray.Length];


                         for(int i  = 0; i < animationArray.Length; i++)
                          {
                                  Instantiate(animationArray[i]);
                                 mBossAnimations[i] = animationArray[i] as AnimationClip;


                                  animation.AddClip(mBossAnimations[i], mBossAnimations[i].name);
                         }

                 }

                  else
                  {

                /*If our animation component does exist, then we just add the animation clips to the reference array that we created. The reference array is so that we can access the animation clips through a numbered index intead of a string value*/

                          int animCounter = 0;
                          mBossAnimations = new AnimationClip[animation.GetClipCount()];
                          foreach(AnimationClip clip in animation)
                         {

                                  mBossAnimations[animCounter]  = clip;
                                  animCounter++;
                          }
                 }


                /*Next let's set the health bar location up depending on the screen size.*/

                  mHealthBarLocation = new Rect(Screen.width * 0.05f, Screen.height * 0.95f, Screen.width * 0.9f, Screen.height * 0.03f);

                  if(healthBar == null)
                  {
                          if((healthBar = Resources.Load("Global Objects/HealthBar") as Texture) == null)
                         {

                                  Debug.Log("health bar texture could not be found");
                                  Debug.Break();
                         }
                  }
                 if(healthBarOutline == null)
                 {
                          if((healthBarOutline  = Resources.Load("Global Objects/HealthBarOutline") as Texture) == null)
                          {
                                  Debug.Log("health bar outline could not be found");
                                  Debug.Break();
                          }
                 }


                /*After we get through everything error free, let's set up the boss's health and set up the weapon array*/

                  mMaxHealth = 1000 + 3000 * Application.loadedLevel;
                 mHealth = maxHealth;
                 mBossWeapons = gameObject.GetComponentsInChildren<Weapon>();
                /*This coroutine is called once everything is initialized. All it does is move the boss to the starting position that we determined towards the top of this class*/

                  StartCoroutine(moveToStartingPosition());
        }


        /*Some simple GUI functionality for a health bar*/

         void OnGUI()

         {


                /*What this first DrawTexture call does is draw the health bar texture that we put into it in the editor, but changes it's width depending on how much health it has*/

                  GUI.DrawTexture(new Rect(mHealthBarLocation.xMin, mHealthBarLocation.yMin, mHealthBarLocation.width * mHealth / mMaxHealth, mHealthBarLocation.height), healthBar);

                /*This one just draws the outline of the health bar. You can exclude this if you don't want to use one*/

                 GUI.DrawTexture(mHealthBarLocation, healthBarOutline);

         }

        /*This Coroutine moves the parent object to the resting position that we got at the top of this class. We want the parent object to move to this position so that the animations will play properly from the position that we want it to*/

         private IEnumerator moveToStartingPosition()
         {
                  Vector3 bossStartingPosition = Vector3.zero;
                 while(Vector3.SqrMagnitude(parent.position - bossStartingPosition) > 1)

                 {

                          bossStartingPosition = mainCamera.ScreenToWorldPoint(startingPoint);
                         parent.position = Vector3.Lerp(parent.position, bossStartingPosition, Time.deltaTime);


                          yield return new WaitForEndOfFrame();
                 }


                 /*Once we get to the starting point, lets activate our weapons*/



                  for(int i = 0; i < mBossWeapons.Length; i++)
                 {

                         mBossWeapons[i].enableWeapon();

                          mBossWeapons[i].projectileColor = projectileColor;
                  }

                /*We then call a function that is filled in below that will randomize the animation that will be played, and then start the main boss behaviour*/

                  randomizeMovementType();
                  StartCoroutine(bossBehaviour());
         }


        /*We'll just make sure that the parent get's destroyed at the same time this object is*/

         protected override void OnDestroy ()

         {

                  Destroy(parent.gameObject);
         }


        /*This coroutine waits until our current animation is complete and then randomizes the next animation, the weapon type, weapon speed and which weapons will be activated*/

         private IEnumerator bossBehaviour()

         {

                 while (true)

                 {

                         yield return new WaitForSeconds(animation.clip.length);

                          randomizeProjectileType();
                          randomizeMovementType();
                          randomizeWeaponActivity();
                          randomizeWeaponFireRate();
                  }
         }


        /*This function creates a random number and sets the projectile type of each weapon depending on that*/

         private void randomizeProjectileType()
         {
                  mFiringMode = Random.Range(0, 3);
                  switch(mFiringMode)
                 {

                  case 0:
                          setProjectileType(basicShot);
                          break;
                 case 1:

                          setProjectileType(rocket);
                         break;

                  case 2:
                          setProjectileType(trackingRocket);
                         break;

                  }
         }


        /*This function enables a random number of weapons on this ship*/

         private void randomizeWeaponActivity()

         {

                  deactivateAllWeapons();
                  int numberOfActiveWeapons = Random.Range(3, mBossWeapons.Length);

                  for(int i = 0; i < numberOfActiveWeapons; i++)
                 {
                         activateRandomWeapon();

                 }
         }


        /*This function will randomize how fast the ship will fire, but also take the loaded level into account*/

         private void randomizeWeaponFireRate()

         {

                  int modifier = 2 * Application.loadedLevel;
                 if(mFiringMode == 0)
                 {

                         setFireRate(Random.Range(3 + modifier, 6 + modifier));

                 }

                  else
                  {
                          setFireRate(Random.Range(1 + modifier, 3 + modifier));
                 }
         }

        /*This function will randomly select an animation for the boss to run through*/

         private void randomizeMovementType()
         {
                  int randomAnimation = Random.Range(0, animation.GetClipCount());
       
                  animation.clip = mBossAnimations[randomAnimation];
                 animation.Play();

         }


        /*This function will set the projectile type of each weapon to the passed projectile*/

         private void setProjectileType(Projectile projectileType)
         {

                 for(int i = 0; i < mBossWeapons.Length; i++)

                  {
                          mBossWeapons[i].bulletType = projectileType;
                 }

         }


        /*Simple function just enables all weapons*/

         private void activateAllWeapons()

         {

                  for(int i = 0; i < mBossWeapons.Length; i++)
                  {
                          mBossWeapons[i].enableWeapon();
                 }

         }


        /*Simple function deactivates all weapons*/

         private void deactivateAllWeapons()
         {

                  for(int i = 0; i < mBossWeapons.Length; i++)
                  {
                          mBossWeapons[i].disableWeapon();
                 }

         }


        /*Activates a random weapon on this ship*/

         private void activateRandomWeapon()
         {
                  int numberOfActive = 0;
                  int numOfWeapons = mBossWeapons.Length;
      
                  for(int i = 0; i < numOfWeapons; i++)
                 {

                          if(mBossWeapons[i].enabled == true)
                          {
                                  numberOfActive++;
                          }
                  }

                  if(numberOfActive == numOfWeapons)
                  {
                          return;
                  }

                  int randomNumber = Random.Range(0, numOfWeapons);

                  if(mBossWeapons[randomNumber].enabled == true)
                 {

                          activateRandomWeapon();
                  }
                 else
                 {

                          mBossWeapons[randomNumber].enableWeapon();
                 }

         }


        /*Sets the firing rate of each weapon on this ship*/

         private void setFireRate(float fireRate)
         {

                  fireRate = 1.0f/fireRate;

                 for(int i = 0; i < mBossWeapons.Length; i++)
                 {

                          mBossWeapons[i].fireRate = fireRate;
                  }
         }


        /*loads the next level, use if the next level should be loaded after the boss dies*/

         private void loadNextLevel()
         {

                  Application.LoadLevel(Application.loadedLevel +1);
         }

}


That's it for the boss. Keep in mind that this is a very simple boss and should just be placeholder until you get your actual boss battle in your game. Having random animations and fire rates and whatnot makes the game hard to balance and won't necessarily make the game fun.


Now we did some basic GUI in the boss, for it's health, so now we need to do that same for the main character. Instead of adding GUI to the already existing Player class, let's create a new dedicated GUI class called MainGUI.cs.

using UnityEngine;
using System.Collections;

public class MainGUI : MonoBehaviour
{

        /*We want to have reference to the main player so let's create a public variable that we can throw it into*/

         public Player player;

        /*We also need a reference to the textures that we will be using for the health bar and the icon for the amount of lives that the player has left, if you choose to have one*/

         public Texture healthBar;
         public Texture healthBarOutline;

         public Texture shipGUI;

        /*Next we'll create some private variables that we can use to cache the locations of all of the GUI objects*/

         private Rect mHealthBarLocation;
        private Rect mScoreLocation;
         private Rect mScoreMultiplierLocation;

         private Rect mShipLivesTextureLocation;

         private Rect mShipLivesTextLocation;


         void Start()
         {


                /*Now that we have the variables declared, let's put a value into them. If these values are used, they will be placed in the top left area of the screen.*/

                 mHealthBarLocation = new Rect(Screen.width * 0.05f, Screen.height * 0.05f, Screen.width * 0.01f, Screen.height * 0.15f);

                  mScoreLocation = new Rect(Screen.width * 0.5f, Screen.height * 0.05f, Screen.width * 0.1f, Screen.height * 0.1f);
                  mScoreMultiplierLocation = new Rect(Screen.width * 0.05f, Screen.height * 0.21f, Screen.width * 0.1f, Screen.height * 0.1f);
                  mShipLivesTextureLocation =new Rect(Screen.width * 0.03f, Screen.height * 0.25f, Screen.width * 0.05f, Screen.height * 0.05f);
                  mShipLivesTextLocation = new Rect(Screen.width * 0.1f, Screen.height * 0.26f, Screen.width * 0.1f, Screen.height * 0.1f);

                /*If nothing was put into the health bar etc. values, lets manually search for them in your resources folder*/

                  if(healthBar == null)
                 {

                         healthBar  = Resources.Load("Global Objects/HealthBar") as Texture;

                 }

                  if(healthBarOutline == null)
                 {

                          healthBarOutline  = Resources.Load("Global Objects/HealthBarOutline") as Texture;
                 }


                 if(shipGUI == null)
                  {
                          shipGUI = Resources.Load("Global Objects/ship") as Texture;
                 }


                /*Let's do the same with the main player. If we forgot to set the value of the player, let's find it ourselves*/

                 if(player == null)

                  {
                          if((player = GameObject.FindObjectOfType(typeof(Player)) as Player) == null)
                         {

                                  Debug.LogError("Cannot locate player object");
                                  Debug.Break();
                          }
                 }

         }


        /*Here is our GUI function. Very simple now that we have the positions all set up.*/

         void OnGUI()
         {

                /*What this will do, is set the font size of the labels to 1/10th of the screen size*/

                 GUI.skin.label.fontSize = (int)(Screen.height * 0.1f);


                /*Let's display the score that the player has earned so far*/

                  GUI.Label(mScoreLocation, player.displayScore.ToString());

                /*Now let's put up the rest of your GUI.*/

                  GUI.DrawTexture(new Rect(mHealthBarLocation.xMin, mHealthBarLocation.yMax, mHealthBarLocation.width, mHealthBarLocation.height * -(player.currentHealth / player.maxHealth)), healthBar);
                 GUI.DrawTexture(mHealthBarLocation, healthBarOutline);


                  string multiplierString = player.scoreMultiplier.ToString() + "X";
                 GUI.Label(mScoreMultiplierLocation, multiplierString);


                 GUI.DrawTexture(mShipLivesTextureLocation, shipGUI,ScaleMode.ScaleToFit);

                 GUI.Label(mShipLivesTextLocation, player.lives.ToString());

         }
}

GUI is fairly easy to use. One problem with the GUI system that Unity uses, is that sometimes OnGUI gets called multiple times per frame. One way around this is to create your own buttons, windows etc, out of GUITextures and GameObjects. They are updated when you tell them to be, so a coroutine could take care of this, or you could throw it in the update function if you wanted to.

Remember that every millisecond counts, and keep coding!

That's it for this tutorial. Follow me on twitter, or subscribe to this blog to receive new updates when brand new posts are up!



NOTE: I have been informed that I accidentally completely skipped the Player class in all of my tutorials, so what i am doing is posting a link to all of the source code for this project. As a test for you guys, i am putting bugs into it for you to fix. Everything is still going to work, but it may start doing some weird stuff as you play through it.

Here is the link: