Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

Day 6: Survival Shooter - Tutorial II

5.00/5 (3 votes)
10 Oct 2017CPOL16 min read 5.5K  
Survival shooter - tutorial II

Today on day 6, we’re going to finish the rest of the Survival Shooter tutorial and finally move on to developing a simple game of my own!

Today, we learn more about:

  • Creating the UI
  • Attacking and Moving for the player and the enemies
  • Raycasting
  • Creating more animations
  • …And more!

So let’s get started!

Health HUD

In the next part of the video series, we went on to create the health UI for the game for when the enemy attacks us.

Creating the Canvas Parent

The first thing we want to do is to create a new Canvas object on the hierarchy. We called it HUDCanvas.

We add a Canvas Group component to our Canvas. According to the documentation, anything we check in Canvas Group will persist to its child.

Specifically, we want to uncheck Interactable and Blocks a Raycast. We want to avoid the UI from doing any of these things.

Adding the Health UI Container

Next, we create an Empty GameObject as a child to our HUDCanvas. This will be the parent container for our Health UI. We’ll call it HealthUI.

What’s interesting to note is that, because it’s a child of the Canvas, we also have a Rect Transform component attached to our Game Object.

Click on the Rect Transform and position our HealthUI to the bottom left corner of the game. Remember to hold alt + shift to move the anchor and the position!

Adding the Health Image

Next up, we create an Image UI as a child to the HealthUI. In the Image (Script) component, we just need to attach the provided Heart.png image.

You should see something like this in our scene tab:

Image 1

And it should look something like this in our game tab:

Image 2

Creating our UI Slider

Next up, we need to create the HP bar that we use to indicate the HP that our player has.

We do that by creating a Slider UI GameObject as a child to our canvas. The Slider will come with children objects of its own. Delete everything, except for Fill Area.

Next, we want to make our HP. In the Slide GameObject, make the Max Value of 100 and set Value to also be 100.

Note: I was not able to get the slider to fit perfectly like the video did in the beginning. If you weren’t able to do so either, go to the Rect Transform of the slider and play with the positioning.

Adding a Screen Flicker When the Player Gets Hit

Next, we created an Image UI called DamageImage that’s a child of the HUDCanvas.

We want to make it fill out the whole canvas. This can be accomplished by going to Rect Transform, clicking the positioning box, and then clicking the stretch width and height button while holding alt + shift.

We also want to make the color opaque. We can do that by clicking on Color and moving the A (alpha) value to 0.

When you’re done with everything, your HUDCanvas should look something like this:

Image 3

Player Health

Now that we have our Player Health UI created, it’s time to use it.

We attached an already created PlayerHealth script to our Player GameObject.

Here’s the code:

