Restarting the game

Revisado con versión: 5.3

-

Dificultad: Principiante

When there is a win or a draw, the game is over. All of the buttons are disabled. Nothing more can be done with the game. The only option is to quit and restart the game if we want to play again. This is not very efficient.

Let’s create a way to restart the game by clicking a button that only appears when the game is over.

This button will have to be fairly robust. It will need to appear only when the game is over. When clicked, the button needs to hide itself again. More importantly, the button will also need to hide the Game Over Panel, clear and re-enable all of the grid spaces on the board, reset the move counter and start the game over again.

We will need two things to do this.

We will need a UI Button element in the scene and a public function that the UI Button can call when clicked. For all practical purposes, it doesn’t make a difference in this case whether we create the button first or the function. If we make the function first, however, we can then hook the function up to the button as we are creating it.

So, let’s start by writing the function.

  • Open the GameController script for editing.
  • Create a new public function that returns void and is called "RestartGame".
public void RestartGame()
{

}
  • In RestartGame,
    • ... set playerSide to "X".
    • ... set moveCount to 0.
    • ... set the Game Over Panel to inactive.
playerSide = "X";
moveCount = 0;
gameOverPanel.SetActive(false);

This code will reset the player side and move count, and deactivate the Game Over Panel. We now need to iterate through all of the grid spaces and remove the "X"s and the "O"s and reactivate the buttons. To help us, we already have something that does this already. This is the for loop in GameOver that deactivated all of the buttons in the grid.

  • Copy the for loop that iterates through all of the grid spaces from GameOver.
  • Paste this code into RestartGame.
  • Inside this new for loop,
    • ... change the boolean value for interactable from false to true.

This will reactivate all of the buttons.

  • Inside the new for loop,
    • ... set the buttonList.text property to an empty string value.

This will remove all of the "X"s and "O"s from the grid spaces.

for (int i = 0; i < buttonList.Length; i++)
{
    buttonList[i].GetComponentInParent().interactable = true;
    buttonList [i].text = "";
}

Is it happening again? Are we starting to have similar, if not identical, code proliferate throughout our codebase? Bad coder. No donut!

All of the code that does the same job should be gathered into one place.

  • Create a new function that returns void called "SetBoardInteractable" that has a bool parameter "toggle".
void SetBoardInteractable (bool toggle)
{

}
  • Copy the code setting the board to non-interactable from GameOver to SetBoardInteractable.
  • Change the value of false to toggle.
for (int i = 0; i < buttonList.Length; i++)
{
    buttonList[i].GetComponentInParent().interactable = toggle;
}
  • In GameOver,
    • ... replace the code we copied with a call to SetBoardInteractable(false);
  • In RestartGame,
    • ... remove the line of code setting the board to interactable from the for loop, but leave the line resetting the text.
    • ... in a new line outside of the for loop, add a call to SetBoardInteractable(true);
SetBoardInteractable(true);
for (int i = 0; i < buttonList.Length; i++)
{
    buttonList [i].text = "";
}

This is much neater and clearer now.

The final script should look like this:

GameController

Code snippet

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    public Text[] buttonList;
    public GameObject gameOverPanel;
    public Text gameOverText;

    private string playerSide;
    private int moveCount;

    void Awake ()
    {
        SetGameControllerReferenceOnButtons();
        playerSide = "X";
        gameOverPanel.SetActive(false);
        moveCount = 0;
    }

    void SetGameControllerReferenceOnButtons ()
    {
        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList[i].GetComponentInParent<GridSpace>().SetGameControllerReference(this);
        }
    }

    public string GetPlayerSide ()
    {
        return playerSide;
    }

    public void EndTurn ()
    {
        moveCount++;
        if (buttonList [0].text == playerSide && buttonList [1].text == playerSide && buttonList [2].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [3].text == playerSide && buttonList [4].text == playerSide && buttonList [5].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [6].text == playerSide && buttonList [7].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [0].text == playerSide && buttonList [3].text == playerSide && buttonList [6].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [1].text == playerSide && buttonList [4].text == playerSide && buttonList [7].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [2].text == playerSide && buttonList [5].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [0].text == playerSide && buttonList [4].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [2].text == playerSide && buttonList [4].text == playerSide && buttonList [6].text == playerSide)
        {
            GameOver();
        }

        if (moveCount >= 9)
        {
            SetGameOverText ("It's a draw!");
        }

        ChangeSides();

    }

    void ChangeSides ()
    {
        playerSide = (playerSide == "X") ? "O" : "X";
    }

    void GameOver ()
    {
        SetBoardInteractable(false);
        SetGameOverText (playerSide + " Wins!");
    }

    void SetGameOverText (string value)
    {
        gameOverPanel.SetActive(true);
        gameOverText.text = value;
    }

    public void RestartGame ()
    {
        playerSide = "X";
        moveCount = 0;
        gameOverPanel.SetActive(false);
        SetBoardInteractable(true)

        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList [i].text = "";
        }
    }

    void SetBoardInteractable (bool toggle)
    {
        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList[i].GetComponentInParent<Button>().interactable = toggle;
        }
    }
}
  • Save the script.
  • Return to Unity.

