Unity Engine - Space Shooter Tutorial

This article contains my rough notes from the Unity's introductory "Space Shooter" tutorial, which covers many of the same basic concepts as Roll a Ball with some additions (materials, audio, etc). The original tutorial is a short video series following the creation a top-down scrolling shooter click-by-click and line-by-line, with some very useful justifications for each step. You can play my version of the final game here, and find my source code on GitHub here.

Note that the original Space Shooter tutorial is a few months old as of this writing, and it does not represent the latest version of Unity. In particular, the tutorial's UI section is outdated so I wrote my own in "Counting Point and Displaying The Score" near the end of these notes.


In this tutorial we will create a top-down scrolling shooter, using imported assets (models, textures, audio, etc). By the end, the will be able to move a ship around on a 2D plane, shooting waves and scoring points. It will be possible for the ship to crash into an asteroid and die, in which case the game ends.

 

SETTING UP THE PROJECT

After creating a new blank project, the first thing we want to do is import the assets we'll be using for this project (the model for our player-controlled ship, etc). We'll get these from the Unity asset store. Oddly, I had to turn off "Use direct3d" in my project settings in order to actually view the store - otherwise it displays a blank screen on Unity's built-in browser. Apparently this is a widespread problem.

In this case, the import process is very simple. On unity's asset store website (https://www.assetstore.unity3d.com/en/#!/content/13866) you find the package you want, press "Open in Unity", which opens up the same page in Unity's browser. Presumably you could also browse the store from inside Unity to skip this step. Anyways, then you hit "download" and Unity pops up a hierarchical list of every asset in the package - you choose what you want and press Start, and Unity handles the rest.

Next we save our scene... nothing interesting here.

Next we set our build target to Web Player, as we did with Roll-a-ball.

Last, we go into Edit -> Project Settings -> Player and set resolution to 600x900. Then change the Game-view layout to Web so that we get an accurate representation of what the player would see in their web browser.


SETTING UP THE PLAYER

Drag our player-ship GameObject from the Assets->Models view into the Hierarchy.

Currently the player ship only has a few components - a Transform which controls its position and orientation, a Mesh and Mesh Renderer determining its shape and lighting, and a pair of shaders determining the appearance of that shape's "skin."

We want our ship to be movable and collidable according to physics, so we'll attach a Rigidbody component. We don't want the ship to fall according to gravity, so we'll deselect "Use Gravity."

Unlike in Roll-a-ball, where the player phere was a "primitive" which Unity intrinsicaly understood, Unity does not know how to handle collisions with our custom-made player ship. We need to tell Unity the shape of our ship by manually attaching a "collider" component. Specifically a "capsule collider" which is basically a cylinder with a sphere at either end. We can resize the spheres and pull them apart to fit nicely around our ship.

Capsule is one of several "primitive" colliders which Unity understands, but of course it wo't perfectly fit every object. Another option would be a "composite collider" which is a set of several primitive colliders all attached to the same object in slightly different positions. A third option is the "mesh collider" which detects collisions precicely according to the object's mesh. This is very performance-intensive, but can be improved by simplifying the mesh. The mesh collider and mesh renderer are allowed to used different meshes.

Colllisions in this game only need to trigger events, not bounce the ship off other objects, so we'll set the mesh collider to be a trigger collider.

Lastly, we'll add the prefab "engines_player" Particle System gameobject as a child of our player object. The transform of a child object is always relative to the parent's transform, so the engine flare will stay attached to our ship exactly as we want.

 

CAMERA AND LIGHTS

By default, Unity places the camera on the origin "behind" the origin, on the same horizontal plane. We want the camera to be aboe the ship looking own, so we'll reset its postion to origin, add some y-axis transform, and rotate it about the X-axis.

There are two subtly-different options for camera projection, "orthographic" and "perspective". Perspective simulates the human eye, seeing everything within a certain angle from a single point. Orthographic does not use a field-of-view, rather it displays everything "as is" within a square whose size we can control. If perspective is an infinitely-long cone, orthographic is an infinitely-long cube. For this project we'll be using orthographic style, to emulate the arcade space shooters of yore. Tune the camera's orthographic size and transform until it feels right. I increased the size (effectively zooming out) and moved the camera ahead of the ship).

As part of the camera gameobject, we can set the background of the scene. For now, set it to solid black. Notice how we can still see the ship even on a black background with no lights? This is due to the default "ambient lighting" feture under Edit -> Render Settings. By default this is a dark grey, which gives a dim undirectional light to every object in the scene. We can hide this light by setting it to solid black.