C#
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.SceneManagement;

    public class PlayerHealth : MonoBehaviour
    {
        public int startingHealth = 100;         // The amount of health 
                                                 // the player starts the game with.
        public int currentHealth;                // The current health the player has.
        public Slider healthSlider;              // Reference to the UI's health bar.
        public Image damageImage;                // Reference to an image to flash on the 
                                                 // screen on being hurt.
        public AudioClip deathClip;              // The audio clip to play when the player dies.
        public float flashSpeed = 5f;            // The speed the damageImage will fade at.
        public Color flashColour = new Color(1f, 0f, 0f, 0.1f); // The colour the damageImage 
                                                                // is set to, to flash.

        Animator anim;                           // Reference to the Animator component.
        AudioSource playerAudio;                 // Reference to the AudioSource component.
        PlayerMovement playerMovement;           // Reference to the player's movement.
        //PlayerShooting playerShooting;         // Reference to the PlayerShooting script.
        bool isDead;                             // Whether the player is dead.
        bool damaged;                            // True when the player gets damaged.

        void Awake ()
        {
            // Setting up the references.
            anim = GetComponent <Animator> ();
            playerAudio = GetComponent <AudioSource> ();
            playerMovement = GetComponent <PlayerMovement> ();
            //playerShooting = GetComponentInChildren <PlayerShooting> ();

            // Set the initial health of the player.
            currentHealth = startingHealth;
        }

        void Update ()
        {
            // If the player has just been damaged...
            if(damaged)
            {
                // ... set the colour of the damageImage to the flash colour.
                damageImage.color = flashColour;
            }
            // Otherwise...
            else
            {
                // ... transition the colour back to clear.
                damageImage.color = 
                Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime);
            }

            // Reset the damaged flag.
            damaged = false;
        }

        public void TakeDamage (int amount)
        {
            // Set the damaged flag so the screen will flash.
            damaged = true;

            // Reduce the current health by the damage amount.
            currentHealth -= amount;

            // Set the health bar's value to the current health.
            healthSlider.value = currentHealth;

            // Play the hurt sound effect.
            playerAudio.Play ();

            // If the player has lost all it's health and the death flag hasn't been set yet...
            if(currentHealth <= 0 &amp;&amp; !isDead)
            {
                // ... it should die.
                Death ();
            }
        }

        void Death ()
        {
            // Set the death flag so this function won't be called again.
            isDead = true;

            // Turn off any remaining shooting effects.
            //playerShooting.DisableEffects ();

            // Tell the animator that the player is dead.
            anim.SetTrigger ("Die");

            // Set the audiosource to play the death clip and play it 
            // (this will stop the hurt sound from playing).
            playerAudio.clip = deathClip;
            playerAudio.Play ();

            // Turn off the movement and shooting scripts.
            playerMovement.enabled = false;
            //playerShooting.enabled = false;
        }

        public void RestartLevel ()
        {
            // Reload the level that is currently loaded.
            SceneManager.LoadScene (0);
        }
    }

Like before, the video commented out some of the code, because we haven’t reached that point yet.

It’s important to note how the functions have been separated into modules that specify what everything does instead of stuffing everything inside Update().

Some things to note from our script:

Looking at Update()

Inside Update(), we create the damage flicker animation effect.

If the player gets damaged (the damaged Boolean becomes true), we set the DamageImage to a red color, then we change the damage Boolean to be false.

Afterwards, as we continue to call Update() on each frame, we would create a lerp that would help us transition from the damaged color back to the original color over time.

Taking Damage

How do we set damaged to be true? From TakeDamage()!

Notice the public in:

C#
public void TakeDamage (int amount)

We’ve seen this before in the previous tutorial. As you recall, this means that we can call use this function whenever we have access to the script component.

Attaching the Components to the Script

The rest of the code is pretty well documented so I’ll leave it to you to read through the comment.

Before we move on, we have to attach the components to our script.

Creating the Enemy Attack Script

It was mentioned earlier that we have a public TakeDamage() function that allows other scripts to call. The question then is, which script calls it?

The answer: the EnemyAttack script. Already provided for us, just attach it to the player.

The code will look something like this:

C#
using UnityEngine;
using System.Collections;

public class EnemyAttack : MonoBehaviour
{
    public float timeBetweenAttacks = 0.5f; // The time in seconds between each attack.
    public int attackDamage = 10;           // The amount of health taken away per attack.

    Animator anim;                          // Reference to the animator component.
    GameObject player;                      // Reference to the player GameObject.
    PlayerHealth playerHealth;              // Reference to the player's health.
    //EnemyHealth enemyHealth;              // Reference to this enemy's health.
    bool playerInRange;                     // Whether player is within the trigger collider 
                                            // and can be attacked.
    float timer;                            // Timer for counting up to the next attack.

    void Awake ()
    {
        // Setting up the references.
        player = GameObject.FindGameObjectWithTag ("Player");
        playerHealth = player.GetComponent <PlayerHealth> ();
        //enemyHealth = GetComponent<EnemyHealth>();
        anim = GetComponent <Animator> ();
    }

    void OnTriggerEnter (Collider other)
    {
        // If the entering collider is the player...
        if(other.gameObject == player)
        {
            // ... the player is in range.
            playerInRange = true;
        }
    }

    void OnTriggerExit (Collider other)
    {
        // If the exiting collider is the player...
        if(other.gameObject == player)
        {
            // ... the player is no longer in range.
            playerInRange = false;
        }
    }