Before we can test, we need to set up the UI Button element.

  • Create a UI Button element in the scene by using Create > UI > Button
  • With the Button GameObject selected,
    • ... rename the GameObject "Restart Button".
    • ... reset the RectTransform using the context sensitive gear menu.
    • ... set the Position Y to 330.
    • ... set the Width to 200.
    • ... set the Height to 60.
    • ... set the Normal Color to blue (0, 204, 204, 255) using the color Preset.
    • ... set the Highlighted Color to light blue (128, 255, 255, 255) using the color Preset.
    • ... set the Pressed Color to dark blue-green (51, 102, 102, 255) using the color Preset.
    • ... set the Disabled Color to very dark blue (33, 44, 55, 255) using the color Preset.
    • ... add a new row to the Button’s OnClick list.
    • ... drag the Game Controller GameObject from the Hierarchy Window onto the Object field in the new row in the Button’s OnClick list.
    • ... set the function in the new row in the Button’s OnClick list to GameController > RestartGame.

description

Now we need to set the look of the button by changing the associated text.

  • Select the child Text GameObject.
  • With the Text GameObject selected,
    • ... set the Text component’s Text property to "Play Again?"
    • ... set the Font Size to 18.

This should complete the setup for the Restart Button.

  • Save the scene.
  • Enter Play Mode.
  • Test by clicking any of the spaces.
  • Click on the Restart Button.
  • Exit Play Mode when satisfied.

Now we can restart the game.

Sadly, however, with the button exposed as it is throughout the entire game, we can restart the game at any time.

This is not necessarily the desired behavior.

If you are losing, it’s not very polite to suddenly restart the game with a "HAH!", leaving your opponent seething with annoyance.

This button should only be available when the game is over. If the button is hidden while the game is playable and only revealed when the game is over, we won’t need to create any special logic in the restart code to check if the game is running to prevent any accidental or willful restarts of the game. This is accomplished simply by hiding and revealing the button.

The button should be hidden by default when the game starts. It then should be activated when the game is over and then deactivated again when the game restarts.

  • Open the GameController script for editing.
  • Declare a new public variable to hold the reference to our Restart Button
public GameObject restartButton
  • In Awake,
    • ... deactivate the Restart Button.
restartButton.SetActive(false);
  • In GameOver,
    • ... activate the Restart Button.
restartButton.SetActive(true);
  • In RestartGame,
    • ... deactivate the Restart Button.
restartButton.SetActive(false);

The final script should look like this:

GameController

Code snippet

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    public Text[] buttonList;
    public GameObject gameOverPanel;
    public Text gameOverText;
    public GameObject restartButton;

    private string playerSide;
    private int moveCount;

    void Awake ()
    {
        SetGameControllerReferenceOnButtons();
        playerSide = "X";
        gameOverPanel.SetActive(false);
        moveCount = 0;
        restartButton.SetActive(false);
    }

    void SetGameControllerReferenceOnButtons ()
    {
        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList[i].GetComponentInParent<GridSpace>().SetGameControllerReference(this);
        }
    }

    public string GetPlayerSide ()
    {
        return playerSide;
    }

    public void EndTurn ()
    {
        moveCount++;
        if (buttonList [0].text == playerSide && buttonList [1].text == playerSide && buttonList [2].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [3].text == playerSide && buttonList [4].text == playerSide && buttonList [5].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [6].text == playerSide && buttonList [7].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [0].text == playerSide && buttonList [3].text == playerSide && buttonList [6].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [1].text == playerSide && buttonList [4].text == playerSide && buttonList [7].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [2].text == playerSide && buttonList [5].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [0].text == playerSide && buttonList [4].text == playerSide && buttonList [8].text == playerSide)
        {
            GameOver();
        }

        if (buttonList [2].text == playerSide && buttonList [4].text == playerSide && buttonList [6].text == playerSide)
        {
            GameOver();
        }

        if (moveCount >= 9)
        {
            SetGameOverText ("It's a draw!");
        }

        ChangeSides();

    }

    void ChangeSides ()
    {
        playerSide = (playerSide == "X") ? "O" : "X";
    }

    void GameOver ()
    {
        SetBoardInteractable(false);
        SetGameOverText (playerSide + " Wins!");
        restartButton.SetActive(true);
    }

    void SetGameOverText (string value)
    {
        gameOverPanel.SetActive(true);
        gameOverText.text = value;
    }

    public void RestartGame ()
    {
        playerSide = "X";
        moveCount = 0;
        gameOverPanel.SetActive(false);
        restartButton.SetActive(false);
        SetBoardInteractable(true);

        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList [i].text = "";
        }
    }

    void SetBoardInteractable (bool toggle)
    {
        for (int i = 0; i < buttonList.Length; i++)
        {
            buttonList[i].GetComponentInParent<Button>().interactable = toggle;
        }
    }
}
  • Save the script.
  • Return to Unity.

Associate the Restart Button GameObject with the Restart Button property of the GameController.

  • Select the Game Controller in the Hierarchy Window.
  • With the Game Controller selected,
    • ... assign the Restart Button property.

