Customizable Navigation Bar

Wednesday, April 13, 2011

Wednesday's Editor Tools 1: Import Utility

Alright, so since the poll said that you guys wanted bi-weekly updates, here they are! Starting now, I will be posting every Sunday and Wednesday. Wednesday's are going to cover editor tools, so that your life is made easier during production.

This first script is going to be called ImportUtility. Now a brief description of what this class does is check to see what is currently being imported, and making materials and prefabs if necessary. This will save you a ton of time in the long run, since creating multiple prefabs or materials at a time can be very tedious.

So, let's get started and create a new C# script called ImportUtility.cs.

using UnityEngine;

/*Since this is extending an Editor class, we are going to have to include the UnityEditor directory*/

using UnityEditor;

/*Since this class is going to need to know when an object is imported, we extend from AssetPostprocessor. AssetPostprocessor fires events when a resource is imported and even fires of different ones depending on whether or not it is a texture, a model or an audio asset.*/

public class ImportUtility : AssetPostprocessor
{

        /*The first event that we are going to overwrite is called OnPreprocessTexture. This event is called right before a texture asset is going to be imported so you can fiddle around with settings such as converting the texture to a normal map, which is what we are going to be doing*/

void OnPreprocessTexture()
{

        /*One thing that all projects should have is a naming convention, it makes things more organized and neat, and when you hand your assets off to someone else they know what they have before they even open up the file! So what we are going to do is check the name of the asset being imported, and if the name contains "_norm" we will convert the texture to a normal map on import*/

                if(assetPath.ToLower().Contains("_norm"))
                {

                /*Cast assetImporter as TextureImporter for extra options when importing a texture*/

                        ((TextureImporter)assetImporter).convertToNormalmap = true;
                }
        }

        /*The next event we overwrite is called OnPostProcessTexture. This is similar to the OnPreProcessTexture event, except this one isn't called until after the object is imported. So what are we going to do once we import this asset? On thing that I can think of that would be helpful is if it automatically created a material and applied the texture to it on import, so guess what we are going to do.*/

        void OnPostprocessTexture(Texture2D texture)
        {

                /*If the name of the asset contains _diff, then we will create a diffuse material for it*/

                if(assetPath.ToLower().Contains("_diff"))
                {
                        /*So let's create a new diffuse material and apply the texture being imported to it*/

                        Material mat = new Material(Shader.Find("Diffuse"));
                        mat.mainTexture = texture;

                        /*What we want to do next is set up a path to where the new material is going to go in our project folder. So we will remove the _diff from the path and add _MAT.mat so you know that it is a material just by looking at it, and Unity knows to save this asset as a material*/

                        string path = assetPath;
                        path = path.Remove(path.LastIndexOf("_"));
                        path += "_MAT.mat";

                        /*Once that is finished, we will use a class called AssetDatabase to create the material in the project folder, and rename the material to the texture's name so that they match*/

                        AssetDatabase.CreateAsset(mat, path);
                        AssetDatabase.RenameAsset(path, texture.name);
                }
        }

        /*So now we've completed the texture importing part of this class. Now we are going to be moving onto the models*/


        /*Before we import any model, let's make sure that we have a few settings set up a certain way*/

        void OnPreprocessModel()
        {

                /*cast assetImporter as ModelImporter to get special model importing tools and functions*/

                ModelImporter importer = (ModelImporter)assetImporter;

                /*We want unity to import the models using the units that maya or max uses, just to keep everything consistent between tool to engine*/

                importer.useFileUnits = true;

                /*We also don't want to automatically add colliders unless specified, so let's turn this option off too*/

                importer.addCollider = false;

                /*Unity will also import any materials applied in the hypershade(Maya) or material editor(Max) automatically. Now if this is what you want, then go nuts and use it, but I find it to be a pain since i create my own materials anyways, so I turn this option off as well*/

                importer.generateMaterials = 0;

                /*Now there are also options for for automatically importing animations, compression rates etc. that you can player around with. For the project that I was working on when i created this, this was more or less enough*/

        }

        /*So once we have those import options set up, we need to create prefabs for the models that are being imported, because they are handy, and more efficient*/