    void Update ()
    {
        // Add the time since Update was last called to the timer.
        timer += Time.deltaTime;

        // If the timer exceeds the time between attacks, 
        // the player is in range and this enemy is alive...
        if(timer >= timeBetweenAttacks &amp;&amp; 
        playerInRange &amp;&amp; enemyHealth.currentHealth > 0)
        {
            // ... attack.
            Attack ();
        }

        // If the player has zero or less health...
        if(playerHealth.currentHealth <= 0) { // ... tell the animator the player is dead. 
        anim.SetTrigger ("PlayerDead"); } } 
        void Attack () { // Reset the timer. 
        timer = 0f; // If the player has health to lose... 
        if(playerHealth.currentHealth > 0)
        {
            // ... damage the player.
            playerHealth.TakeDamage (attackDamage);
        }
    }
}

Like before, some things aren’t commented in yet, however the basic mechanic for the function is:

  • Enemy gets near the player, causing the OnTriggerEnter() to activate and we switch the playerInRange Boolean to be true.
  • In our Update() function, if it’s time to attack in the enemy is in range, we call the Attack() function which then would call TakeDamage() if the player is still alive.
  • Afterwards, if the player has 0 or less HP, then we set the animation trigger to make the player the death animation.
  • Otherwise, if the player outruns the zombie and exits the collider, OnTriggerExit() will be called and playerInRange would be set to false, avoiding any attacks.

With that, we have everything for the game to be functional… or at least in the sense that we can only run away and get killed by the enemy.

Note: If the monster doesn’t chase you, make sure you attached the Player object with the Player tag, otherwise the script won’t be able to find the Player object.

Harming Enemies

In the previous video, we made the enemy hunt down and kill the player. We currently have no way of fighting back.

We’re going to fix this in the next video by giving HP to the enemy. We can do that by attaching the EnemyHealth script to our Enemy GameObject.

Here’s the script:

C#
using UnityEngine;

public class EnemyHealth : MonoBehaviour
{
    public int startingHealth = 100;  // The amount of health the enemy starts the game with.
    public int currentHealth;         // The current health the enemy has.
    public float sinkSpeed = 2.5f;    // The speed at which the 
                                      // enemy sinks through the floor when dead
    public int scoreValue = 10;       // The amount added to the player's score when the enemy dies.
    public AudioClip deathClip;       // The sound to play when the enemy dies.

    Animator anim;                    // Reference to the animator.
    AudioSource enemyAudio;           // Reference to the audio source.
    ParticleSystem hitParticles;      // Reference to the particle system that plays 
                                      // when the enemy is damaged.
    CapsuleCollider capsuleCollider;  // Reference to the capsule collider.
    bool isDead;                      // Whether the enemy is dead.
    bool isSinking;                   // Whether the enemy has started sinking through the floor.

    void Awake ()
    {
        // Setting up the references.
        anim = GetComponent <Animator> ();
        enemyAudio = GetComponent <AudioSource> ();
        hitParticles = GetComponentInChildren <ParticleSystem> ();
        capsuleCollider = GetComponent <CapsuleCollider> ();

        // Setting the current health when the enemy first spawns.
        currentHealth = startingHealth;
    }

    void Update ()
    {
        // If the enemy should be sinking...
        if(isSinking)
        {
            // ... move the enemy down by the sinkSpeed per second.
            transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
        }
    }

    public void TakeDamage (int amount, Vector3 hitPoint)
    {
        // If the enemy is dead...
        if(isDead)
            // ... no need to take damage so exit the function.
            return;

        // Play the hurt sound effect.
        enemyAudio.Play ();

        // Reduce the current health by the amount of damage sustained.
        currentHealth -= amount;
            
        // Set the position of the particle system to where the hit was sustained.
        hitParticles.transform.position = hitPoint;

        // And play the particles.
        hitParticles.Play();

        // If the current health is less than or equal to zero...
        if(currentHealth <= 0)
        {
            // ... the enemy is dead.
            Death ();
        }
    }

