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:
And it should look something like this in our game tab:
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:
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:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.SceneManagement;
public class PlayerHealth : MonoBehaviour
{
public int startingHealth = 100;
public int currentHealth;
public Slider healthSlider;
public Image damageImage;
public AudioClip deathClip;
public float flashSpeed = 5f;
public Color flashColour = new Color(1f, 0f, 0f, 0.1f);
Animator anim;
AudioSource playerAudio;
PlayerMovement playerMovement;
bool isDead;
bool damaged;
void Awake ()
{
anim = GetComponent <Animator> ();
playerAudio = GetComponent <AudioSource> ();
playerMovement = GetComponent <PlayerMovement> ();
currentHealth = startingHealth;
}
void Update ()
{
if(damaged)
{
damageImage.color = flashColour;
}
else
{
damageImage.color =
Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime);
}
damaged = false;
}
public void TakeDamage (int amount)
{
damaged = true;
currentHealth -= amount;
healthSlider.value = currentHealth;
playerAudio.Play ();
if(currentHealth <= 0 && !isDead)
{
Death ();
}
}
void Death ()
{
isDead = true;
anim.SetTrigger ("Die");
playerAudio.clip = deathClip;
playerAudio.Play ();
playerMovement.enabled = false;
}
public void RestartLevel ()
{
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:
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:
using UnityEngine;
using System.Collections;
public class EnemyAttack : MonoBehaviour
{
public float timeBetweenAttacks = 0.5f;
public int attackDamage = 10;
Animator anim;
GameObject player;
PlayerHealth playerHealth;
bool playerInRange;
float timer;
void Awake ()
{
player = GameObject.FindGameObjectWithTag ("Player");
playerHealth = player.GetComponent <PlayerHealth> ();
anim = GetComponent <Animator> ();
}
void OnTriggerEnter (Collider other)
{
if(other.gameObject == player)
{
playerInRange = true;
}
}
void OnTriggerExit (Collider other)
{
if(other.gameObject == player)
{
playerInRange = false;
}
}
void Update ()
{
timer += Time.deltaTime;
if(timer >= timeBetweenAttacks &&
playerInRange && enemyHealth.currentHealth > 0)
{
Attack ();
}
if(playerHealth.currentHealth <= 0) {
anim.SetTrigger ("PlayerDead"); } }
void Attack () {
timer = 0f;
if(playerHealth.currentHealth > 0)
{
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:
using UnityEngine;
public class EnemyHealth : MonoBehaviour
{
public int startingHealth = 100;
public int currentHealth;
public float sinkSpeed = 2.5f;
public int scoreValue = 10;
public AudioClip deathClip;
Animator anim;
AudioSource enemyAudio;
ParticleSystem hitParticles;
CapsuleCollider capsuleCollider;
bool isDead;
bool isSinking;
void Awake ()
{
anim = GetComponent <Animator> ();
enemyAudio = GetComponent <AudioSource> ();
hitParticles = GetComponentInChildren <ParticleSystem> ();
capsuleCollider = GetComponent <CapsuleCollider> ();
currentHealth = startingHealth;
}
void Update ()
{
if(isSinking)
{
transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
}
}
public void TakeDamage (int amount, Vector3 hitPoint)
{
if(isDead)
return;
enemyAudio.Play ();
currentHealth -= amount;
hitParticles.transform.position = hitPoint;
hitParticles.Play();
if(currentHealth <= 0)
{
Death ();
}
}
void Death ()
{
isDead = true;
capsuleCollider.isTrigger = true;
anim.SetTrigger ("Dead");
enemyAudio.clip = deathClip;
enemyAudio.Play ();
}
public void StartSinking ()
{
GetComponent <NavMeshAgent> ().enabled = false;
GetComponent <Rigidbody> ().isKinematic = true;
isSinking = true;
ScoreManager.score += scoreValue;
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.
- 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…
and pass that into the GunBarrelEnd
Game Object which is the child of Player
.
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:
using UnityEngine;
public class PlayerShooting : MonoBehaviour
{
public int damagePerShot = 20;
public float timeBetweenBullets = 0.15f;
public float range = 100f;
float timer;
Ray shootRay;
RaycastHit shootHit;
int shootableMask;
ParticleSystem gunParticles;
LineRenderer gunLine;
AudioSource gunAudio;
Light gunLight;
float effectsDisplayTime = 0.2f;
void Awake ()
{
shootableMask = LayerMask.GetMask ("Shootable");
gunParticles = GetComponent<ParticleSystem> ();
gunLine = GetComponent <LineRenderer> ();
gunAudio = GetComponent<AudioSource> ();
gunLight = GetComponent<Light> ();
}
void Update ()
{
timer += Time.deltaTime;
if(Input.GetButton ("Fire1") && timer >= timeBetweenBullets)
{
Shoot ();
}
if(timer >= timeBetweenBullets * effectsDisplayTime)
{
DisableEffects ();
}
}
public void DisableEffects ()
{
gunLine.enabled = false;
gunLight.enabled = false;
}
void Shoot ()
{
timer = 0f;
gunAudio.Play ();
gunLight.enabled = true;
gunParticles.Stop ();
gunParticles.Play ();
gunLine.enabled = true;
gunLine.SetPosition (0, transform.position);
shootRay.origin = transform.position;
shootRay.direction = transform.forward;
if(Physics.Raycast (shootRay, out shootHit, range, shootableMask))
{
EnemyHealth enemyHealth = shootHit.collider.GetComponent <EnemyHealth> ();
if(enemyHealth != null)
{
enemyHealth.TakeDamage (damagePerShot, shootHit.point);
}
gunLine.SetPosition (1, shootHit.point);
}
else
{
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:
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 && 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.
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:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class ScoreManager : MonoBehaviour
{
public static int score;
Text text;
void Awake ()
{
text = GetComponent <Text> ();
score = 0;
}
void Update ()
{
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.
using UnityEngine;
public class EnemyHealth : MonoBehaviour
{
public int startingHealth = 100;
public int currentHealth;
public float sinkSpeed = 2.5f;
public int scoreValue = 10;
public AudioClip deathClip;
Animator anim;
AudioSource enemyAudio;
ParticleSystem hitParticles;
CapsuleCollider capsuleCollider;
bool isDead;
bool isSinking;
void Awake ()
{
anim = GetComponent <Animator> ();
enemyAudio = GetComponent <AudioSource> ();
hitParticles = GetComponentInChildren <ParticleSystem> ();
capsuleCollider = GetComponent <CapsuleCollider> ();
currentHealth = startingHealth;
}
void Update ()
{
if(isSinking)
{
transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
}
}
public void TakeDamage (int amount, Vector3 hitPoint)
{
if(isDead)
return;
enemyAudio.Play ();
currentHealth -= amount;
hitParticles.transform.position = hitPoint;
hitParticles.Play();
if(currentHealth <= 0)
{
Death ();
}
}
void Death ()
{
isDead = true;
capsuleCollider.isTrigger = true;
anim.SetTrigger ("Dead");
enemyAudio.clip = deathClip;
enemyAudio.Play ();
}
public void StartSinking ()
{
GetComponent <NavMeshAgent> ().enabled = false;
GetComponent <Rigidbody> ().isKinematic = true;
isSinking = true;
ScoreManager.score += scoreValue;
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:
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:
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:
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:
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:
using UnityEngine;
public class GameOverManager : MonoBehaviour
{
public PlayerHealth playerHealth;
public float restartDelay = 5f;
Animator anim;
float restartTimer;
void Awake ()
{
anim = GetComponent <Animator> ();
}
void Update ()
{
if(playerHealth.currentHealth <= 0)
{
anim.SetTrigger ("GameOver");
restartTimer += Time.deltaTime;
if(restartTimer >= restartDelay)
{
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.
CodeProject