description

  • Save the Scene.
  • Enter Play Mode.
  • Test by clicking any of the spaces.

Now, when we are playing the scene, the Restart Button is not visible. When the game ends with a win, the Restart Button is shown. When the Restart Button is used to restart the game, the button is hidden.

If we end in a draw or a tie, however, our configuration fails, as we are not actually ending the game with a draw.

The solution to this is to end the game if we have a draw as well as a win. This makes sense, as well. When there is a draw, the game ends. This will require an update to how we work our GameOver function by including all of our Game Over logic in one place.

As we saw before with the SetGameOverText, we will see again how useful it is to keep all of our logic doing one thing in one place. Now, at first glance, this may seem over-complicated and it may seem that we are adding more code to our script to do this, but in many cases to make a script more clear and more generic, there will be more code.

  • Open the GameController script for editing.
  • Change the signature of the GameOver function to include a string parameter called "winningPlayer".
void GameOver(string winningPlayer);
  • Add an if/else check that if the winningPlayer is "draw", to set the Game Over Text to "It’s a draw!" or else set the Game Over Text to the winning player.
if (winningPlayer == "draw")
{
    SetGameOverText("It's a Draw!");
} 
else
{
    SetGameOverText(winningPlayer + " Wins!");
}

Only add to or replace this code related to SetGameOverText. Leave the rest of the code in the block. It is worth noting that the exiting line calling SetGameOverText(playerSide + " Wins!"); can be retained as long as it’s part of the if/else logic by placing it inside the else part of the code block.

  • Adjust the code in EndTurn. In each of the eight cases where there is a win
    • ... change the call to GameOver to include the playerSide as an argument.
GameOver(playerSide);
  • Adjust the code in EndTurn. After If (moveCount >= 9),
    • ... remove the code SetGameOverText("It's a draw!");
    • ... replace this with a call to call GameOver, including the string value of "draw" as an argument.
if (moveCount >= 9)
{
    GameOver("draw");
} 

The final script should look like this:

GameController

Code snippet

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

   public Text[] buttonList;
   public GameObject gameOverPanel;
   public Text gameOverText;
   public GameObject restartButton;

   private string playerSide;
   private int moveCount;

   void Awake ()
   {
       SetGameControllerReferenceOnButtons();
       playerSide = "X";
       gameOverPanel.SetActive(false);
       moveCount = 0;
       restartButton.SetActive(false);
   }

   void SetGameControllerReferenceOnButtons ()
   {
       for (int i = 0; i < buttonList.Length; i++)
       {
           buttonList[i].GetComponentInParent<GridSpace>().SetGameControllerReference(this);
       }
   }

   public string GetPlayerSide ()
   {
       return playerSide;
   }

   public void EndTurn ()
   {
       moveCount++;

       if (buttonList [0].text == playerSide && buttonList [1].text == playerSide && buttonList [2].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [3].text == playerSide && buttonList [4].text == playerSide && buttonList [5].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [6].text == playerSide && buttonList [7].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [0].text == playerSide && buttonList [3].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [1].text == playerSide && buttonList [4].text == playerSide && buttonList [7].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [2].text == playerSide && buttonList [5].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [0].text == playerSide && buttonList [4].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (buttonList [2].text == playerSide && buttonList [4].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       }

       if (moveCount >= 9)
       {
           GameOver("draw");
       }

       ChangeSides();

   }

   void ChangeSides ()
   {
       playerSide = (playerSide == "X") ? "O" : "X";
   }

   void GameOver (string winningPlayer)
   {
       SetBoardInteractable(false);
       if (winningPlayer == "draw")
       {
           SetGameOverText("It's a Draw!");
       } else
       {
           SetGameOverText(winningPlayer + " Wins!");
       }
       restartButton.SetActive(true);
   }

   void SetGameOverText (string value)
   {
       gameOverPanel.SetActive(true);
       gameOverText.text = value;
   }

   public void RestartGame ()
   {
       playerSide = "X";
       moveCount = 0;
       gameOverPanel.SetActive(false);
       restartButton.SetActive(false);
       SetBoardInteractable(true);

       for (int i = 0; i < buttonList.Length; i++)
       {
           buttonList [i].text = "";
       }
   }

   void SetBoardInteractable (bool toggle)
   {
       for (int i = 0; i < buttonList.Length; i++)
       {
           buttonList[i].GetComponentInParent<Button>().interactable = toggle;
       }
   }
}

This code is beginning to look much more neat and logical. In EndTurn if the game is over, the function GameOver is called regardless as to whether this is from a draw or a win. The function GameOver handles all of the game over logic. Each function that we write is doing what it says on the can. The name of the function states what the function does. All related functionality is contained within that appropriate function. This way we can more easily follow the flow of logic when reading the code, while more easily fixing and maintaining the code as it is clear, ordered and compact.

  • Save the script.
  • Return to Unity.
  • Enter Play Mode.
  • Test by clicking any of the spaces.

The Restart Button now works if either we win or draw.

description

In the next lesson we will create two small panels to indicate whose turn it is to play.