We'll create three lights: main, fill and rim. We want the main light to emulate a sun, so we'll rotate it about the x and y axes to shine on the ship from one side, slightly above and ahead. Set the color to white and increate the intensity, to emulate a sun. The fill light should show the opposite side of the ship, so we'll give it an opposite rotation on the y-axis. We want this light to be subtle, so we'll cut its intensity and change it from white to a light blue.  Now we have two lights above and in front of the ship, so we want a rim light behind and below the ship to balance it out. Again, this should be subtle so cut its intensity even further and use a light blue color. Lastly, we'll organize our lights by making them children of an empty gameobject, resetting the transform and moving it out of the way. Moving the parent gameobject will not affect the look of our lighting, because "directional" lights light the entire scene based only on rotation.


ADDING A BACKGROUND

Currently the background for our game is flat black, which we want to replace with something more interesting. First, we will create a "Quad" gameobject which will hold our background. This is essentially a single-sided square formed by two triangles (whereas a "Plane" may be made of many more triangles... don't ask me why.). We'll move our quad to the origin and rotate it about the X-axis, so that it can be seen from above. We won't need this quad to be a collider so we can remove that default component.

To make the Quad interesting to look at, we will need to apply a texture. Some textures are included in the premade assets for this project, and the one we want is "tile_nebula_green_dff." For unity to apply a texture, it must make a "material" out of that texture, and then use that material in the Mesh Renderer component of the quad. Simply dragging an image from the Assets view onto the quad in the Scene view will do all of this automatically.

By default, the entire image will be resized to fit on the quad, which can make it look squashed if the image has a different shape than the quad. To fix that, we should scale up the quad until it fills our game-view and fits the proportions of the image. We can see that the image we are using has a resolution of 1024x2048, so whatever scale we apply to the quad should follow that 1:2 ratio in order to keep the image undistorted. Use whatever scale makes the image fill your game view, which for me was 16x32.

Next, we want to make the background a bit brighter. By default Unity applies the "diffuse" shader, which is matte rather than glossy. The directional lights in our scene shine onto the background at a very shallow angles, and this combined with the matte shader mean the background is very dark. We don't want to change the existing lights as they already suit our ship, and it would be wasteful to add new unique lights on a separate layer just to hit this one quad. It is most efficient to just change the material shader being used for the background image. For this image, we really don't want or need any special lighting, we just want the image to appear exactly as it was created: change the shader from Diffuse to Unlit->Texture.

Lastly, notice that the player ship is currently buried halfway through the background quad, because both of them are at origin. We'll move the background quad down the Y-axis a few units to get it out of the way. Thanks to our orthographic camera, the Y-axis position of the ship and background will not affect how they look to the player.


MOVING THE PLAYER

To get our player ship moving, we need to attach a script to the Player gameobject much like we did for Roll-a-ball. Select the Player gameobject, click "Add Component" -> "New Script", and make it a C# script called "PlayerController". We'll edit the script in Unity's MonoDevelop environment, by double-clicking on the script.

Now, we want the player to move according to physics, much like we did with Roll-a-ball. When the player presses a key left or right, a force should push the ship left or right. These actions should be performed whenever Unity's physics engine updates, so we will place these actions in the "FixedUpdate()" function.

The following code snippet will give us the results of whatever input method is set up in Unity's input manager. By default, inputs are mapped to the WASD keys.

        float moveHorizontal = Input.GetAxis ("Horizontal");
        float moveVertical = Input.GetAxis ("Vertical");

The next code snippet maps those inputs to a Vector3 object which can be understood by our Player gameobject's RigidBody component. We put 0.0f in the y-axis because we do not want the ship to move on that axis.

        Vector3 movement = new Vector3 (moveHorizontal, 0.0f, moveVertical);
        rigidbody.velocity = movement;


Back in Unity, we can press play and see that our ship moves.. albeit slowly. We want to control the speed of our ship, so we'll go back to the script and add a public global variable named "speed." SInce this is a public variable, we will be able to set its value in the Inspector view in Unity. Now we need to apply speed to our ship's movement.

        rigidbody.velocity = movement * speed;

One quick note: unlike in Roll-a-ball we do not need to multiply by Time.deltaTime to remove the factor of the player's framerate. In Space Shooter, we are directly setting the player ship's velocity rather than imparting a force. If a user holds down their "A" key for a full second at 10 FPS versus 60 FPS, we'll simply be re-setting the velocity to the same value 10 or 60 times during that second... which has no effect regardless of frequency. In Roll-a-ball, we applied physical forces rather than directly setting a velocity, and so every repeated application of force would further increase the object's speed. One second with 60 pushes per second would move the object much faster than 10 pushes per second, unless we balance the size of each push by using Time.deltaTime.


Testing again, we notice that our ship is able to move off the edges of the screen, because we never constrained it. Back in our script, we'll declare four more public global variables: xMin, xMax, zMin and zMax, which will represent the edges of the player's movement space. In our FixedUpdate() function, once we've determined where the player wants to move we need to ensure that the ships actual position does not go beyond those boundaries. We can do so with the following code snippet:

        rigidbody.position = new Vector3
        (
            Mathf.Clamp(rigidbody.position.x, xMin, xMax),
            0.0f,
            Mathf.Clamp(rigidbody.position.z, zMin, zMax)
        );

The Mathf.Clamp function above takes a number, a minimum and a maximum. If the number is between minimum and maximum, the number is returned. If the number is above max, max is return and if below min, min is returned.

So now we can restrict the movement of our player ship, great. However, eventually we will want to apply those same restrictions to other objects, and in Unity's Inspect view those four public variables are taking up a lot of space. To make this code reusable and get it out of the way, we'll put these variables into their own "Boundary" class, create an instance of Boundary in the PlayerController class, and reference those variables through the instance.

    public class Boundary {
        public float xMin, xMax, zMin, zMax;
    }

    ...

    public Boundary boundary;

    ...

    rigidbody.position = new Vector3
            (
                Mathf.Clamp(rigidbody.position.x, boundary.xMin, boundary.xMax),
                0.0f,
                Mathf.Clamp(rigidbody.position.z, boundary.zMin, boundary.zMax)
            );

Now, when we switch back to the Unity inspector, we cannot see the Boundary class or its public variables, meaning we cannot set them. We cannot see them because the Boundary class is not serialized... meaning the data from that class is not given to the Inspector. The default PlayerController class inherits its own serialized stats from MonoBehaviour, but for our custom Boundary class we must add that status manually, like so:

    [System.Serializable]
    public class Boundary {
        ...


Returning to the Unity inspector, we see a "Boundary" tab in the Player Controller component, which we can expand to find XMin, XMax, ZMin and ZMax. As for their values, I went with -6, 6, -4 and 8. Testing the game shows that the ship now stops at those boundaries.


Finally, we'll add some tilt when the ship moves on the X-axis, as if the ship were banking. In the PlayerController script, we'll add a public tile variable, and a new snipped to the end of FixedUpdate()

    rigidbody.rotation = Quaternion.Euler (0.0f, 0.0f, rigidbody.velocity.x * -tilt);

"Quaternion" is Unity's internal method of storing and processing angles, and uses four values (X, Y, Z and W) and which I cannot really visualize. "Euler" translates that angle into the more comfortable Euler format (X, Y, and Z) which is what we normally use in Unity's Inspector view. Euler format suffers some issues ("Gimbal Lock") when used continuously to calculate changes in angle, which is why Unity converts back to Quaternion for its own processing... but the full explanation requires some hardcore math which I cannot deal with at this time of night. Anyways, we specify an Euler angle with no rotation on the X and Y axes, and rotate about the Z axis according to the direction our ship is moving. Back in the Inspector I set tilt = 4, and in testing it looks good.


CREATING SHOTS

Our player ship needs to be able to shoot at enemies, so have to create shots.

We'll start with an empty gameobject named "Bolt" which will be the parent for both the logical and visual portions of our shots. This way, we can reuse the logic for different types of weapons while replacing the visuals.

Next we'll create the visual portion, by creating a Quad as a child of Bolt, and rotating that quad about the X-axis so that it faces up towards the camera. Then we need to apply a texture to the quad, but we do not want to let Unity create a default material for the texture this time.

We'll create a new material in the Materials folder, by right-clicking in the assets view and selecting Create->Material. Then with the new material open in the Inspector view, we'll drag the texture fx_lazer_orange from the Textures directory onto the texture slot of the material in the Inspector. Now simply drag our material onto the VFX quad which we created earlier.

In the game view, our shot looks like a bright bolt on a flat black square, which is not what we want. We want the black to become transparent. We can achieve this by switching the VFX mesh renderer's shader from "Diffuse" to "Particles->Additive". When the additive shader is used, Black is left out of the scene and White is strengthened, and then all other colors are added on top. This will get rid of the black square and make our bolt stand out very clearly. Note, "Mobile" shaders are generally more efficient but may lose some customization or visual quality. But for our game, the mobile additive shader looks fine.

Now for movement and collision. First we'll add a RigidBody component and deselect gravity, because we don't want the shot to fall through the background. Next, in the VFX quad, notice how a Mesh Collider is already present. Quads are created by default with a mesh collider, but we don't want one here. The quad is much larger than the actual shot that we want, so using a mesh collider on the quad would lead to collisions even when the shot appears to miss. So we'll remove the Mesh Collider component from VFX.

So the collider we really want is a Capsule Collider, scaled to the size and shape of our bolt. We'll add this component to the Bolt gameobject, then rotate and and scale it by eye. I set it to run along the Z-Axis, with radius 0.035 and height 0.55.

Finally we need to create our logic, in a script component. For now, we just want our bolt to move forward as soon as it is fired... meaning as soon as a copy of Bolt is created. We can achieve this by simply setting the RigidBody's velocity to "transform.forward" in the Start() function. Transform.forward will move the object along its local Z-axis without moving on X or Y, and that is exactly what we want. To let us tune the speed of the bolt, we add a public "speed" variable and multiply the velocity by it.

    public float speed;

    ...

    void Start () {
        rigidbody.velocity = transform.forward * speed;
    }

Test it and choose a comfortable speed value, I went with 15. Note that because we are setting the velocity directly rather than imparting a physical force, the player's framerate will not affect the movement of the bolt... meaning we don't need to multiply by Time.deltaTime.

We will want to make many copies of Bolt eventually, so well save it as a Prefab by dragging "Bolt" into the Prefabs folder in the assets view. And we only want a bolt to be created when the player fires, so we should delete the original bolt from the scene.

A cool trick to test our Bolts even without any firing code is to launch the game, and then drag the Bolt prefab into the hierarchy view. This will create temporary instances of Bolt within our game, which disappear from the hierarchy when we stop playing.


SHOOTING SHOTS

We want our player to be able to shoot copies of the Bolt prefab, and to make this happen we'll need to modify our PlayerController script. We need to be watching for player input on every frame, so we'll use the "Update()" function which is called just before Unity draws a new frame, every frame.

We need to "Instantiate" a Bolt gameobject, and we'll use the Object.Instantiate(object, position, rotation) function. We know what object we want to create, but how do we determine a position and rotation?

We'll create a new empty gameobject named "Shot Spawn" and make it a child of the Player gameobject. We can then move Shot Spawn around relative to the player ship (ie, move it to the front of the ship), and then when the ship moves the Shot Spawn will move with it. I used position (0, -0.05, 1). Then we add public variables to our script, drag the Bolt prefab and Shot Spawn object into those variables in the Inspector, and add our logic.

    public GameObject shot;
    public Transform shotSpawn;

    void Update() {
            Instantiate (shot, shotSpawn.position, shotSpawn.rotation);
    }

At this point, playing the game would spawn a new shot every single frame. We want to check for player input every frame, but only spawn a shot if the player has pressed the appropriate key. Here's our updated code:

    public float fireRate;
    private float nextFire;

    void Update() {
        if (Input.GetButton ("Fire1") && Time.time > nextFire) {
            nextFire = Time.time + fireRate;
            Instantiate (shot, shotSpawn.position, shotSpawn.rotation);
        }
    }

This code does two things. It checks if the user has pressed the "Fire1" key as defined in Unity's input manager, and it checks if "fireRate" time has passed since the last shot was fired. This way, holding down the fire key will fire at a limited rate, rather than fire a new shot every frame. fireRate is public so that we can tune it from the Inspector view.

So, set a fire rate value, check the input manager to determine your fire key (default "Fire1" is left crtl) and try it out. Notice that as we play, the hierarchy view fills with clones of the "Bolt" gameobject because we do not despawn shots when they leave the play area.


BOUNDARY

Previously we created a boundary to sop our ship from moving outside of our scene. But for shots (and eventually hazards) we want destroy them rather than stop this.

First, create a Cube gameobject, which we will modify to act as our bounding box. We don't want this box to physically collide with objects but to simply trigger events, so we'll set "Is Trigger" in the Box Collider component. We'll also deactivate the mesh renderer compoennt so that we don't see the bounding box when playing the game.

Now we need to center the cube and scale it to cover everything the player would see. This is easiest to do by reenabling the cube's mesh renderer, checking the game view, and modifying the transform and scale until the cube covers everything.

Now for the functionality of the boundary, we need to add a script. We'll use the function "OnTriggerExit" which is called whenever some other object ceases colliding with the object we are scripting. The Unity API's sample code for this function does exactly what we want, so we can just copy it:

     void OnTriggerExit(Collider other) {
        Destroy(other.gameObject);
    }

Build and test, fire some shots and notice that when they leave the edge of the screen they disappear from the scene hierarchy. Great!


HAZARDS

We will create hazards for the player to shoot and avoid, in the form of asteroids flying down the screen.

First create an empty gameobject, name it "Asteroid" and reset it to origin. Then in Assets->Models, drag an asteroid model into the Asteroid gameobject. This will create a child gameobject representing the model, with its own transform, mesh and mesh renderer.

For logic, we want this asteroid to be collidable. We need to add a RigidBody component to the Asteroid gameobject, and disable gravity. The asteroid is oblong and has a fairly complex mesh, so we'll use a capsule collider instead of a mesh. Scale the capsule collider to fit, I went with radius 0.5 and Height 1.7.

Next we want to make the asteroid tumble through space at a random rotation speed. This will require a script component, so create one and name it "RandomRotator."

In the script we'll need a public float "tumble" to let us tune our rotation speed from the Unity inspector. Then to achieve randomization, we'll use "Random.InsideUnitSphere" which gives us a random Vector3 representing a point within a sphere of radius 1, centered at origin. This is convenient for us because Vector3 is also the type of our Asteroid's rigidbody angular velocity.

We want to set each asteroid's speed when the asteroid spawns, so we'll place our code inside the Start() function:

    public float tumble;

    void Start () {
            rigidbody.angularVelocity = tumble * Random.insideUnitSphere;
    }


Testing this code, the asteroid looks great at first but eventually its rotation slows and stops. This is caused by the default "Angular Drag" value of the rigidbody component, which gradually slows any angular rotation. We don't want our asteroids to stop rotating, so change the drag value from 0.05 to 0.

Next we'll let the player shoot the asteroid. Currently, both the player's shots and our asteroids are trigger colliders, so they will simply pass through eachother. We create another script for our Asteroid, "DestroyByContact".

This script will be very similar to our previous DestroyByBoundary, but we 'll be using "OnTriggerEnter" isntead of "OnTriggerExit".

    void OnTriggerEnter(Collider other){
        Destroy (other.gameObject);
        Destroy (gameObject);
    }

Now back in testing, there's a problem. As soon as we start the game, our asteroid disappears. Notice how our DestroyByContact code doesn't specify WHICH object it is colliding with... it simply destroys whenever any collision happens. And our bounding box has a collider covering the entire field, meaning it immediately destroys the asteroid (and itself) when the game starts. Notice that when you fire bolts, they don't disappear any more because the bounding box was also destroyed.

If we couldn't guess the problem, we could log the Asteroid's collision to the Unity console by adding the following line to DestroyByCollision:

    Debug.Log (other.name);

So, now for the solution. We know want to prevent collision with Boundary, so back in our script we can check if the object we are colliding with is Boundary. We can do that by looking at the object's name or its tag, but checking by tag is good practice (it makes our code reusable if we add that same tag to more objects). So we'll go into the Unity inspector for Boundary, create and add a tag named "Boundary." Then we'll rewrite our DestroyByContact script as follows:

    void OnTriggerEnter(Collider other){

            if(other.tag == "Boundary"){
                return;
            }

            Destroy (other.gameObject);
            Destroy (gameObject);
        }

Compile and test, and now the asteroid should remain last until we shoot it. And because the script only checks boundary and destroys ANY other collision, it will also destroy our ship if we fly into the asteroid. This is fine, I suppose.

 

EXPLOSIONS

Right now, when we shoot an asteroid both the asteroid and the bolt simply disappear. We want to add a visual explosion to make things more interesting. We only want to create an explosion when a shot destroys something, so we'll need to start with no explosion in the scene hierarchy and instantiate one in our DestroyByContact script:

    public GameObject explosion;

    ...

    Instantiate (explosion, transform.position, transform.rotation);

Back in the Unity inspector, we'll set "explosion" to the exlposion_asteroid prefab from Assets->VFX->Explosions. Then run the program, shoot the asteroid and watch as a cloned explosion is created.

Now, if we fly our ship into the asteroid then we get the exact same effect, which is a bit underwhelming for something as important as the player ship. We want to use a different explosion for this case.

In Assets->VFX->Explosions is another gameobject called explosion_player, and we'll start by creating a new public variable in DestroyByContact to store that gameobject. Call it "playerExplosion".

Next we need to check if the collided object is the player, so we'll add a "Player" tag to the player's ship gameobject in the Unity inspector, and then check for that tag in the script:

    if (other.tag == "Player") {
        Instantiate (shipExplosion, other.transform.position, other.transform.rotation);
    }


Now to make the asteroid a bit trickier, we'll make it move down the screen. We can reuse the "Mover" script from player shots, attaching it to Asteroid as a component and tuning the Speed value.


Our asteroid is ready now, and we'll want to reuse it many times, so drag it from the scene hierarchy into Assets->Prefabs for later use.


GAME CONTROLLER

Right now we have a lot of disparate objects with their own logic, but no overall "game"  logic. We want to be able to start and end the game, decide when to spawn enemies, and so forth. We'll create a new GameObject and attach scripts to perform these functions. Create an empty GameObject, asign the premade tag "GameController," and then create a new script.

 The main job of this script will be to spawn waves of hazards, so we'll create a new function called "SpawnWaves". We want this function to Instantiate hazard objects, and this requires three arguments: what object, what position and what rotation.

THe objct will be our Asteroid, we can just create a public "hazard" variable ad assign our Asteroid prefab to it in the Unity inspector.

Position is trickier, we want to be able to determine the Asteroid's y-axis and z-axis positions to ensure the asteroid can collide with the player ship and that the asteroid spawns ahead of the player (out of view). But we want the x-axis to be random within the boundary of the game. With those goals in mind, the code is fairly easy:

        public Vector3 spawnValues;

        ....

        Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);


As for rotation... well we don't care very much. Our Asteroids already rotate randomly on their own, so it doesn't really matter if we also apply a rotation here. Instantiate demands that we pass in a Quaternion to set the rotation, so we'll simply use the default "Quaternion Identity" which applies no rotation.

    Quaternion spawnRotation = Quaternion.identity;

So, the final script looks like this:

    using UnityEngine;
    using System.Collections;

    public class GameController : MonoBehaviour {

        public GameObject hazard;
        public Vector3 spawnValues;

        // Use this for initialization
        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);
        
        }

    }

