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

Day 3: Going through the Unity Space Shooter - Tutorial II

5.00/5 (2 votes)
10 Oct 2017CPOL8 min read 6.4K  
Going through the Unity Space Shooter - tutorial II

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.

Image 1

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.

Image 2

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:

C#
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
C#
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:

C#
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:

C#
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 GameObjects 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.

C#
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:

C#
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:

C#
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:

C#
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 &amp;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:

C#
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:

C#
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:

C#
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:

C#
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.

C#
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

License

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