    void Death ()
    {
        // The enemy is dead.
        isDead = true;

        // Turn the collider into a trigger so shots can pass through it.
        capsuleCollider.isTrigger = true;

        // Tell the animator that the enemy is dead.
        anim.SetTrigger ("Dead");

        // Change the audio clip of the audio source to the death clip 
        // and play it (this will stop the hurt clip playing).
        enemyAudio.clip = deathClip;
        enemyAudio.Play ();
    }

    public void StartSinking ()
    {
        // Find and disable the Nav Mesh Agent.
        GetComponent <NavMeshAgent> ().enabled = false;

        // Find the rigidbody component and make it kinematic 
        // (since we use Translate to sink the enemy).
        GetComponent <Rigidbody> ().isKinematic = true;

        // The enemy should no sink.
        isSinking = true;

        // Increase the score by the enemy's score value.
        ScoreManager.score += scoreValue;

        // After 2 seconds destroy the enemy.
        Destroy (gameObject, 2f);
    }
}

In a way, this is very similar to the PlayerHealth script that we have.

The biggest difference is that when the player dies, the games ends, however when the enemy dies, we need to somehow get them out of the game.

The flow of this script would go something like this:

  • We initialize our script in Awake()
  • Whenever the enemy takes damage via our public function: TakeDamage(), we play our special effects to show the enemy received damage and adjust our health variable
  • If the enemy’s HP ends up 0 or below, we run the death function which triggers the death animation and other death related code.
  • We call StartSinking() which will set the isSinking Boolean to be true.
  • You might notice that StartSinking() isn’t called anywhere. That’s because it’s being called as an event when our enemy animation finishes playing its death clip. You can find it under Events in the Animations for the Zombunny.

Image 4

  • After isSinking is set to be true, our Update() function will start moving the enemy down beneath the ground.

Moving to the Player

Our enemy has HP now. The next thing we need to do is to make our player character damage our enemy.

The first thing we need to do is some special effects.

We need to copy the particle component on the GunParticles prefab…

Image 5

and pass that into the GunBarrelEnd Game Object which is the child of Player.

Image 6

Next, still in GunBarrelEnd, we add a Line Renderer component. This will be used to draw a line, which will be our bullet that gets fired out.

For a material, we use the LineRendereredMaterial that’s provided for us.

We also set the width of our component to 0.05 so that the line that we shoot looks like a small assault rifle that you might see in other games.

Make sure to disable the renderer as we don’t want to show this immediately when we load.

Next, we need to add a Light component. We set it to be yellow.

Next up, we attach player gunshot as the AudioSource to our gun.

Finally, we attach the PlayerShooting script that was provided for us to shoot the gun. Here it is:

C#
using UnityEngine;

public class PlayerShooting : MonoBehaviour
{
    public int damagePerShot = 20;             // The damage inflicted by each bullet.
    public float timeBetweenBullets = 0.15f;   // The time between each shot.
    public float range = 100f;                 // The distance the gun can fire.

    float timer;                               // A timer to determine when to fire.
    Ray shootRay;                              // A ray from the gun end forwards.
    RaycastHit shootHit;                       // A raycast hit to get information about what was hit.
    int shootableMask;                         // A layer mask so the raycast only hits things 
                                               // on the shootable layer.
    ParticleSystem gunParticles;               // Reference to the particle system.
    LineRenderer gunLine;                      // Reference to the line renderer.
    AudioSource gunAudio;                      // Reference to the audio source.
    Light gunLight;                            // Reference to the light component.
    float effectsDisplayTime = 0.2f;           // The proportion of the timeBetweenBullets 
                                               // that the effects will display for.

    void Awake ()
    {
        // Create a layer mask for the Shootable layer.
        shootableMask = LayerMask.GetMask ("Shootable");

        // Set up the references.
        gunParticles = GetComponent<ParticleSystem> ();
        gunLine = GetComponent <LineRenderer> ();
        gunAudio = GetComponent<AudioSource> ();
        gunLight = GetComponent<Light> ();
    }