Save the script and run our game, and there should be a single asteroid tumbling into view from the top edge of the screen. The asteroid can be shot, rammed, and it will despawn when it passes through the bottom of the screen.


SPAWNING WAVES

So we can spawn one Asteroid, but we want to spawn lots of them. We'll put our spawn code into a loop:

    public int hazardCount;

    ...

    for(int i = 0; i < hazardCount; i++){
        Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
        Quaternion spawnRotation = Quaternion.identity;
        Instantiate (hazard, spawnPosition, spawnRotation);
    }

This lets us spawn many enemies at once, but this will still only give is one big wave at the start of the game. What we really want is a delay after each spawn.

    public float spawnWait;

    ...

    void SpawnWaves () {

        for(int i = 0; i < hazardCount; i++){
            Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
            Quaternion spawnRotation = Quaternion.identity;
            Instantiate (hazard, spawnPosition, spawnRotation);
            WaitForSeconds(spawnWait);
        }
    }

Now, if we ran this code we would simply pause our entire game for the duration of spawnWait. We need to turn SpawnWaves into a "co-routine" which allows the rest of our code to continue running while this function is paused. To turn SpawnWaves into a co-routine, we must make it return "IEnumerator", and WaitForSeconds must become "yield return new WaitForSeconds." When SpawnWaves is called, we must call it as "StartCoroutine (SpawnWaves ())". While we're at it, we'll add a small delay to the start of SpawnWaves as well, to give the player a bit of time to get ready when the game starts.

