Wednesday, November 12, 2014

Unity Endless Runner Tutorial #5 - Pause

Original Tutorial
http://catlikecoding.com/unity/tutorials/runner/

My version of the game (Version Log)
http://www.hansadrian.com/Stuff/Endless%20Runner%20Tutorial/build.html

GitHub Gist (for full code files)
https://gist.github.com/HansNewbie/10005219/a6046d42e2b35dc67e411ff8ee22647992871e8f

Completed Project Files (V0.6.1)
Buy this on Selz Selz powering ecommerce websites

For version 0.5, I added pause system.

If you google "unity pause", the answer for implementation on simple game - i.e. a pause would not bring up separate pause menu, the game is just paused and nothing else is affected, is to stop time. You can do that by putting Time.timescale as 0. Of course, when implementing it, I needed to change some things for pause to work perfectly, but for now, let's get down to business. PS: I will not put all the codes in the post; some things should be simple or intuitive enough for you to put. Refer to GitHub Gist for full code, or you can check the GitHub Gist revision comparison for easier change view.

The first question in my mind would be where to put this change in timescale. For the sake of code elegance (and also separation of concern, if you take software engineering before), I would like it to be handled by one separate component. However, we already have one component that only handles exit. Why don't we extend this component? We will make GameExitManager as the manager of game states, i.e. controlling pause, exit and resume. Since it no longers manage exit only, it is better to rename it appropriately into GameStateManager.cs (if you notice, in the GitHub Gist V0.5, there is no more GameExitManager.cs). We will rewrite this component completely. One reason is that to make the game intuitive, we would like the system to pause on pressing ESC and quit if player press ESC on pause or on menu. As such, it no longer handles "Press ESC to quit".
Pressing Back button on Android is equivalent to pressing ESC.

How would GameStateManager.cs know what state the game currently is in? We need to store it. As such, we will create an enumeration for the states, namely PLAY and PAUSE. Only GameStateManager.cs keep track of current game state. As such, we keep it as private.
public class GameStateManager : MonoBehaviour {
    enum State { PLAY, PAUSE }

    private State gameState;

    void Start() {
        gameState = State.PLAY;
    }
}
Next is handling of input. If the game state is PLAY, pressing ESC would pause the game. If the game is in pause, triggering jump (which is mapped to Space for every platform and a tap for Android) will resume the game while pressing ESC will exit the game application. The code will be something like:
public class GameStateManager : MonoBehaviour {
    enum State { MENU, PLAY, PAUSE }

    private State gameState;

    void Start() {
        gameState = State.PLAY;
    }

    void Update() {
        if (gameState == State.PLAY && Input.GetKeyDown("escape")) {
            Time.timeScale = 0.0f;
            gameState = State.PAUSE;
        }
        else if (gameState == State.PAUSE &&
                 (Input.GetButtonDown("Jump") ||
                    ((Application.platform == RuntimePlatform.Android) && (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)))) {
            Time.timeScale = 1.0f;
            gameState = State.PLAY;
        }
        else if (gameState == State.PAUSE && Input.GetKeyDown("escape")) {
            Application.Quit();
        }
    }
}
At this point, I thought, "Cool. It's done :) Just need to polish things here and there and add some instructions." How mistaken I was. I discovered one troublesome truth; the Runner update still runs on timeScale 0, because technically you only stop time, not input. As such, if you press jump, the game will resume AND the Runner will jump straight away (or use boost if it is in mid-air and has booster). Another problem is since we only record PLAY and PAUSE, the game can pause on main menu, which is pretty stupid. Of course, this is minor problem. To fix the first problem, we need to disable the updating of Runner on pause. We also would like to make it applicable to wider range of object, say change gameState in our GameStateManager. What could be more convenient than delegate! Yes, we will change GameEventManager to have pause..... and resume (did you forget that?). So here is the new GameEventManager:
public static class GameEventManager {
 
 public delegate void GameEvent();
 
 public static event GameEvent GameStart, GameOver,
  GamePause, GameResume;
 
 public static void TriggerGameStart() {
  if (GameStart != null) {
   GameStart();
  }
 }
 
 public static void TriggerGameOver() {
  if (GameOver != null) {
   GameOver();
  }
 }
 
 public static void TriggerGamePause() {
  if (GamePause != null) {
   GamePause();
  }
 }
 
 public static void TriggerGameResume() {
  if (GameResume != null) {
   GameResume();
  }
 }
}
Now that that's done, we will need to update our Runner accordingly. Add Runner's GamePause and GameResume to their respective lists in GameEventManager. Then, simply enable and disable Runner on resume and pause respectively like this:
 private void GamePause() {
  enabled = false;
 }
 
 private void GameResume() {
  enabled = true;
 }
For GameStateManager, we need to change some things. One is that we don't want GameStateManager to be enabled at the start of the game because that means player can pause on menu, i.e. before game starts. We will then enable it on game start and respectively disable it on game over because on game over the game is sort of in MENU state. Also, since now we have delegate for game pause and resume, it would be nicer to change gameState using delegate instead of on button trigger because in the future we might want to do more than just changing gameState. So, here is the final code for GameEventManager.
using UnityEngine;
 
public class GameStateManager : MonoBehaviour {
 enum State { PLAY, PAUSE }
 
 private State gameState;
 
 void Start() {
  GameEventManager.GameStart += GameStart;
  GameEventManager.GameOver += GameOver;
  GameEventManager.GamePause += GamePause;
  GameEventManager.GameResume += GameResume;
  gameState = State.PLAY;
  enabled = false;
 }
 
 void Update() {
  if (gameState == State.PLAY && Input.GetKeyDown("escape")) {
   Time.timeScale = 0.0f;
   GameEventManager.TriggerGamePause();
  }
  else if (gameState == State.PAUSE &&
           (Input.GetButtonDown("Jump") ||
      ((Application.platform == RuntimePlatform.Android) && (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)))) {
   Time.timeScale = 1.0f;
   GameEventManager.TriggerGameResume();
  }
  else if (gameState == State.PAUSE && Input.GetKeyDown("escape")) {
   Application.Quit();
  }
 }
 
 private void GameStart() {
  enabled = true;
 }
 
 private void GameOver() {
  enabled = false;
 }
 
 private void GamePause() {
  gameState = State.PAUSE;
 }
 
 private void GameResume() {
  gameState = State.PLAY;
 }
}
I am not going to write about GUIManager polishing since at this point in time, you should be familiar with what to do. Take this as a form of exercise :) That being said, we still have one more thing to take care of which I will give to GUIManager. Since we disable GameStateManager before the game starts and on game over, player cannot quit the game when they are on MENU state, i.e. player cannot quit when the game is in start page or when the high score is being shown. Since GUIManager is enabled during those times, it would be the simplest component to handle quitting during those times as well. as such, add pressing ESC to Update() in GUIManager. Update() now becomes like below.
 void Update() {
  if ((Input.GetButtonDown("Jump")) ||
      ((Application.platform == RuntimePlatform.Android) && (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began))) {
   GameEventManager.TriggerGameStart();
  }
  else if (Input.GetKeyDown("escape"))
  {
   Application.Quit();
  }
 }
That's it for pausing game! I am not sure if I will continue the series since at this point the game play is settled. Further extension would be on making the game nice, such as visual, animation and sound. I will see what I will do. If you have any question about what I wrote or about what I did not write but is in the code, feel free to post a comment on this post!

No comments:

Post a Comment