    void Update ()
    {
        // Add the time since Update was last called to the timer.
        timer += Time.deltaTime;

        // If the Fire1 button is being press and it's time to fire...
        if(Input.GetButton ("Fire1") &amp;&amp; timer >= timeBetweenBullets)
        {
            // ... shoot the gun.
            Shoot ();
        }

        // If the timer has exceeded the proportion of 
        // timeBetweenBullets that the effects should be displayed for...
        if(timer >= timeBetweenBullets * effectsDisplayTime)
        {
            // ... disable the effects.
            DisableEffects ();
        }
    }

    public void DisableEffects ()
    {
        // Disable the line renderer and the light.
        gunLine.enabled = false;
        gunLight.enabled = false;
    }

    void Shoot ()
    {
        // Reset the timer.
        timer = 0f;

        // Play the gun shot audioclip.
        gunAudio.Play ();

        // Enable the light.
        gunLight.enabled = true;

        // Stop the particles from playing if they were, then start the particles.
        gunParticles.Stop ();
        gunParticles.Play ();

        // Enable the line renderer and set it's first position to be the end of the gun.
        gunLine.enabled = true;
        gunLine.SetPosition (0, transform.position);

        // Set the shootRay so that it starts at the end of the gun and points forward from the barrel.
        shootRay.origin = transform.position;
        shootRay.direction = transform.forward;

        // Perform the raycast against gameobjects on the shootable layer and if it hits something...
        if(Physics.Raycast (shootRay, out shootHit, range, shootableMask))
        {
            // Try and find an EnemyHealth script on the gameobject hit.
            EnemyHealth enemyHealth = shootHit.collider.GetComponent <EnemyHealth> ();

            // If the EnemyHealth component exist...
            if(enemyHealth != null)
            {
                // ... the enemy should take damage.
                enemyHealth.TakeDamage (damagePerShot, shootHit.point);
            }

            // Set the second position of the line renderer to the point the raycast hit.
            gunLine.SetPosition (1, shootHit.point);
        }
        // If the raycast didn't hit anything on the shootable layer...
        else
        {
            // ... set the second position of the line renderer 
            // to the fullest extent of the gun's range.
            gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range);
        }
    }
}

The flow of our script is:

  • Awake() to initialize our variables
  • In Update(), we wait for the user to left click to shoot, which would call Shoot()
  • In Shoot(), we create a Raycast that will go straight forward until it either hits an enemy or a structure, or it reaches the max distance we sit it. From there, we create the length of our LineRenderer from the gun to the point we hit.
  • After a couple more frames in Update(), we will disable the LineRenderer to give the illusion that we’re firing something out.

At this point, we have to do some cleanup work. We have to go back to the EnemyMovement script and uncomment the code that stops the enemy from moving when either the player or it dies.

The changes are highlighted:

C#
using UnityEngine;
using System.Collections;

public class EnemyMovement : MonoBehaviour
{
    Transform player;
    PlayerHealth playerHealth;
    EnemyHealth enemyHealth;
    UnityEngine.AI.NavMeshAgent nav;

    void Awake ()
    {
        player = GameObject.FindGameObjectWithTag ("Player").transform;
        playerHealth = player.GetComponent <PlayerHealth> ();
        enemyHealth = GetComponent <EnemyHealth> ();
        nav = GetComponent <UnityEngine.AI.NavMeshAgent> ();
    }

    void Update ()
    {
        if(enemyHealth.currentHealth > 0 &amp;&amp; playerHealth.currentHealth > 0)
        {
            nav.SetDestination (player.position);
        }
        else
        {
            nav.enabled = false;
        }
    }
}

After all of this is done, we have a playable game!

Note: if you start playing the game and try shooting the enemy and nothing happens. Check if the enemy’s Layer is set to Shootable.

Scoring Points

At this point, we have a complete game! So what’s next? As you can guess from the next video, we’re creating a score system.

We end up doing something similar to what has been done before with the previous 2 video tutorials where we put a UI Text on the screen.

Image 7

Anchor

With that being said, we create a UI Text in our HUDCanvas. We set the RectTransform to be the top. This time we want to just set the anchor by clicking without holding shift + ctrl.

Font

Next, in the Text component, we want to change the Font Style to LuckiestGuy, which was a font asset that was provided for us

Add Shadow Effect