        void OnPostprocessModel(GameObject root)
        {

                /*recursiveColliderCheck() is a function declared below that will go through each object in the model and check to see if the name of it contains collider. If it does it adds a MeshCollider component to it and moves to the next one*/

                recursiveColliderCheck(root.transform);

                /*We are going to take the same approach for generating the prefab asset path as we did with the material*/


                string prefabPath = assetPath;
                prefabPath = prefabPath.Remove(prefabPath.LastIndexOf("."));
                prefabPath += ".prefab";

                /*If the prefab already exists then we don't need to create a new one, otherwise go through the steps*/


                if(AssetDatabase.LoadAssetAtPath(prefabPath, typeof(object)) == null)
                {

                        /*First we will create an empty prefab to store the mesh in*/

                        Object prefab = EditorUtility.CreateEmptyPrefab(prefabPath);

                        /*This is a temporary GameObject that will be the parent of the mesh. I had explained in a previous tutorial why we want to do this but I will explain again. Animations are done locally, so if the object animating is not parented, it will jump to where ever it was during that animation and play it. So if the animation started at the origin (0,0,0), the object will jump there and animate. If the object is parented, it will stay where the parent of the object is and animate at (0,0,0) locally.*/

                        GameObject obj = new GameObject("____delete me");

                        /*We move the gameobject to the root's position and parent it*/

                        obj.transform.position = root.transform.position;
                        root.transform.parent = obj.transform;

                        /*In order to store the GameObject in the prefab we need to use a static function EditorUtility.ReplacePrefab(). You pass the object going in and the prefab that you are replacing, and you are good to go*/

                        EditorUtility.ReplacePrefab(obj, prefab);

                        /*Before you do this line, go into the editor and create a new tag called delete. We need this so that we know what to delete after importing*/

                        obj.tag = "delete";

                        /*Then we will rename the prefab to the name of the mesh*/

                        AssetDatabase.RenameAsset(prefabPath, root.name);
                }
        }

        /*Now that we have done this to textures and models, we are going to do the same to audio. For me, the default audio settings were fine, so I leave OnPreprocessAudio out. Instead I just skip ahead to OnPostProcessAudio and create a prefab for it just like I did with the models*/

        void OnPostProcessAudio(AudioClip clip)
        {

                /*So once again, edit the asset path and create a new empty prefab*/

                string path = assetPath;
                path.Remove(path.LastIndexOf("."));
                path += ".prefab";
                Object prefab = EditorUtility.CreateEmptyPrefab(path);

                /*And create a new GameObject*/

                GameObject obj = new GameObject("____delete me");

                /*What you are going to want to do then is add an AudioSource component and set the clip to the clip being imported*/

                AudioSource audio = obj.AddComponent<AudioSource>();
                audio.clip = clip;

                /*Once that is complete we will replace the prefab and set the tag to delete*/

                EditorUtility.ReplacePrefab(obj, prefab);
                obj.tag = "delete";
                AssetDatabase.RenameAsset(path, clip.name);
        }

        /*This function is called in the OnPostprocessModel event. It is recursive, so what it does is check through each child of the object and add a MeshCollider if the name of the child contains collider*/

        private void recursiveColliderCheck(Transform tran)
        {
                if(tran.name.ToLower().Contains("collider"))
                {
                        tran.gameObject.AddComponent<MeshCollider>();

                        /*If the object has grandchildren we want to check those too, so for each child in this object, we call the function again*/

                        foreach(Transform currentTran in tran)
                        {
                                recursiveColliderCheck(currentTran);
                        }
                }
        }

        /*This event is called once every asset is imported. What we use this event for is deleting all of the GameObjects we created with the tag delete. We cannot delete the objects during the events because Unity will just completely crash. Of it works for you, then thats awesome, but that was the workaround that i ended up trying out and it works like a charm*/

        static void OnPostprocessAllAssets (string[] importedAssets , string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {

                /*So let's find all of the objects in the scene with the tag delete*/

                GameObject[] list = GameObject.FindGameObjectsWithTag("delete");

                /*We will then destroy these objects and you will never know that they existed*/

                foreach(GameObject obj in list)
                {
                        Editor.DestroyImmediate(obj);
                }
        }
}
        
That's it for this script. It seems simple, but it will save you a lot of time in the long run. Making prefabs individually is a real pain, and is very tedious, so why not get something to do it for you.

This is it for Wednesday, keep coding!

Sunday, April 10, 2011

Building A Game In Unity Part 7: Fine Tuning Your Code Before Release

Alright guys, this week I'm going to be covering some tips on what you should be keeping an eye out for during your last steps in your development cycle. Profiling and tuning your code is an extremely important step in development, because one tiny slip could bottleneck the whole performance of your game or application.

Memory leaks, overextended loops, dead code, all of these can mess around with your performance goals, and if you don't have performance goals, you need to set them. 

Where do you start with performance goals? Well something that you could start with is being able to run your game or application, with multiple programs loaded in the background taking up memory. Your game should always be using as little processor time and memory as possible at all times. 

Why? One thing you should keep in mind is that the lower your footprint left on the computer is, the more people will be able to play it, and at higher settings. Also, it's nice to have a smaller footprint left on the computer for those unavoidable instances where you need the extra resources, because they do happen. 

So where do I start? I will be giving some examples of what you need to look for during your profiling stage that may seem small, but will make a difference in the long run.

Example # 1: Declaring variable inside a loop

You should turn this:
        
        foreach(Entity currentEntity in GameObject.FindObjectsOfType(typeof(Entity)))
        {
               Transform example = currentEntity.transform;
                ...........
        }

into this:

         Transform example

        foreach(Entity currentEntity in GameObject.FindObjectsOfType(typeof(Entity)))
        {
               example = currentEntity.transform;
                ...........
        }

??? What difference does declaring a variable outside of the loop make? Well, declaring the storage variable outside of the loop allocates memory once, which is one step in the processor. Declaring the storage variable within the loop allocates memory as many times as the loop is long. These two work the exact same way, but declaring the variable outside of the loop gets rid of a tiny bit of overhead that the other way introduces. 

This is a small tweak, but if you have dozens of scripts that do the same thing, the overhead just grows and grows, and this is where the problem arises. Small seemingly insignificant code quarks add up, and they can do a number to your performance. 


Note: One test I always make sure my game passes. If you have Unity pro, turn on deep profiling and test your game. Tweak and test your code until you get a smooth frame rate with it turned on, because it takes a huge hit out of performance. Deep profiling will tell you how much processor and memory each function is using which is a HUGE benefit when you are trying to remove a bottleneck.


Example 2: Creating Static Variables

Now lets say that your enemy class needs reference to the main player. What you would obviously want to do is create a variable to store it:

         public Player mainPlayer;

Then you will want to store the player reference here like so.

         enemy1.mainPlayer = mainPlayer.....

Sure, good to go, but there is one problem. That only changes the single enemy instance. If you want to change it in every instance you need to use either a for or a foreach statement:

         foreach(Enemy currentEnemy in GameObject.FindObjectsOfType(typeof(Enemy)) as Enemy[])
         {
                  currentEnemy.mainPlayer = mainPlayer;
         }

and that is just unnecessary processor cycles being used, and extra memory being used for each enemy! How do we fix this?

Make it static:

         static public Player mainPlayer;

then you would use:

         Enemy.mainPlayer = mainPlayer;

instead of the for loop and whatnot. It is one cycle, and every enemy's mainPlayer variable changes, and there is only one chunk of memory taken up for that single static mainPlayer variable. This is definitely something that you should keep in mind.


Example 3: Coroutines Coroutines Coroutines!

Coroutines are awesome. Taking stuff out of the Update function and putting it into a coroutine is a great way to save processor time. Square roots for example, one of the more expensive things to do. Do they need to be done every frame? Sometimes, but not all of the time. What you need to do as a programmer, is see what you can get away with. As bad as it sounds, if you cannot see the difference between how something moves every 5 frames and every frame, then go nuts and take it out of the update loop.

        void Update()
        {
                movementDirection = Vector3.Normalize(other.transform.position - transform.position);
        }

Now the normalize uses a square root function. It is fairly expensive to use every frame, and if we say did it 5 times a second, we will get a very similar effect with a huge chunk of the overhead gone!

        void Start()
        {
                StartCoroutine(getDirection());
        }

        IEnumerator getDirection()
        {
                while(true)
                {
                        movementDirection = Vector3.Normalize(other.transform.position - transform.position);
                        yield return new WaitForSeconds(0.2f);
                }
        }


These three are the most common steps that people either don't think about, or just get lazy and skip over. This is a good starting point for optimizing your scripts in Unity. A part 2 to this article will go over a few optimizations that are extremely nit picky, and are definitely things that few programmers do. The overhead reduction from these optimizations are very small, but they still exist. If you are an OCD programmer, these will be the optimizations for you.

This is it for this Sunday. Next Sunday I will go over the steps you can take to build your game for release, and what you can do with the settings. Keep coding!

Monday, April 4, 2011

Latest Poll

Well guys, it looks as though I will be posting twice a week now. Which is good. It will keep me busy and there is definitely a lot to talk about. I have some utility scripts that I have been working on to make a ton of stuff quicker and easier, so I will start posting those along with the building a game tutorials.

Now, I was hoping that you guys would vote for the second option, which was to include more that just Unity. It's not that I am bored, it's just nice to branch out and tackle different engines/programming languages. Perhaps a similar poll will come around a month or two down the road, we'll see.

Anyways, the new posting dates will be Sunday and Wednesday from now on, so keep coding!

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: