Chapter 13. Audio, Menus, Death, and Explosions!

The core gameplay of your space shooter is done, but it’s not a complete game yet. In order to be playable outside of Unity, you’ll need to add menus and other controls that let the player navigate around the game as an application. Finally, we’ll polish up the game by replacing the temporary art with higher-fidelity 3D models and materials.

Menus

Right now, the gameplay is entirely constrained to the editor’s Play button. When you start the game, you’re in the action immediately, and if your space station is destroyed, you have to stop the game and start it again.

To provide the player with a little more context for their game, we need to add menus. In particular, we need to add an especially important button: “New Game.” If the space station is destroyed, we need a way to let the player start again.

Adding the menu structure to a game goes a long way toward making the game feel complete. We’ll be adding four components as part of the menus:

Main Menu

This screen presents the game’s title, and shows the New Game button.

Paused screen

This screen shows the text “Paused,” and contains a button to unpause.

Game Over screen

This screen shows Game Over and the New Game buttons.

In-Game UI

This screen contains the joystick, indicators, Fire button, and everything else that the player actually sees while playing the game.

Each of these UI groups will be exclusive—only one will appear at a time. The game will start with the Main Menu, and when you click on the New Game button, the menu will disappear and be replaced with the In-Game UI (in addition to the actual game action being started).

Note

Unity’s UI system lets you test your menu using your computer’s mouse or touchpad. However, you should still test how the menu feels on a real touchscreen as you develop it, such as through the Unity Remote app (see “Unity Remote”.)

The first step in this process is to bring the In-Game UI components together into a single object, so that it can be managed all at once.

  1. Create the In-Game UI container. Select the Canvas object, and make a new empty child object. Name this object “In-Game UI”.

  2. Configure the container. Make In-Game UI’s anchors to stretch horizontally and vertically, and set the left, top, bottom, and right margins to zero. This makes it fill the entire canvas.

Next, we’ll bring all of the existing UI elements together into the container.

  1. Group the game’s UI. Select every child of the canvas except the In-Game UI container, and move it into In-Game UI.

We’ll now start building the other menus. Before we begin, it’s helpful to turn off the In-Game UI, so that it doesn’t distract from the other UI content you’re about to build.

  1. Disable the In-Game UI. Select the In-Game UI object, and disable it by clicking the checkbox at the top-left of the Inspector. When you’re done, it should look like Figure 13-1.

mgdu 1301
Figure 13-1. The In-Game UI, shown disabled; also note the size and position of the object, which is set to fill the entire canvas with no border

Main Menu

The content of the Main Menu is very simple—it’s a text label that shows the game’s title (“Rockfall”), and a button that creates a new game.

Much like the In-Game UI, the Main Menu will be composed of an empty container object, with all of the UI components that belong to the menu added as a child.

  1. Create the Main Menu container. Create a new empty game object, and make it a child of the Canvas. Name it “Main Menu”.

    Make it fill the entire canvas by setting it to stretch horizontally and vertically. Set all of the margin values to zero.

  2. Create the title label. Create a new Text object by opening the GameObject menu, and choosing UI → Text. Make it a child of the Main Menu, and name it “Title”.

    Set the anchor of this new Text object to Center Top. Set the Pos X value to 0, and the Pos Y to -100. Set the height to 120, and the width to 1024.

    Next, you’ll need to set up the text itself. Set the text’s color to the hex color #FFE99A (slightly yellow-y), the text’s alignment to center, and the text itself to “Rockfall”. Additionally, turn on the Best Fit setting. This will make the text automatically size itself to fit the Text object’s boundaries. Finally, drag the At Night font into the Font Slot.

  3. Create the Button. Create a new Button object, and name it “New Game”. Make it a child of the Main Menu.

    Set the anchors of the button to be center top, and set the X and Y position values to [0, -300]. Set its height to 330, and its height to 80.

    Set the Source Image of the button to the Button sprite, and set its Image Type to Sliced.

    Select the Text child object, and change its text value to “New Game”. Set the Font as CRYSTAL-Regular, the Font Size to 28, and the Color as 3DFFD0FF.

When you’re done, the menu should look like Figure 13-2.

Before you continue, disable the Main Menu container.

Paused Screen

The Paused screen shows the text “Paused”, along with a button to unpause the game. To build it, follow the same steps as you did for the Main Menu, but with the following changes:

  • The container object should be called “Paused”.

  • The text of the Title object should be “Paused”.

  • The button object should be called “Unpause Button”.

  • The text of the button should be “Unpause”.

When you’re done, the Pause menu should look like Figure 13-3.

mgdu 1303
Figure 13-3. The Pause menu

Disable the Paused container before building the final menu: the Game Over screen.

Game Over Screen

The Game Over screen shows the text “Game Over”, along with a button to start the game again. The Game Over screen will appear when the space station is destroyed, which will end the game.

Again, follow the same steps as for the Main Menu and Paused screens, but with the following changes:

  • The container object should be called “Game Over”.

  • The text of the Title object should be “Game Over”.

  • The button object should be called “New Game Button”.

  • The text of the button should be “New Game”.