Next up, we attach the shadow component to our text to give it a cool little shadow. I’ve played around with some of the values to make it look nice.

Adding the ScoreManager

Finally, we need to add a script that would keep track of our score. To do that, we’ll have to create a ScoreManager script, like the one provided for us:

C#
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ScoreManager : MonoBehaviour
{
    public static int score;        // The player's score.


    Text text;                      // Reference to the Text component.


    void Awake ()
    {
        // Set up the reference.
        text = GetComponent <Text> ();

        // Reset the score.
        score = 0;
    }


    void Update ()
    {
        // Set the displayed text to be the word "Score" followed by the score value.
        text.text = "Score: " + score;
    }
}

This code is pretty straightforward. We have a score variable and we display that score in Unity, in every Update() call.

So where will score be updated? It won’t be in the ScoreManager, it’ll be whenever our enemy dies. Specifically, that’ll be in our EnemyHealth Script.

C#
using UnityEngine;

public class EnemyHealth : MonoBehaviour
{
    public int startingHealth = 100;   // The amount of health the enemy starts the game with.
    public int currentHealth;          // The current health the enemy has.
    public float sinkSpeed = 2.5f;     // The speed at which the enemy 
                                       // sinks through the floor when dead.
    public int scoreValue = 10;        // The amount added to the player's score when the enemy dies.
    public AudioClip deathClip;        // The sound to play when the enemy dies.

    Animator anim;                     // Reference to the animator.
    AudioSource enemyAudio;            // Reference to the audio source.
    ParticleSystem hitParticles;       // Reference to the particle system that plays 
                                       // when the enemy is damaged.
    CapsuleCollider capsuleCollider;   // Reference to the capsule collider.
    bool isDead;                       // Whether the enemy is dead.
    bool isSinking;                    // Whether the enemy has started sinking through the floor.

    void Awake ()
    {
        // Setting up the references.
        anim = GetComponent <Animator> ();
        enemyAudio = GetComponent <AudioSource> ();
        hitParticles = GetComponentInChildren <ParticleSystem> ();
        capsuleCollider = GetComponent <CapsuleCollider> ();

        // Setting the current health when the enemy first spawns.
        currentHealth = startingHealth;
    }

    void Update ()
    {
        // If the enemy should be sinking...
        if(isSinking)
        {
            // ... move the enemy down by the sinkSpeed per second.
            transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
        }
    }

    public void TakeDamage (int amount, Vector3 hitPoint)
    {
        // If the enemy is dead...
        if(isDead)
            // ... no need to take damage so exit the function.
            return;

        // Play the hurt sound effect.
        enemyAudio.Play ();

        // Reduce the current health by the amount of damage sustained.
        currentHealth -= amount;
        
        // Set the position of the particle system to where the hit was sustained.
        hitParticles.transform.position = hitPoint;

        // And play the particles.
        hitParticles.Play();

        // If the current health is less than or equal to zero...
        if(currentHealth <= 0)
        {
            // ... the enemy is dead.
            Death ();
        }
    }

    void Death ()
    {
        // The enemy is dead.
        isDead = true;

        // Turn the collider into a trigger so shots can pass through it.
        capsuleCollider.isTrigger = true;

        // Tell the animator that the enemy is dead.
        anim.SetTrigger ("Dead");

        // Change the audio clip of the audio source to the death clip and play it 
        // (this will stop the hurt clip playing).
        enemyAudio.clip = deathClip;
        enemyAudio.Play ();
    }

    public void StartSinking ()
    {
        // Find and disable the Nav Mesh Agent.
        GetComponent <NavMeshAgent> ().enabled = false;

        // Find the rigidbody component and make it kinematic 
        // (since we use Translate to sink the enemy).
        GetComponent <Rigidbody> ().isKinematic = true;

        // The enemy should no sink.
        isSinking = true;

        // Increase the score by the enemy's score value.
        ScoreManager.score += scoreValue;

        // After 2 seconds destroy the enemy.
        Destroy (gameObject, 2f);
    }
}

And that’s it! Now we can get a grand total score of… 1. But we’ll fix that in the next video when we add more enemies.

Creating a Prefab

Before we move on to the next video, we made a prefab of our enemy. Like we saw in previous videos, prefabs can be described as a template of an existing GameObject you make.

They’re handy for making multiple copies of the same thing… like multiple enemies!

Spawning

In this upcoming video, we learned how to create multiple enemies that would chase after the player.

The first thing to done was to create the Zombear.

To be re-usable, if you have enemy models that have similar animations like the Zombear and Zombunny, you can re-use the same animations.

However, I was not able to see any animation clips for the Zombear so… I decided to just skip this part.

Then at that point, I got into full-blown laziness and decided to skip the Hellephant too.

However, some important thing to note was that if we have models that have the same types of animation, but different models, we can create an AnimatorOverrideController that takes in an AnimtorController which uses the same animation clips.

EnemyManager

So after our… brief attempt at adding multiple types of enemies, we have to somehow create a way to spawn an enemy.

To do this, we create an empty object which we’ll call EnemyManager in our hierarchy.

Then, we attach the EnemyManager script provided to it:

C#
using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    public PlayerHealth playerHealth;
    public GameObject enemy;
    public float spawnTime = 3f;
    public Transform[] spawnPoints;


    void Start ()
    {
        InvokeRepeating ("Spawn", spawnTime, spawnTime);
    }


    void Spawn ()
    {
        if(playerHealth.currentHealth <= 0f)
        {
            return;
        }

        int spawnPointIndex = Random.Range (0, spawnPoints.Length);

        Instantiate (enemy, spawnPoints[spawnPointIndex].position, 
                     spawnPoints[spawnPointIndex].rotation);
    }
}

The flow of this code is:

  • In Start(), we call InvokeRepeating to call the method “Spawn” starting in spawnTime and then repeating every spawnTime, with spawnTime being 3 seconds
  • Inside Spawn(), we would randomly create an enemy from the array of spawnPoints. However, in this case, we only have 1 location. It was made into an array for re-usability purposes.

And that’s it!

But before we move on, we have to create the spawn point.

We created a new empty object: Zombunny Spawn Point and I set it at:

  • position: (-20.5, 0, 12.5)
  • Rotation (0, 130, 0)

And then from there, just drag the Zombunny Spawn Point to the spawnPoint label inside the EnemyManager script to add the GameObject to our array.

If we followed the video perfectly, we’d have multiple Spawn points that would be hard to tell the difference between.

Unity has an answer for that.

We create add a label by clicking on the colored cube in the inspector in your Game Object and select a color:

Image 8

Play the game and now you should see an endless wave of Zombunny coming at you! Now we’re really close to having a full game!

Gameover

In the final video in this tutorial, we create a more fluid game over state for the player.

Currently, when the player dies, all that happens is that we reload the game and the player starts over. We’re going to do better and add some nifty UI effects!

The first thing we want to do is create an Image UI that we’ll call screenFader. We set the color of the Image to be black and the alpha to be 0. Later on, we create a transition to change the Alpha of the Image so that we’ll have an effect of fading into the game.

Next, we created a Text UI called GameOverText to show to the player that the game is over.

At this point, we have to make sure that we have this ordering inside our HUDCanvas:

  • HealthUI
  • DamageImage
  • ScreenFader
  • GameOverText
  • ScoreText

It’s important that we have this ordering, as the top element on the list will be placed in the screen first.

If we were to stack everything on top of each other, our HealthUI would be at the bottom and the ScoreText would be on the top.

Creating an Animation

Now that we have all the UI elements in place, we want to create a UI animation.

The first thing we need to do is go to Unity > Window > Animation selecting HUDCanvas to create a new animation using the objects that are attached to HUDCanvas.

Click Create a new clip and make a new clip called GameOverClip.

Click Add Property and select:

  • GameOverText > Rect Transform > Scale
  • GameOverText > Text > Color
  • ScoreText > Rect Transform > Scale
  • ScreenFader > Image > Color

This will add these 4 properties to our animation.

How animation works is that you start at some initial value as represented in the diamond:

Image 9

When you double click in the timeline of the effects, you create a diamond for a property.