Here's how the script looks:

    using UnityEngine;
    using System.Collections;

    public class GameController : MonoBehaviour {

        public GameObject hazard;
        public Vector3 spawnValues;
        public int hazardCount;
        public float spawnWait;
        public float startWait;

        // Use this for initialization
        void Start () {
            StartCoroutine(SpawnWaves());
        }
        
        IEnumerator SpawnWaves () {

            yield return new WaitForSeconds(startWait);
            for(int i = 0; i < 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);
            }
        }
    }


At this point, we can run the game with various numbers of hazards, have them spawn one-by-one, and shoot through them. It works nicely but eventually hazardCount runs out and there's nothing left to do. What we'll do is put our for-loop inside a "while" loop, so that we'll continuously spawn waves of hazards, with each wave containing hazardCount asteroids. We'll also add a delay between waves.

 

Running our game and shooting several waves of asteroids, a problem becomes obvious in the hierarchy view: We never destroy the explosions which spawn every time we shoot an asteroid. We need to create a new script which despawns explosions (and perhaps other things) after a certain amount of time has passed. Here's the script, which I named DestroyByTime:

    using UnityEngine;
    using System.Collections;

    public class DestroyByTime : MonoBehaviour {

        public float lifeTime;

        void Start(){
            Destroy (gameObject, lifeTime);
        }
    }


Return to Unity and navigate Assets->VFX->Explosions, select explosion_asteroid, add component -> script -> DestroyByTime. Then set the lifeTime value to some number of seconds. This would be useful functionality for all of our game's explosions, so we'll attach this script to the explosion_player and explosion_enemy prefabs as well.

Run the game again, shoot some asteroids and notice that the Explosion objects now disappear from the hierarchy. Crash into an asteroid and notice that the player explosion disappears from the hierarchy as well.


AUDIO

Unity has three main audio components: Audio Clips, Audio Sources and the Audio Listener. We'll focus on clips and sources.

Audio clips hold audio data/sound files.

Audio sources play our clips in the scene.

We have some audio clips already under Assets->Audio. We can preview these sounds in the Unity Inspector view, and chane some basic settings such as audio format or compression. For our simple 2D game, the only option we really care about is ensuring "3D sound" is NOT enabled.

To play our audio, we need to add an Audio Source component to one of our GameObjects, and then associate the Audio Source with a clip. We can do this very quickly by dragging a clip from the assets view onto a gameobject in the scene hierarchy. We can also drag audio clips onto prefabs which are themselves in the assets view.

First, let's set up the sound effect for an exploding asteroid. Simply drag the explosion_asteroid clip onto the explosion_asteroid prefab.  In the inspector view, with explosion_asteroid selected, we can see that an Audio Source component has been added with a reference to the explosion_asteroid audio clip. Notice the "Play On Awake" checkbox, this will play the audio clip as soon as the associated gameobject is instantiated. Enable this, so that the explosion sound will play when the visual explosion appears.

Repeat the above for the explosion_player audio clip and prefab effect.

Now for the sound of the player shooting. The easy solution is to just repeat the above process, attaching the weapon_player clip to our custom Bolt VFX so that whenever a bolt spawns (ie, when we fire) the firing sound is played. However, the tutorial chooses to go a more complicated route using a script, and for instruction's sake I will follow along:

Drag the weapon_player clip onto the Player gameobject, then deselect "Play On Awake" so that the clip does not play when the player spawns. In the PlayerController script, add "audio.Play ()" to the result of the firing if-statement. Note that if multiple audio sources are attached to a single gameobject, only the first source will be played.

    if (Input.GetButton ("Fire1") && Time.time > nextFire) {
        nextFire = Time.time + fireRate;
        Instantiate (shot, shotSpawn.position, shotSpawn.rotation);
        audio.Play();
    }    


Next is Background Music. We'll attach that audio to the GameController object and enable "Play On Awake" and "Loop," so that the audio plays immediately and continuously.

Upon testing, some of the audio sounds unbalanced. The weapons and background music are quote loud relative to the explosions, so in the Unity Inspector, we'll redice the "Volume" setting for the background music and player shots.


COUNTING POINTS AND DISPLAYING THE SCORE

As we saw in the Roll-a-ball project, this tutorial series is a bit out of date. The GuiText gameobject used in the tutorial has been deprecated, so I'll be creating my own solution:

Create a "Canvas" gameobject in the scene hierarchy and reset its position to origin, and then create a child "Text" gameobject. Adjust the Text's transform to put it in the top-left corner of the player view (my settings were top-left anchor, position 60/-60/0). Set the default text to "Score:". The text's default gray colour is difficult to see against the game's dark background, so change the text colour to something bright.

Now to hookup our game logic to the score text. In the GameController script we'll add a reference to our Text object, a variable to store our current score, and functions to modify the current store and update the displayed score. Then we'll modify the Start function to initialize our score to 0 when the game starts.

    public UnityEngine.UI.Text scoreText;
    public int score;

    ...

    void Start () {
        score = 0;
        UpdateScore ();
        StartCoroutine(SpawnWaves());
    }

    ...

    public void AddScore(int newScoreValue){
            score += newScoreValue;
            UpdateScore();
    }

    void UpdateScore(){
        scoreText.text = "Score: " + score;
    }

Next we need to call AddScore with an appropriate value whenever the player does something worthy of points... shooting an asteroid for example. Open up the DestroyByContact script.

We'll need a variable to reference our GameController instance, and then we'll need to call AddScore in that GameController.

    public GameController gameController;
    public int scoreValue;

    ...

    void OnTriggerEnter(Collider other){

        if(other.tag == "Boundary"){
            return;
        }

        Instantiate (explosion, transform.position, transform.rotation);

        if (other.tag == "Player") {
            Instantiate (shipExplosion, other.transform.position, other.transform.rotation);
        }
        gameController.AddScore (scoreValue);
        Destroy (other.gameObject);
        Destroy (gameObject);
    }


Now close the script, select the Asteroid prefab in Assets->Prefabs, and try to drag our instance of GameController into the script slot. It doesn't work, because our Asteroid is just a prefab. This asteroid could be reused in any scene, so it doesn't make sense to associate it with a single instance of GameController from a single scene. We'll have to wait until an instance of Asteroid is created, and then find an instance of GameController to reference. Every instance of Asteroid will have to do this, all at runtime, and to do this we need a script. Reopen DestroyByContact:


    private GameController gameController;

    ...

    void Start(){
        GameObject gameControllerObject = GameObject.FindWithTag ("GameController");
        if (gameControllerObject != null) {
            gameController = gameControllerObject.GetComponent <GameController> ();
        }
        if (gameController == null){
            Debug.Log("Cannot find 'GameController' script");
        }
    }


The above code is mostly self-explanatory. Note the syntax <GameController>, this is specifying the type "GameController" meaning it will only accept instances of the GameController class (even if someone puts the GameController tag on another object). Also note that we changed the gameController variable from public to private. Since we cannot set gameController from the Unity inspector, there was no benefit to having the variable public.

Two last bits of cleanup: Open the GameController script and set "Score" to private, since we don't need to modify it from the Unity inspector. Then check the Asteroid prefab and set its score value to some positive integer (I used 10).

Save and test. The score should now increment every time an asteroid is destroyed, whether by a shot or a collision with your ship.


ENDING THE GAME

At this point our game is playable but it runs forever. We need to end the game when the player is destroyed, and give the player an option to restart.

In the Canvas gameobject, create two new text gameobjects: GameOver and RestartText. Adjust their transforms and colours to your liking, the tutorial puts Game Over in the center and Restart in the upper right corner.

Open the GameController script and add references, states and initialization for these texts:

    public UnityEngine.UI.Text gameOverText;
    public UnityEngine.UI.Text restartText;

    private bool gameOver;
    private bool restart;

    ...

    void Start () {
        gameOver = false;
        restart = false;
        restartText.text = "";
        gameOverText.text = "";
        score = 0;
        UpdateScore ();
        StartCoroutine(SpawnWaves());
    }

Then we need to make a function which can be called to end our game:

    public void GameOver(){
        gameOverText.text = "Game Over";
        gameOver = true;
    }

And modify the SpawnWaves loop to respect the state of gameOver:

    while (true) {
            for (int i = 0; i < 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);

            if (gameOver){
                restartText.text = "Press 'R' for Restart";
                restart = true;
                break;
            }
        }

Now we need to actually check for the "R" keypress and cause a restart.

    void Update(){
        if (restart){
            if(Input.GetKeyDown (KeyCode.R)){
                Application.LoadLevel(Application.loadedLevel);
            }
        }
    }

In the code fragment above, Application.LoadLevel will load whichever scene we specify. We use Application.loadedLevel to specify the current scene.

Finally, we need to call GameOver() when the player ship is destroyed. Our asteroids already detect this, so that's where we'll put our call. Fortunately we already did the hard work of connecting each Asteroid instance to our GameController instance. Open DestroyByContact and call gameController.GameOver() when we destroy the player:

    void OnTriggerEnter(Collider other){

        if(other.tag == "Boundary"){
            return;
        }

        Instantiate (explosion, transform.position, transform.rotation);

        if (other.tag == "Player") {
            Instantiate (shipExplosion, other.transform.position, other.transform.rotation);
            gameController.GameOver();
        }
        gameController.AddScore (scoreValue);
        Destroy (other.gameObject);
        Destroy (gameObject);
    }

Now save, run the game, and crash into an asteroid. The "Game Over" text should immediately appear, and once the current asteroid wave ends you should see the "Restart" text as well. At this point, pressing R should restart the game.

Last, we'll make our lables a bit prettier. Select the Score and RestartText gameobjects in the scene hierarchy and set their Font Size to 20. Select the GameOver gameobject and set its Font Size to 25.

Test again, and our game is complete.


BUILDING THE GAME

This will be very similar to Roll-a-ball. Open File->Build Settings and select "Web Player", then press "Add Current" to add our current scene to the build, then press "Build."

Select a folder to place the build in, and continue. When the build is finished, check this folder and you should find two files: an HTML file which you can open to run your game, and a .Unity3D file which contains your game's data. The HTML file can be modified to suit your purposes, but by default both the HTML file and .Unity3D file must be in the same folder in order to run.

Done!