When you’re done, the Game Over screen should look like Figure 13-4.

mgdu 1304
Figure 13-4. The Game Over menu
Note

All three of these new menus are pretty much identical, and you might be wondering why you’ve done the same work three times. The reason is that you’ll want to customize them later, and breaking them apart now will save you work later.

There’s one last UI component that we need to add to the game, and that’s a way to Pause the game.

Adding a Pause Button

The Pause button will appear at the top-right of the In-Game UI, and will signal to the game that the user wants to stop the action for a moment.

To build the Pause button, you first need to create a new Button object, and make it a child of the In-Game UI container object. Name it “Pause Button”.

Set the anchors of the Pause button to the top-right, and set the X and Y position values to [-50, -30]. Set the width to 80, and the height to 70.

Set the Source Image of the Image component to the Button sprite.

Set the text of the Text child object to “Pause”. Set its font to CRYSTAL-Regular, and its size to 28. Set its color to #3DFFD0FF.

Congratulations! Your UI is all done. However, none of the buttons that you’ve set up work correctly. In order for it all to work, you’ll need to add a Game Manager to coordinate everything.

Game Manager and Death

The Game Manager, much like the Input Manager and the Indicator Manager, is a singleton object. The Game Manager has two main jobs:

  • Managing the state of the game and the menus, and

  • Spawning the ship and station

When the game starts, the game will be in an unstarted state. The ship and station won’t be in the scene, and the asteroid spawner won’t be creating an asteroids. Additionally, the Game Manager will display the Main Menu container object, and hide every other menu.

When the user taps the New Game button, the In-Game UI will be displayed, the ship and station created, and the asteroid spawner will be told to start creating asteroids. Additionally, the Game Manager will set up some important elements of the game: the Camera Follow script will be told to follow the new Ship object, and the Asteroid Spawner will be told to aim its asteroids at the Space Station.

Finally, the Game Manager will handle the Game Over state. You might recall from earlier that the DamageTaking script has a checkbox called “Game Over On Destroyed”. We’ll be setting that up to instruct the Game Manager to end the game whenever the object that the script is attached to is destroyed, if the checkbox is on. Ending the game is simply a matter of turning off the asteroid spawner, and destroying the current ship (and the station, if it happens to still be around).

Before we get started building the Game Manager, we need to be able to create multiple copies of the Ship and the Station. This requires turning both of these objects into prefabs, and also defining the positions at which they’ll both appear.

Turn the Ship and Space Station into prefabs. Drag and drop the Ship into the Project pane to create the prefab, and then remove it from the scene. Repeat this process for the Space Station.

Start Points

We’ll now create two marker objects, which will serve as indicators for where the Ship and Space Station should be created when a new game starts. These indicators won’t be visible to the player, but we’ll make them visible to you inside the editor.

  1. Create the Ship position marker. Create a new empty game object, and name it “Ship Start Point”.

    Click the icon at the top-left of the Inspector and choose the red label (see Figure 13-5). The object will now appear in the scene view, despite being invisible to the player.

    Position the marker where you want the ship to appear.

mgdu 1305
Figure 13-5. Selecting a label for the ship’s start point
  1. Create the Space Station position marker. Repeat these steps, this time creating an object called “Station Start Point”. Position it where you want the space station to appear.

With this done, we’re now able to create and set up the Game Manager.

Creating the Game Manager

The Game Manager largely serves as a central point for storing critical information about the game, such as references to the current Ship and Space Station, as well as changing the state of important game objects when either a button is clicked or when a DamageTak⁠ing script reports that the game should end.

To set up the Game Manager, create a new empty game object called Game Manager, add a new C# script to it called GameManager.cs, and add the following code to the file:

  public class GameManager : Singleton<GameManager> {

    // The prefab to use for the ship, the place it starts from,
    // and the current ship object
    public Gameobject shipPrefab;
    public Transform shipStartPosition;
    public GameObject currentShip {get; private set;}

    // The prefab to use for the space station, the place
    // it starts from, and the current ship object
    public GameObject spaceStationPrefab;
    public Transform spaceStationStartPosition;
    public GameObject currentSpaceStation {get; private set;}

    // The follow script on the main camera
    public SmoothFollow cameraFollow;

    // The containers for the various bits of UI
    public GameObject inGameUI;
    public GameObject pausedUI;
    public GameObject gameOverUI;
    public GameObject mainMenuUI;

    // Is the game currently playing?
    public bool gameIsPlaying {get; private set;}

    // The game's Asteroid Spawner
    public AsteroidSpawner asteroidSpawner;

    // Keeps track of whether the game is paused or not.
    public bool paused;

    // Show the main menu when the game starts
    void Start() {
      ShowMainMenu();
    }

    // Shows a UI container, and hides all others.
    void ShowUI(GameObject newUI) {

      // Create a list of all UI containers.
      GameObject[] allUI
        = {inGameUI, pausedUI, gameOverUI, mainMenuUI};

      // Hide them all.
      foreach (GameObject UIToHide in allUI) {
        UIToHide.SetActive(false);
      }

      // And then show the provided UI container.
      newUI.SetActive(true);
    }

    public void ShowMainMenu() {
      ShowUI(mainMenuUI);

      // We aren't playing yet when the game starts
      gameIsPlaying = false;

      // Don't spawn asteroids either
      asteroidSpawner.spawnAsteroids = false;
    }

    // Called by the New Game button being tapped
    public void StartGame() {
      // Show the In-Game UI
      ShowUI(inGameUI);

      // We're now playing
      gameIsPlaying = true;

      // If we happen to have a ship, destroy it
      if (currentShip != null) {
        Destroy(currentShip);
      }

      // Likewise for the station
      if (currentSpaceStation != null) {
        Destroy(currentSpaceStation);
      }

      // Create a new ship, and place it
      // at the start position
      currentShip = Instantiate(shipPrefab);
      currentShip.transform.position
        = shipStartPosition.position;
      currentShip.transform.rotation
        = shipStartPosition.rotation;

      // And likewise for the station
      currentSpaceStation = Instantiate(spaceStationPrefab);

      currentSpaceStation.transform.position =
        spaceStationStartPosition.position;

      currentSpaceStation.transform.rotation =
        spaceStationStartPosition.rotation;

      // Make the follow script track the new ship
      cameraFollow.target = currentShip.transform;

      // Start spawning asteroids
      asteroidSpawner.spawnAsteroids = true;

      // And aim the spawner at the new space station
      asteroidSpawner.target = currentSpaceStation.transform;

    }

    // Called by objects that end the game
    // when they're destroyed
    public void GameOver() {
      // Show the Game Over UI
      ShowUI(gameOverUI);

      // We're no longer playing
      gameIsPlaying = false;

      // Destroy the ship and the station
      if (currentShip != null)
        Destroy (currentShip);

      if (currentSpaceStation != null)
        Destroy (currentSpaceStation);

      // Stop spawning asteroids
      asteroidSpawner.spawnAsteroids = false;

      // And remove all lingering asteroids from the game
      asteroidSpawner.DestroyAllAsteroids();
    }

    // Called when the Pause or Resume buttons are tapped
    public void SetPaused(bool paused) {

      // Switch between the in-game and paused UI
      inGameUI.SetActive(!paused);
      pausedUI.SetActive(paused);

      // If we're paused..
      if (paused) {
        // Stop time
        Time.timeScale = 0.0f;
      } else {
        // Resume time
        Time.timeScale = 1.0f;
      }
    }

  }

The Game Manager script is bulky, but simple. It has two main functions: managing the appearance of the menus and In-Game UI and creating and destroying the space station and ship when the game begins and ends.

Let’s walk through what it does, one step at a time.

Initial setup

The Start method is called when the Game Manager first appears in the scene—that is, at the start of the game. The only thing that it does is cause the main menu to appear, calling ShowMainMenu.

  // Show the main menu when the game starts
  void Start() {
    ShowMainMenu();
  }

In order to show any UI, we use a method called ShowUI that handles the presentation of the desired UI object and the dismissal of all other UI objects. It does this by hiding all UI objects, and then un-hiding the desired UI element:

  // Shows a UI container, and hides all others.
  void ShowUI(GameObject newUI) {

    // Create a list of all UI containers.
    GameObject[] allUI
      = {inGameUI, pausedUI, gameOverUI, mainMenuUI};

    // Hide them all.
    foreach (GameObject UIToHide in allUI) {
      UIToHide.SetActive(false);
    }

    // And then show the provided UI container.
    newUI.SetActive(true);
  }

With this implemented, ShowMainMenu can be implemented. All it does is show the main menu UI (via ShowUI), and indicates to the game that gameplay isn’t currently happening, and that the asteroid spawner should not be spawning asteroids:

  public void ShowMainMenu() {
    ShowUI(mainMenuUI);

    // We aren't playing yet when the game starts
    gameIsPlaying = false;

    // Don't spawn asteroids either
    asteroidSpawner.spawnAsteroids = false;
  }

Starting the game

The StartGame method, which is called when the New Game button is tapped, shows the In-Game UI (which hides the other UI as a result), and sets up the scene for gameplay by removing any existing ship or station, and creating new ones. It also makes the camera start tracking the newly created ship, and tells the asteroid spawner to start throwing asteroids at the newly created station:

    // Called by the New Game button being tapped
    public void StartGame() {
      // Show the In-Game UI
      ShowUI(inGameUI);

      // We're now playing
      gameIsPlaying = true;

      // If we happen to have a ship, destroy it
      if (currentShip != null) {
        Destroy(currentShip);
      }

      // Likewise for the station
      if (currentSpaceStation != null) {
        Destroy(currentSpaceStation);
      }

      // Create a new ship, and place it
      // at the start position
      currentShip = Instantiate(shipPrefab);
      currentShip.transform.position
        = shipStartPosition.position;
      currentShip.transform.rotation
        = shipStartPosition.rotation;

      // And likewise for the station
      currentSpaceStation = Instantiate(spaceStationPrefab);

      currentSpaceStation.transform.position =
        spaceStationStartPosition.position;

      currentSpaceStation.transform.rotation =
        spaceStationStartPosition.rotation;

      // Make the follow script track the new ship
      cameraFollow.target = currentShip.transform;

      // Start spawning asteroids
      asteroidSpawner.spawnAsteroids = true;

      // And aim the spawner at the new space station
      asteroidSpawner.target = currentSpaceStation.transform;

    }

Ending the game

The GameOver method will be called by certain objects that, when they’re destroyed, end the game. It shows the Game Over UI, stops gameplay, and destroys the current ship and station. Additionally, asteroid spawning is stopped, and all remaining asteroids are removed. Essentially, we’re returning to the initial starting conditions of the game:

  // Called by objects that end the game when they're destroyed
  public void GameOver() {
    // Show the Game Over UI
    ShowUI(gameOverUI);

    // We're no longer playing
    gameIsPlaying = false;

    // Destroy the ship and the station
    if (currentShip != null)
      Destroy (currentShip);

    if (currentSpaceStation != null)
      Destroy (currentSpaceStation);

    // Stop spawning asteroids
    asteroidSpawner.spawnAsteroids = false;

    // And remove all lingering asteroids from the game
    asteroidSpawner.DestroyAllAsteroids();
  }

Pausing the game

The SetPaused method is called when either the Pause or Resume buttons are tapped. All it does is manage the display of the pause UI, and stop or resume the flow of time.

  // Called when the Pause or Resume buttons are tapped
  public void SetPaused(bool paused) {

    // Switch between the in-game and paused UI
    inGameUI.SetActive(!paused);
    pausedUI.SetActive(paused);

    // If we're paused..
    if (paused) {
      // Stop time
      Time.timeScale = 0.0f;
    } else {
      // Resume time
      Time.timeScale = 1.0f;
    }
  }

Setting Up the Scene

With the code written, we can now set up the Game Manager in the scene. Configuring the Game Manager is entirely a matter of connecting objects in the scene to variables in the script:

  • Ship Prefab should be the ship prefab you just made.

  • Ship start position should be the ship start position in the scene.

  • Station prefab should be the station prefab you just made.

  • Station start position should be the station start position in the scene.

  • Camera Follow should be the Main Camera in the scene.

  • In-Game UI, Main Menu UI, Paused UI, and Game Over UI should be their equivalent UIs in the scene.

  • Asteroid Spawner should be the Asteroid Spawner object in the scene.

  • Leave Warning UI for now; that’s for the next section.

When you’re done, the Inspector for the Game Manager should look like Figure 13-6.

mgdu 1306
Figure 13-6. The Inspector for the Game Manager

Now that the Game Manager is set up, we need to connect the various buttons that are in the Game UI to the Game Manager.

  1. Connect the Pause button. Select the Pause button in the In-Game UI, and click the + button at the bottom of the Clicked event. Drag the Game Manager into the slot that appears, and change the function to GameManager → SetPaused. A checkbox will appear; turn it on. This has the effect of calling the SetPaused method on the Game Manager, and passing in the boolean value true.

  2. Connect the Unpause button. Select the Unpause button in the Paused menu. Perform the same set of steps as for the Pause button, but with one change: turn the checkbox off. This will make the button call SetPaused with the boolean value false.

  3. Connect the New Game buttons. Select the New Game button in the Main Menu, and click the + button at the bottom of the Clicked event. Drag the Game Manager into the slot, and change the function to GameManager → StartGame.

    Next, repeat these steps for the New Game button in the Game Over screen.

The buttons will now be set up! Before we’re done, there are a few more minor things we need to set up to get the full gameplay experience.

First, we need to make it so that destroying the Space Station ends the game. The Space Station already has the DamageTaking script on it; we need to make this script call the GameOver function on the Game Manager.

  1. Add the call to GameOver in DamageTaking.cs. Open the file and add the following code:

  public class DamageTaking : MonoBehaviour {

    // The number of hit points this object has
    public int hitPoints = 10;

    // If we're destroyed, create one of these at
    // our current position
    public GameObject destructionPrefab;

    // Should we end the game if this object is destroyed?
    public bool gameOverOnDestroyed = false;

    // Called by other objects (like Asteroids and Shots)
    // to take damage
    public void TakeDamage(int amount) {

      // Report that we got hit
      Debug.Log(gameObject.name + " damaged!");

      // Deduct the amount from our hit points
      hitPoints -= amount;

      // Are we dead?
      if (hitPoints <= 0) {

        // Log it
        Debug.Log(gameObject.name + " destroyed!");

        // Remove ourselves from the game
        Destroy(gameObject);

        // Do we have a destruction prefab to use?
        if (destructionPrefab != null) {

          // Create it at our current position
          // and with our rotation.
          Instantiate(destructionPrefab,
                      transform.position, transform.rotation);
        }

>       // If we should end the game now, call the
>       // GameManager's GameOver method.
>       if (gameOverOnDestroyed == true) {
>         GameManager.instance.GameOver();
>       }
      }

    }

  }

This code makes the object check to see if the gameOverOnDestroyed variable is set to true; if it is, the Game Manager’s GameOver method is called, ending the game.

We also need to make the asteroids inflict damage when they collide. To do this, we’ll add a DamageOnCollide script to them.

To make the asteroids apply damage, select the Asteroid prefab, and add a DamageOnCollide component.

Next, the asteroids should display their distance to the space station. This will help the player decide which asteroid is the most important one to go over. We’ll do this by modifying the Asteroid script to query the Game Manager for the current space station, which is then given to the showDistanceTo variable of the asteroid’s indicator.

To make the asteroid show the distance label, open Asteroid.cs, and add the following code to the Start function:

  public class Asteroid : MonoBehaviour {

    // The speed at which the asteroid moves.
    public float speed = 10.0f;

    void Start () {
      // Set the velocity of the rigidbody
      GetComponent<Rigidbody>().velocity
        = transform.forward * speed;

      // Create a red indicator for this asteroid
      var indicator =
        IndicatorManager.instance.AddIndicator(
          gameObject, Color.red);

>         // Track the distance from this object to
>         // the current space station that's
>         // managed by the GameManager
>         indicator.showDistanceTo =
>           GameManager.instance.currentSpaceStation
>             .transform;
    }

  }

This code sets up the indicator to show the distance from the asteroid to the current space station, which helps the player to prioritize the asteroids that are closest to the station.

You’re done!

Play the game. You can now fly around and shoot asteroids, and asteroids will destroy the space station if too many hit it; you can also destroy the space station by shooting at it, and if the station is destroyed, game over!

Boundaries

There’s one last core piece of gameplay that we need to add: we want to warn the player if they’re getting too far away from the space station. If the player goes too far, we’ll show a red warning border around the edges of the screen; if turn around, it’s game over.

Creating the UI

First, we’ll set up the UI for the warning:

  1. Add the Warning sprite. Select the Warning texture. Change the texture’s type to Sprite/UI.

    What we need to do is slice the sprite, so that it can be stretched over the entire screen without distorting the shape of the corners.

  2. Slice the sprite. Click the Sprite Editor button, and the sprite will appear in a new window. In the panel at the lower-right of the window, set the border to 127 for all sides. This will make the corners not get stretched (see Figure 13-7).

    mgdu 1307
    Figure 13-7. Slicing the Warning sprite

    Click the Apply button.

  3. Next, we’ll create the Warning UI. This will simply be an image displayed on the UI, which will be set to stretch over the entire screen.

    To set up the warning UI, create a new empty game object, and name it “Warning UI”. Make it a child of the Canvas object.

    Set the anchors to stretch horizontally and vertically, and set the margins to zero. This will make it fill the entire canvas.

    Add an Image component to it. Make the Source Image of this Image component be the Warning sprite that you just created, and set the Image Type to sliced. The image will be stretched over the entire screen.

With that done, it’s time to code it up.

Coding the Boundary

The boundaries are invisible to the player, which means that they’ll be invisible while editing the game. If you want to visualize the volume in which the player can fly around, you’ll need to use the Gizmos feature again, just like you did for the Asteroid Spawner.

There are two concentric spheres that we care about, which we’ll call the warning sphere and the destroy sphere. Both of these spheres will be centered on the same point, but they’ll have different radii: the warning sphere’s radius will be less than that of the destroy sphere.

  • If the ship’s position is within the warning sphere, then all is good, and no warning will be visible.

  • If the ship is outside the warning sphere, then the warning will appear on screen, which will signal to the player that they need to turn around and head back.

  • If the ship is outside the destroy sphere, the game ends.

The actual checking of the ship’s position will be handled by the Game Manager, which will use the data stored inside the Boundary object (which you’re about to create) to determine whether the ship is outside either of the two spheres.

Let’s get started by creating the Boundary object, and adding the code that visualizes the two spheres:

  1. Create the Boundary object. Create a new empty object, with the name “Boundary”.

    Add a new C# script to the object called Boundary.cs, and add the following code to it:

  public class Boundary : MonoBehaviour {

    // Show the warning UI when the player is this far from the
    // center
    public float warningRadius = 400.0f;

    // End the game when the player is this far from the center
    public float destroyRadius = 450.0f;

    public void OnDrawGizmosSelected() {
      // Show a yellow sphere with the warning radius
      Gizmos.color = Color.yellow;
      Gizmos.DrawWireSphere(transform.position,
        warningRadius);

      // And show a red sphere with the destroy radius
      Gizmos.color = Color.red;
      Gizmos.DrawWireSphere(transform.position,
        destroyRadius);
    }
  }

When you return to the game editor, you’ll see two wireframe spheres. The yellow sphere shows the warning radius, and the red sphere shows the destroy radius (as seen in Figure 13-8).

mgdu 1308
Figure 13-8. The boundaries
Note

The Boundary script doesn’t actually do any logic of its own in-game. Instead, the GameManager uses its data to determine if the player has flown beyond the boundary radii.

Now that the boundary object has been created, we just need to set up the Game Manager to use it.

  1. Add the boundary fields to the GameManager script, and update GameManager to use them. Add the following code to GameManager.cs:

  public class GameManager : Singleton<GameManager> {

    // The prefab to use for the ship, the place it starts from,
    // and the current ship object
    public GameObject shipPrefab;
    public Transform shipStartPosition;
    public GameObject currentShip {get; private set;}

    // The prefab to use for the space station, the place it
    // starts from, and the current ship object
    public GameObject spaceStationPrefab;
    public Transform spaceStationStartPosition;
    public GameObject currentSpaceStation {get; private set;}

    // The follow script on the main camera
    public SmoothFollow cameraFollow;

>   // The game's boundary
>   public Boundary boundary;

    // The containers for the various bits of UI
    public GameObject inGameUI;
    public GameObject pausedUI;
    public GameObject gameOverUI;
    public GameObject mainMenuUI;

>   // The warning UI that appears when we approach
>   // the boundary
>   public GameObject warningUI;

    // Is the game currently playing?
    public bool gameIsPlaying {get; private set;}

    // The game's Asteroid Spawner
    public AsteroidSpawner asteroidSpawner;

    // Keeps track of whether the game is paused or not.
    public bool paused;

    // Show the main menu when the game starts
    void Start() {
      ShowMainMenu();
    }

    // Shows a UI container, and hides all others.
    void ShowUI(GameObject newUI) {

      // Create a list of all UI containers.
      GameObject[] allUI
        = {inGameUI, pausedUI, gameOverUI, mainMenuUI};

      // Hide them all.
      foreach (GameObject UIToHide in allUI) {
        UIToHide.SetActive(false);
      }

      // And then show the provided UI container.
      newUI.SetActive(true);
    }

    public void ShowMainMenu() {
      ShowUI(mainMenuUI);

      // We aren't playing yet when the game starts
      gameIsPlaying = false;

      // Don't spawn asteroids either
      asteroidSpawner.spawnAsteroids = false;
    }

    // Called by the New Game button being tapped
    public void StartGame() {
      // Show the In-Game UI
      ShowUI(inGameUI);

      // We're now playing
      gameIsPlaying = true;

      // If we happen to have a ship, destroy it
      if (currentShip != null) {
        Destroy(currentShip);
      }

      // Likewise for the station
      if (currentSpaceStation != null) {
        Destroy(currentSpaceStation);
      }

      // Create a new ship, and place it
      // at the start position
      currentShip = Instantiate(shipPrefab);
      currentShip.transform.position
        = shipStartPosition.position;
      currentShip.transform.rotation
        = shipStartPosition.rotation;

      // And likewise for the station
      currentSpaceStation = Instantiate(spaceStationPrefab);

      currentSpaceStation.transform.position =
        spaceStationStartPosition.position;

      currentSpaceStation.transform.rotation =
        spaceStationStartPosition.rotation;

      // Make the follow script track the new ship
      cameraFollow.target = currentShip.transform;

      // Start spawning asteroids
      asteroidSpawner.spawnAsteroids = true;

      // And aim the spawner at the new space station
      asteroidSpawner.target = currentSpaceStation.transform;

    }

    // Called by objects that end the
    // game when they're destroyed
    public void GameOver() {
      // Show the Game Over UI
      ShowUI(gameOverUI);

      // We're no longer playing
      gameIsPlaying = false;

      // Destroy the ship and the station
      if (currentShip != null)
        Destroy (currentShip);

      if (currentSpaceStation != null)
        Destroy (currentSpaceStation);

>     // Stop showing the warning UI, if it was visible
>     warningUI.SetActive(false);

      // Stop spawning asteroids
      asteroidSpawner.spawnAsteroids = false;

      // And remove all lingering asteroids from the game
      asteroidSpawner.DestroyAllAsteroids();
    }

    // Called when the Pause or Resume buttons are tapped
    public void SetPaused(bool paused) {

      // Switch between the in-game and paused UI
      inGameUI.SetActive(!paused);
      pausedUI.SetActive(paused);

      // If we're paused..
      if (paused) {
        // Stop time
        Time.timeScale = 0.0f;
      } else {
        // Resume time
        Time.timeScale = 1.0f;
      }
    }

>   public void Update() {
>
>     // If we don't have a ship, bail out
>     if (currentShip == null)
>       return;
>
>     // If the ship is outside the Boundary's Destroy Radius,
>     // game over. If it's within the Destroy Radius, but
>     // outside the Warning radius, show the Warning UI. If
>     // it's within both, don't show the Warning UI.
>
>     float distance =
>       (currentShip.transform.position
>         - boundary.transform.position)
>           .magnitude;
>
>     if (distance > boundary.destroyRadius) {
>       // The ship has gone beyond the destroy radius,
>       // so it's game over
>       GameOver();
>     } else if (distance > boundary.warningRadius) {
>       // The ship has gone beyond the warning radius,
>       // so show the warning UI
>       warningUI.SetActive(true);
>     } else {
>       // It's within the warning threshold, so don't
>       // show the warning UI
>       warningUI.SetActive(false);
>     }
>
>   }

  }