When you move the white line slider to the diamond, and select it, you can change the value of the property in the inspector that the game object will be at in that specific time in the animation.

Essentially, the animation will make gradual changes from the 1st diamond to the 2nd diamond. Or from the original location to the diamond.

An example is: at 0:00 if X scale is 1 and at 0:20 X scale is 2, at 0:10, X scale will be 1.5

So follow what was done in the above picture.

  • GameOverText : Scale – We want to create a popping text, where the text appears disappears, and then pops back.
    • 0:00 Scales are all 1
    • 0:20 Scales are all 0
    • 0:30 Scales are all 1
  • GameOverText : Text.Color – We want to create white text that gradually fades in.
    • 0:00 color is white with alpha at 0
    • 0:30 color is white with alpha at 255
  • ScoreText: Scale – we want the score to shrink a bit
    • 0:00 scales are all 1
    • 0:30 scales are all 0.8
  • ScreenFader : Image.Color – We want to gradually make a black background show up
    • 0:00 color is black with alpha 0
    • 0:30 color is black with alpha 255

When we create an animation, Unity will already create an Animator Controller with the name of the object we created the animation for us (HUDCanvas).

Setting Up Our HudCanvas Animator Controller

In the HudCanvas animator controller, we create 2 New State.

One will act as a main transition and the other we’ll name it GameOver.

We also create a new trigger called GameOver.

We make the New State our main transition. From there, we create a transition from New State to GameOver when the trigger GameOver is enabled.

We should have something like this after you’re done:

Image 10

Save our work and then we’re done!

Note: When we create an Animation from HUDCanvas, it would add the animator controller to it. If it doesn’t, manually create an Animator component to HUDCanvas and attach the HUDCanvas Animator Controller.

Creating a GameOverManager to Use Our Animation

Finally, in the last step, we need to create some code that will use our animation that we just created when the game is over.

To do this, we just add the provided GameOverManager script to our HUDCanvas. Here’s the code:

C#
using UnityEngine;

public class GameOverManager : MonoBehaviour
{
    public PlayerHealth playerHealth;       // Reference to the player's health.
    public float restartDelay = 5f;         // Time to wait before restarting the level


    Animator anim;                          // Reference to the animator component.
    float restartTimer;                     // Timer to count up to restarting the level


    void Awake ()
    {
        // Set up the reference.
        anim = GetComponent <Animator> ();
    }


    void Update ()
    {
        // If the player has run out of health...
        if(playerHealth.currentHealth <= 0) 
        { // ... tell the animator the game is over. 
            anim.SetTrigger ("GameOver"); // .. increment a timer to count up to restarting. 
            restartTimer += Time.deltaTime; // .. if it reaches the restart delay... 
            if(restartTimer >= restartDelay)
            {
                // .. then reload the currently loaded level.
                Application.LoadLevel(Application.loadedLevel);
            }
        }
    }
}

The basic flow of the code is:

  • We initialize our Animator by grabbing the Animator component that is attached to our game object inside Awake()
  • Inside Update(), we’ll always check to see if the player is alive, if he’s not, we play the GameOver animation and set a timer so that after our clip is over, we would restart the game.

Conclusion

Phew, this has really drawn out long past 2 days.

The only reason why I decided to follow through is:

  • There’s a lot of good learning that happens when you have to write
  • Most likely, this will be the last of the long articles. From now on, I’ll be going on by myself to create a simple game and progress will be much slower as I try to Google for my answer.

There were a lot of things that we saw again, but even more things that we learned.

We saw a lot of things that we already knew like:

  • The UI system
  • Colliders
  • Raycasts
  • Navigating Unity

…And then we saw a lot more things that we have never seen before like:

  • Character model animations
  • Management scripts to control the state of the game
  • Creating our own UI animation
  • Using Unity’s built in AI

It’s only day 6 of our 100 days of VR please end me now I’m going to collapse on my bed now.

I’ll see you back for day 7 where I start trying to develop my own simple game.

Day 5 | 100 Days of VR | Day 7

The post Day 6: Survival Shooter Tutorial II appeared first on Coding Chronicles.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)