Breakout Clone 7: Start, Victory, and Failure Screens

2023-03-21

I don't want the game to start immediately after the player launches it. Instead, I want to show a menu and let the player choose to start the game or close the application. We also want a screen to display when the player looses, (when the ball collides with the bottom of the screen below the paddle,) and when they "win", (when there are no more levels left to play.)

I don't want to over-design these screens. The whole point of making this game is to test myself and see if I have the basics down. What I'm picturing is very simple: two text elements for each screen, and a cursor that the player can control with the up & down arrows on the keyboard. The two text elements will be something like "Start Game" and "Close Game". When the player hits the enter key, we call the appropriate function. On the "victory" and "failure" screens, we'll show a third text element like "You won" or "You lost".

The complicated bit of this is going to be the cursor. The "Start" and "Close" options should be simple text objects on the page, but I'll need some kind of script to handle button presses and control the cursors current position.

Since this is all cursor-related logic, I created an object named Cursor and added a script to it. The selectable options will be an array of references to game objects that I can assign through Unity's UI. Instead of having a literal cursor like an arrow, I'll highlight each option as they're selected.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro; // for TMP_Text

public class Cursor : MonoBehaviour
{
  [SerializeField] TMP_Text[] items;

  private int cursorPosition = 0;
  private Color32 defaultColor = new Color32(0, 0, 0, 255);
  private Color32 highlightColor = new Color32(140, 199, 64, 255);

  void Start()
  {
    items[cursorPosition].color = highlightColor;
  }

  void Update()
  {
    bool upOrDownPressed = Input.GetButtonDown("Vertical");

    if (upOrDownPressed) {
      HandleUpOrDownPressed();
    }

    bool submitButtonPressed = Input.GetButtonDown("Submit");
    if (submitButtonPressed) {
      SubmitCurrentItem(items[cursorPosition]);
    }

  }

  private void HandleUpOrDownPressed() {
    int vertical = 0;
    vertical = (int) Input.GetAxisRaw("Vertical");

    items[cursorPosition].color = defaultColor;

    if (vertical > 0) {
      MoveCursorUp();
    } else {
      MoveCursorDown();
    }

    items[cursorPosition].color = highlightColor;
  }

  private void MoveCursorUp() {
    if(cursorPosition == 0) {
      cursorPosition = items.Length - 1;
    } else {
      cursorPosition -= 1;
    }
  }

  private void MoveCursorDown() {
    if(cursorPosition == items.Length - 1) {
      cursorPosition = 0;
    } else {
      cursorPosition += 1;
    }
  }

  private void SubmitCurrentItem(TMP_Text menuItem) {
    GameSession gameSession = FindObjectOfType<GameSession>();

    FindObjectOfType<LeaderboardManager>().UpdateBoard(gameSession);

    if (menuItem.tag == "StartMenuItem") {
      gameSession.StartGame();
    } else if (menuItem.tag == "ExitMenuItem") {
      gameSession.EndGame();
    }
  }
}

I'm calling the StartGame() and EndGame() methods on GameSession, so I should define them:

  // in GameSession.cs

  public void StartGame() {
    SceneManager.LoadScene(1);
    // this is where we'll start playing the music!
    SoundManager.instance.PlayMusic();
  }

  public void EndGame() {
    Application.Quit();
  }

Under File > Build Settings in Unity, I'll add the new scene to the list of Scenes In Build, and make sure it's the first one at index 0. I'll also make sure that my "Level 1" scene is at index 1.

Let's give it a try!

Note: between writing this post and making changes to the game, I also updated the sprites used for the paddle, ball, and blocks. These are visual changes only and I didn't think they were worth their own post.

Looks good enough to me! All that's left is to add two more screens: one for when we run out of levels (the "Victory" screen) and one for when the player fails to prevent the ball from hitting the bottom of the screen (the "Failure" screen.)

I've put the victory screen after the last level so the game will transition to it by itself. The failure screen will be after the victory screen, but to see it, we'll need to transition to it from our GameSession object:

  // in GameSession.cs
  public void LostGame() {
    SceneManager.LoadScene("Failure Level");
  }

We can then call it from our Ball object via the BottomHit method:

  // in Ball.cs
  private void BottomHit() {
    FindObjectOfType<GameSession>().LostGame();
  }

Now we have all the basics in place. Hey, it actually kinda looks like a game!

My next few posts will wrap up this series as I've already moved on to other projects. We'll need a few more levels, and I want to track some kind of score for the player as they move through them. Finally, I'll add in some items that drop randomly when the ball collides with a block.