This new code makes use of the Boundary class that you just created to check to see if the player has gone beyond either the warning radius or the destroy radius. Every frame, the distance from the player to the center of the boundary spheres is checked; if they’re beyond the warning radius, the warning UI is made to appear, and if they’re beyond the destroy radius, the game ends. If the player is within the warning radius, they’re fine, so the warning radius is disabled. This means that if the player flies outside the warning radius and then returns to safety, they’ll see the warning UI appear and then disappear.

Next, you just need to connect up the slots. The Game Manager needs a reference to the Boundary object you created a moment ago, as well as a reference to the Warning UI.

  1. Configure the Game Manager to use the boundary objects. Drag Warning UI into the Warning UI slot, and the Boundary object into the Boundary slot.

  2. Play the game. When you get close to the boundary, the warning will appear, and if you don’t turn around, game over!

Final Polish

Congratulations! You’ve now finished setting up the core gameplay of a rather sophisticated space shooter. As you followed along in the preceding sections, you set up a space environment, created spaceships, space stations, asteroids, and laser beams; set up their physics; and set up all of the various logical components that connect them together. On top of that, you’ve created the UI that’s necessary for actually playing the game outside of the Unity editor.

The core of the game is done, but there’s still room for some visual improvements. Because the visuals of the game are quite sparse, we don’t have many visual reference points to give the player a sense of traveling at speed. Additionally, we’ll add a little more color to the game by adding trail renderers to the ship and asteroids.

Space Dust

If you’ve ever played a spaceflight game before, like Freelancer or Independence War, you might have noticed how, when the player flies around, small pieces of dust, debris, and other small objects move past the player.

To improve our game, we’ll add small dust motes that provide a sense of depth and perspective as the player moves past them. We’ll achieve this with a particle system that moves with the player, continuously creating dust particles in a sphere surrounding the player. Importantly, these dust particles will not move relative to the player. This means that, as the player flies, dust particles will appear that the player then flies past. This creates a much better impression of speed in the game.

To create the dust particles, follow these steps:

  1. Drag the Ship prefab into the scene. We’ll be making some changes to the prefab.

  2. Create the Dust child object. Create a new empty game object, and name it “Dust”. Make it a child of the Ship game object you just dragged out.

  3. Add a Particle System component to it. Copy the settings in Figure 13-9 to it.

The settings for the dust particles
Figure 13-9. The settings for the dust particles

The critical parts of this particle system are the fact that the Simulation Space setting is World, and the Shape is a Sphere. By setting the Simulation Space to World, the particles will not move with their parent object (the Ship). This means that the Ship will fly right past them.

  1. Apply your changes to the prefab. Select the Ship object, and click the Apply button at the top of the Inspector. This will save your changes to the prefab. We’re not quite done with the ship, so don’t delete it just yet.

You can see the particle system in action in Figure 13-10. Note how it creates a feeling of a field of stars against the relatively smooth colors of the skybox.

mgdu 1310
Figure 13-10. The dust particle system

Trail Renderers

The ship is a very simple model, but there’s no reason why you can’t dress it up a little with some special effects. We’ll add two line renderers to the ship that create the effect of engines behind them.

  1. Create a new Material for the trail. Do this by opening the Assets menu, and choosing Create → Material. Name the new material “Trail”, and place it in the Objects folder.

  2. Make the Trail material use an Additive shader. Select the Trail material, and change its Shader to Mobile → Particles → Additive. This is a simple shader that simply adds its color to the background. Leave the Particle Texture empty—it won’t be needed.

  3. Add a new child object to the Ship. Name it “Trail 1”. Position it at (-0.38,0,-0.77).

  4. Add a Trail Renderer component. Make it use the settings in Figure 13-11. Note that the Material it’s using is the new Trail material you just created.

mgdu 1311
Figure 13-11. The settings for the ship’s trail renderer
Note

The colors used in the trail renderer’s gradient are:

  • #000B78FF

  • #061EF9FF

  • #0080FCFF

  • #000000FF

  • #00000000

You’ll notice that the colors darken toward the end. Because the Trail material uses an Additive shader, this has the result of making the trail fade out.

  1. Duplicate the object. Once you’ve set up the first trail, duplicate it by opening the Edit menu and choosing Duplicate. Move this new duplicate object to (0.38,0,-0.77).

Tip

The location for this second trail is the same as the first, but with the X component flipped.

  1. Apply the changes you’ve made to the prefab. Select the Ship object, and click the Apply button at the top of the Inspector. Finally, delete the Ship from the scene.

You’re now ready to test it out! When you fly the ship, two blue lines will appear behind it, as seen in Figure 13-12.

