Introduction
Finally, back to the original coding on Day 3. I left off creating the background, the player object, and the ability to shoot!
Some of the core topics that were talked about in today’s tutorial include:
- Creating a boundary box to delete objects
- Creating enemies/obstacles
Let’s get started!
Boundaries, Hazard, and Enemies
Boundary
Leaving off from last time, we created bullets that would fire off from the ship, but if you were to look at the game hierarchy pane, you would see a lot of the bullet objects would just remain there.
The more you shoot, the more you’ll have. So what gives?
If you were to pause the game and go to the Scene tab, you’ll see that the bullets actually keep going, never disappearing.
Is this a problem? You bet it is! The more GameObject
we instantiate, the more Unity has to calculate, which means our performance will suffer!
What was done to solve this problem was to create a giant cube that covers over the scene.
I attached a script to this cube and I added:
using UnityEngine;
using System.Collections;
public class DestroyByBoundary : MonoBehaviour
{
void OnTriggerExit(Collider other)
{
Destroy(other.gameObject);
}
}
What we’re relying on for this is the OnTriggerExit()
function. As you might suspect, by the name, the function gets called when a collider leaves the object it’s colliding with.
When we trigger the code, we would Destroy()
the object, which in this case is the laser.
Afterwards, we attach this script, you’ll see that the lasers disappears.
Creating Hazards
In the next video, we learn how to create asteroids that will fly down at the player.
We:
- Used the provided asteroid model to create the
GameObject
- Attached a capsule collider component to it
- Adjusted the collider to much the asteroid shape as much as possible
- Added a
Rigidbody
component and made it a trigger - Added the provided
RandomRotator
script to the asteroid
using UnityEngine;
using System.Collections;
public class RandomRotator : MonoBehaviour
{
public float tumble;
void Start ()
{
GetComponent<RigidBody>().angularVelocity = Random.insideUnitSphere * tumble;
}
}
AngularVelocity
AngularVelocity
is the speed of how fast the object rotates.
In the video, we’re using AngularVelocity
to create a random rotation of the object.
We do this by using Unity’s random function. Specifically, we chose to use insideUnitSphere
to create a random position Vector
that’s inside the gameobject
and multiply it by the speed we want the asteroid to roll.
Destroy Asteroids When Shot
Now that we have our first “enemy
”, we want to be able to shoot and get rid of it!
When our laser touches the asteroid, nothing would happen, and that’s because both the asteroids are a trigger so they don’t collide with each other.
What we have to do at this point is add a Script
to our object that have logic that deals with what happens when it collides with another object.
We attach this script to our asteroid
class:
using UnityEngine;
using System.Collections;
public class DestroyByContact : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.tag == "Boundary")
{
return;
}
Destroy(other.gameObject);
Destroy(gameObject);
}
}
We’re already familiar with this code. When our asteroid runs into something, it’ll destroy both the other object and itself.
An interesting thing here is that we check to see if the object we run into is the Boundary box that we created and if it is, we stop our code.
It’s important that we check for the boundary, because if we don’t, the first thing that’ll happen when the game loads is that the asteroid will collide with the boundary and they’ll both be destroyed.
To solve this, the video created a tag called Boundary
and attached it to the Boundary GameObject
. With this, whenever the asteroid collides with the Boundary GameObject
, we’ll end the function call and nothing will happen.
Explosions
In the next video, we added some more special effects, specifically what happens when the asteroid gets hit.
Opening up the DestroyByContact
script that was created previously, the video made some changes:
using UnityEngine;
using System.Collections;
public class DestroyByContact : MonoBehaviour
{
public GameObject explosion;
public GameObject playerExplosion;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Boundary")
{
return;
}
Instantiate(explosion, transform.position, transform.rotation);
if (other.tag == "Player")
{
Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
}
Destroy(other.gameObject);
Destroy(gameObject);
}
}
In the code, 2 GameObject
s were made public
variables. These are the explosion effects the tutorial provided: one is the asteroid explosion and the other is the player explosion.
Similar to how we create a new bullet GameObject
, we Instantiate()
an explosion GameObject
for the asteroid and if the asteroid collides with the player
object (we set a tag to it), we would also make the player blow up.
Once we added the code above to the script, I went back to the editor and attached my explosion effects to my script component.
Re-using Code
It’s also interesting to take note that in this video, we re-attached our Mover
script to our asteroid and set the speed to -5
.
As a result, instead of the Asteroid moving up like our bullet, it goes the opposite direction: down.
What’s important about this is that Scripts are re-usable components themselves, we don’t have to create a Script for every GameObject
, if an existing script already does something that’s needed, we can just re-use the same script with different values!
Game Controller
In the next video, we worked on creating a Game Controller. The game controller is responsible for controlling the state of the game, which in this case is generating asteroids.
For the GameController
script, we attached it to a new Empty GameObject
. We’ll call this game object GameController
and we create our GameController
script for it.
using UnityEngine;
using System.Collections;
public class GameController : MonoBehaviour
{
public GameObject hazard;
public Vector3 spawnValues;
void Start ()
{
SpawnWaves ();
}
void SpawnWaves ()
{
Vector3 spawnPosition = new Vector3
(Random.Range (-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
Quaternion spawnRotation = Quaternion.identity;
Instantiate (hazard, spawnPosition, spawnRotation);
}
}
Let’s go through this code for a bit.
We created some public
variables:
public GameObject hazard;
public Vector3 spawnValues;
hazard
is the asteroid
and spawnValues
are the range of locations where we would instantiate our Asteroids
.
We create a new function SpawnWaves()
and call it from the Start()
function. We’ll see why the video does this later, but reading the code in the function:
void SpawnWaves ()
{
Vector3 spawnPosition = new Vector3 (Random.Range
(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
Quaternion spawnRotation = Quaternion.identity;
Instantiate (hazard, spawnPosition, spawnRotation);
}
We create a Vector3
that represents the point we want to create an Asteroid
. We use Random.Range()
to create a randomize value between the 2
values we give it. We don’t want to change the Y
or Z
value of our GameObject
, so we only randomize our starting X
location (left and right)
Quarternion.identity
just means no rotation. What this means for our code is that we’re creating an Asteroid
at a random position and without rotation.
The reason why we don’t have a rotation is because that would interfere with the rotation that we already added for our Asteroid
script.
Spawning Waves
Currently, the code only generates one asteroid
. It’ll be a pretty boring game if the player only has to avoid one asteroid to win.
So next up, in this video, we create waves of asteroids that will come down for the player to dodge.
To do this, we could do something like copy and pasting more prefabs into Start()
in the GameController
script, however, not only does this make me cry a bit on the inside, it’ll also make it harder to make changes in the future.
Here’s what we ended up making:
using UnityEngine;
using System.Collections;
public class GameController : MonoBehaviour
{
public GameObject hazard;
public Vector3 spawnValues;
public int hazardCount;
public float spawnWait;
public float startWait;
public float waveWait;
void Start ()
{
StartCoroutine (SpawnWaves ());
}
IEnumerator SpawnWaves ()
{
yield return new WaitForSeconds (startWait);
while (true)
{
for (int i = 0; i &lt; hazardCount; i++)
{
Vector3 spawnPosition = new Vector3 (Random.Range
(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
Quaternion spawnRotation = Quaternion.identity;
Instantiate (hazard, spawnPosition, spawnRotation);
yield return new WaitForSeconds (spawnWait);
}
yield return new WaitForSeconds (waveWait);
}
}
}
Coroutine
Coroutines are functions that run your code, return and yield control back to rest of Unity, and then resumes again on the starting back once the condition for waiting has been met.
We can see more about this in the code above. That we have:
yield return new WaitForSeconds (spawnWait);
This means that we will wait whatever seconds to spawn a new enemy. However, if we were to do something like:
yield return null;
The code will execute immediately after the next frame.
Does that sound kind of familiar? That’s because they act very similarly to how Update()
works!
From my understanding, you can almost use coroutines to replace Update()
if you wanted to, but the main benefit of using them is to prevent cramming code inside Update()
.
If there is some code logic that we want to use only once a while, we can use coroutines instead to avoid unnecessary code from running.
Another thing to note, coroutines run outside the normal flow of your code. If you were to put something like this:
void Start()
{
StartCoroutine(test());
print("end start");
}
IEnumerator test()
{
for (int i = 0; i < 3; i++)
{
print("in for loop " + i);
yield return new WaitForSeconds(1);
}
}
Our console will print this:
In for loop 0
End start
In for loop 1
In for loop 2
And if we were to do something like this:
void Update()
{
StartCoroutine(test());
}
IEnumerator test()
{
for (int i = 0; i < 3; i++)
{
print("in for loop " + i);
yield return new WaitForSeconds(1);
}
}
We would have something like this
In for loop 0
In for loop 0
In for loop 0
In for loop 0
In for loop 0
In for loop 0
In for loop 0
In for loop 0
In for loop 0
… and so on for 1 second and then from there, we’ll have a mix of:
In for loop 0
and in for loop 1
This happens because we call the coroutine multiple times, specifically, once per frame and then after a second, the function will start printing when i = 1
while Update()
is still making new coroutine calls that print when i = 0
.
Besides the coroutine, the rest of the code is pretty straigthforward, we add a couple public
variables for waiting time.
Cleaning Up Explosions
Moving on from creating our waves, whenever our ship destroys an asteroid, we create an explosion. However that explosion never disappears. This is because these explosions never leave our boundary.
What we do is attach the DestroyByTime
script to the explosion. The script will destroy the explosion GameObject
after a set amount of time.
The code to do this is pretty straight forward.
using UnityEngine;
using System.Collections;
public class DestroyByTime : MonoBehaviour
{
public float lifetime;
void Start ()
{
Destroy (gameObject, lifetime);
}
}
Conclusion
Phew and that’s it for day 3, today we learned:
- How to use boundaries to clean up some of our
GameObject
leaving it - How to create, move, and destroy enemy waves
- How to use corroutines, which in some ways are similar to
Update()
I’m going to call it a day! In the next part of the series, we’ll be looking into creating UIs and audio to finish the space shooter game.
Day 2 | 100 Days of VR | Day 4