mgdu 1312
Figure 13-12. The engine trails behind the ship

We’ll now apply a similar effect to the asteroids. The asteroids in the game are quite dark, and while they have indicators to help the player keep track of where they are, they could do with a little more color. To improve things, we’ll add a trail renderer to them.

  1. Add an Asteroid to the scene. Drag out the Asteroid prefab into the scene, so that you can make changes.

  2. Add a Trail Renderer component to the Graphics child object. Use the settings you see in Figure 13-13.

mgdu 1313
Figure 13-13. The settings for the asteroid’s trail
  1. Apply the changes to the Asteroid prefab, and remove it from the scene.

The asteroids will now have a bright trail behind them. You can see the full game in action in Figure 13-14.

mgdu 0901
Figure 13-14. The game in action

Audio

There’s one last part that we need to add: audio! Even though there’s no sound in real space, video games are seriously improved with the addition of sound. There are three sounds that we need to add: the roar of the ship’s engines, the zap of laser blasts, and the boom of asteroids exploding. We’ll add each one, one at a time.

Note

We’ve included some public-domain sound effects in the book’s files, which you’ll find in the Audio folder.

Spaceship

First, we’ll add a looping sound effect to the spaceship.

  1. Add the Ship to the scene. We’re about to make some changes to it.

  2. Add an Audio Source component to the Ship. Audio Sources are how you make sound happen.

  3. Turn on the Loop setting. We want the engine noises to play continuously as the player flies the ship.

  4. Add the rocket sound. Drag the Engine audio clip into the AudioClip slot.

  5. Save the changes to the prefab. That’s it!

Adding looping sounds is incredibly easy, and gives you a huge amount of overall improvement to the game for very little effort on your part.

Tip

Don’t delete the Ship from the scene yet—we’ll add some more to it in a moment.

Weapon effects

Adding sound effects to the weapons is a little more complex. We want to play a sound effect every time the weapon fires, which means we’ll need to make the code aware of sound effects.

First, we’ll need to add audio sources to the two weapon points:

  1. Add Audio Sources to the weapon fire points. Select both of the weapon fire points. With both of them selected, add an Audio Source.

  2. Add the Laser effect to the audio sources. Once you’ve done that, turn off the Play On Awake setting—we only want to play sound when we fire a shot.

  3. Add code to play the sound effect when shots are fired. Add the following code to ShipWeapons.cs:

  public class ShipWeapons : MonoBehaviour {

    // The prefab to use for each shot
    public GameObject shotPrefab;

    public void Awake() {
      // When this object starts up, tell the input manager
      // to use me as the current weapon object
      InputManager.instance.SetWeapons(this);
    }

    // Called when the object is removed
    public void OnDestroy() {
      // Don't do this if we're not playing
      if (Application.isPlaying == true) {
        InputManager.instance.RemoveWeapons(this);
      }
    }

    // The list of places where a shot can emerge from
    public Transform[] firePoints;

    // The index into firePoints that the next
    // shot will fire from
    private int firePointIndex;

    // Called by InputManager.
    public void Fire() {

      // If we have no points to fire from, return
      if (firePoints.Length == 0)
        return;

      // Work out which point to fire from
      var firePointToUse = firePoints[firePointIndex];

      // Create the new shot, at the fire point's position
      // and with its rotation
      Instantiate(shotPrefab,
        firePointToUse.position,
        firePointToUse.rotation);

>     // If the fire point has an audio source
>     // component, play its sound effect
>     var audio
>       = firePointToUse.GetComponent<AudioSource>();
>     if (audio) {
>       audio.Play();
>     }

      // Move to the next fire point
      firePointIndex++;

      // If we've moved past the last fire point in the list,
      // move back to the start of the queue
      if (firePointIndex >= firePoints.Length)
        firePointIndex = 0;

    }

  }

This code checks to see if the fire point that the shot is being fired from has an AudioSource component. If it does have one, it’s made to play the shot sound.

  1. Save your changes to the Ship prefab, and remove it from the scene.

You’re done. Now, you’ll hear a sound effect every time a shot is fired!

Explosions

There’s one last sound effect to add: an explosion sound effect, for when explosions appear. This one’s easy: we just need to add an audio source to the explosion object, and set it to play on awake. When an explosion appears, it will automatically play the Explosion sound.

  1. Add an Explosion to the scene.

  2. Add an Audio Source component to the explosion. Drag in the Explosion audio clip, and turn on Play On Awake.

  3. Save your changes to the prefab and remove it from the scene.

You now get explosions whenever one appears!

Wrapping Up

You’re now all done. Congratulations! Rockfall is now complete, and in your hands. It’s up to you to decide what to do next with it!

Some ideas:

Add new weapons

Maybe a rocket that turns to face its target?

Add enemies that attack the player

The asteroids are pretty simple, and fly straight at the space station, while nothing actually goes after the player.

Add damage effects to the space station

When an asteroid hits, add a particle system that emits smoke and flames at the point of impact. It’s not realistic, but that hasn’t stopped us adding any of the other features to the game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset