Choosing the starting side and starting the game

Revisado con versión: 5.3

-

Dificultad: Principiante

Why should "X" have all the fun?

If we want to allow our players to pick the starting side, how should we do it?

The easiest way to do this would be to give the player a button to click.

It would be convenient if we could just click on the Player X or Player Y panels. Unfortunately, they are not buttons. Now, we could create new button elements, delete the panels we have just created and reconnect all of these elements new by swapping out the references to the GameObjects in the GameController, but this will be extra work and time.

One important point to bear in mind, however, is that the Button component is just that - a component.

We can simply add a Button component to the existing player panel GameObjects and we would not need to make any changes to the existing references to the Image or Text components on the player panels accommodate a new button.

We can add Button components to both player panels at the same time using multi-selection editing. For more information on multi-selection editing, please see the page on multi-selection editing and the information linked below.

  • Make sure both Player X and Player O GameObjects are closed or collapsed.
  • Select both Player X and Player O GameObjects.
  • With both Player X and Player O GameObjects selected,
    • ... add a Button component using UI > Button.

description

We don’t want these buttons to be interactable by default. We want control over these buttons and will activate them during the appropriate states of the game.

  • With both Player X and Player O GameObjects selected,
    • ... set the Button’s Transition to None.

Next, we need to update our code to react to the button being clicked.

  • Open the GameController script for editing.

To react to a UI Button we will need a public function so we can access it from our new Button components. Looking at our code, we are setting the starting playerSide in Awake and resetting it in RestartGame.

We will need a new public function to set the starting side.

  • Create a new public function that returns void called "SetStartingSide" that has a string parameter called "startingSide".
  • Set playerSide to the parameter startingSide.
  • Add an if/else statement, where the logic checks if playerSide is "X" and,
    • ... when the if is true, call SetPlayerColors with playerX as the new player.
    • ... when the if is false, in the else block, call SetPlayerColors with playerO as the new player.
public void SetStartingSide (string startingSide)
{
    playerSide = startingSide;
    if (playerSide == "X")
    {
        SetPlayerColors(playerX, playerO);
    } 
    else
    {
        SetPlayerColors(playerO, playerX);
    }
}

As we are now setting the starting side in a function that will be called by the player buttons, we need to remove any code setting playerSide or the color scheme from inside our script.

  • Remove the line setting playerSide from Awake.
  • Remove the line calling SetPlayerColors from Awake.
  • Remove the line setting playerSide from RestartGame.
  • Remove the line calling SetPlayerColors from RestartGame.

If we now wait for the player to choose a side, we can’t simply have the game ready and waiting to be played. The game board will have to start in a non-interactable state, so no one can select any grid spaces before they have chosen a starting side. As such, we will now need to find a way to start the game. We have our RestartGame function, which can help, but RestartGame is really doing something different.

Let’s think through our game and the cycle that it goes through as we are playing.

The game really exists in a few distinctly different states.

Before we created SetStartingSide, the game started in a game playing state. Anyone could jump in and just start playing. The function GameOver transitioned us into a new state, that of game over. In the game over state, the game board is deactivated, the winning conditions and the winner - if there is one - are displayed. The function RestartGame would take us back into the game playing state.

We are now adding a new state to this cycle.

This will be the waiting to play state where we have to choose our starting side..

We now need to change a few things. We need to have a way to transition from the waiting to play state into the game playing state. This will be done by calling a function when we click the player panel to choose a side. We also need to change RestartGame so that it carries us into the waiting to play state rather that the game playing state.

The first thing we want to do is set up our waiting to play state.

In our waiting to play state, we will want all of the grid spaces on the game board non-interactable. We will want the buttons on the player panel, on the other hand, as interactable. This initial state can be setup in the Inspector and in Awake as our entry point into the game. Let’s start by setting all of the grid spaces to non-interactable.

  • Save the Script.
  • Return to Unity.
  • Make sure all of the Grid Space GameObjects are closed or collapsed so only to root GameObject is shown in the Inspector..
  • Select all of the Grid Space GameObjects.
  • Using multi-selection editing, set the Button component’s interactable property to false.

description

While we are in the Editor, let’s also hook up the buttons to the SetStartingSide function we have just written.

  • Make sure both Player X and Player O GameObjects are closed or collapsed.
  • Select both Player X and Player O GameObjects.
  • With both Player X and Player O GameObjects selected,
    • ... add a new row to the Buttons’ OnClick list.
    • ... drag the Game Controller GameObejct 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 > SetStartingSide.

We need to now pass in a parameter for either "X" or "O" depending upon which button is clicked. We can do this using the Argument Field in the Button’s OnClick List. When we have a button and we have selected a public function that requires requires an argument, we can use the Argument Field to send it.

  • Select only the Player X GameObject.
  • In the Argument Field in the Button’s OnClick List set the value to "X".

description

  • Select only the Player O GameObject.
  • In the Argument Field in the Button’s OnClick List set the value to "O".

This sets up the functionality of the buttons on our player panels.

  • Save the Scene.
  • Open the GameController script for editing.

To transition from the waiting to play state into the game playing state, we need to call a function when we have selected our player panel and chosen a starting side. This function will be called from SetStartingSide.

  • Create a new function that returns void called "StartGame".

Next, we need to call StartGame. Where want to do this is after we select our starting side.

  • Add a call to StartGame at the end of SetStartingSide.

RestartGame will now carry us into the waiting to play state rather than starting the game. Setting the game board as interactable will now be taken care of by StartGame, so we need to move it.

  • In RestartGame,
    • ... cut the call to SetBoardInteractable.
  • In StartGame,
  • ... paste the call to SetBoardInteractable.
void StartGame ()
{
    SetBoardInteractable(true);
}

This sets up the primary functional code that we need. We will need a little more code to make the game look and feel polished. We will be selecting our starting side by clicking a button. We only want these buttons interactable when the game is waiting to play or the players would be able to, again, arbitrarily restart the game. Just like we did the the Restart Button we will need to control when the player panels are interactable.

We could add two new variables to hold the references to the buttons, but, as we already have a definition of the Player and we are using these references already, let’s add a reference to the player panel’s Button component to the Player class.

  • To the Player class,
    • ... add a public Button variable called "button".
[System.Serializable]
public class Player {
    public Image panel;
    public Text text;
    public Button button;
}

By default, the buttons on the player panels are interactable. This is how we set them up in the Inspector. Now, in StartGame we will need to turn them off so they can’t be used during the game. In RestartGame we will need to turn them back on again so the player can choose a side for the next game.

Let's do this right from the beginning!

  • Create a new function that returns void called "SetPlayerButtons" that has a boolean parameter called "toggle".
  • In SetPlayerButtons,
    • ... set the playerX button’s interactable property to toggle.
    • ... set the playerO button’s interactable property to toggle.
void SetPlayerButtons (bool toggle)
{
    playerX.button.interactable = toggle;
    playerO.button.interactable = toggle;  
}
  • In StartGame,
    • ... call SetPlayerButtons with false as the argument.
SetPlayerButtons (false);
  • In RestartGame,
    • ... call SetPlayerButtons with *true *as the argument.
SetPlayerButtons (true);

The last bit of polish we need to do before we test this step is to remove the highlight from the current player panel when we restart the game. When the game is over, the last player to take a turn has their panel highlighted. When we restart the game, we need to reset this back to the starting color. Again, we want to put all of this code in a single place that we can easily call from any function.

  • Create a new function that returns void called "SetPlayerColorsInactive".
  • Set all of the player’s panels and text to the inactive color scheme.
void SetPlayerColorsInactive ()
{
    playerX.panel.color = inactivePlayerColor.panelColor;
    playerX.text.color = inactivePlayerColor.textColor;
    playerO.panel.color = inactivePlayerColor.panelColor;
    playerO.text.color = inactivePlayerColor.textColor;
}
  • In RestartGame,
    • ... call SetPlayerColorsInactive.

Now, as we have put all of this code into one place and we can call it any time we want, let’s add a call to SetPlayerColorsInactive when there is a draw. This way neither player gets their panel highlighted if they did not win.

  • In GameOver when the winningPlayer is "draw",
    • ... call SetPlayerColorsInactive.
if (winningPlayer == "draw")
{
    SetGameOverText("It's a Draw!");
    SetPlayerColorsInactive();
}

The final script should look like this:

GameController

Code snippet

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

[System.Serializable]
public class Player {
   public Image panel;
   public Text text;
   public Button button;
}

[System.Serializable]
public class PlayerColor {
   public Color panelColor;
   public Color textColor;
}

public class GameController : MonoBehaviour {

   public Text[] buttonList;
   public GameObject gameOverPanel;
   public Text gameOverText;
   public GameObject restartButton;
   public Player playerX;
   public Player playerO;
   public PlayerColor activePlayerColor;
   public PlayerColor inactivePlayerColor;

   private string playerSide;
   private int moveCount;

   void Awake ()
   {
       SetGameControllerReferenceOnButtons();
       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 void SetStartingSide (string startingSide)
   {
       playerSide = startingSide;
       if (playerSide == "X")
       {
           SetPlayerColors(playerX, playerO);
       } 
       else
       {
           SetPlayerColors(playerO, playerX);
       }

       StartGame();
   }

   void StartGame ()
   {
       SetBoardInteractable(true);
       SetPlayerButtons (false);
   }

   public string GetPlayerSide ()
   {
       return playerSide;
   }

   public void EndTurn ()
   {
       moveCount++;

       if (buttonList [0].text == playerSide && buttonList [1].text == playerSide && buttonList [2].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [3].text == playerSide && buttonList [4].text == playerSide && buttonList [5].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [6].text == playerSide && buttonList [7].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [0].text == playerSide && buttonList [3].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [1].text == playerSide && buttonList [4].text == playerSide && buttonList [7].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [2].text == playerSide && buttonList [5].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [0].text == playerSide && buttonList [4].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [2].text == playerSide && buttonList [4].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (moveCount >= 9)
       {
           GameOver("draw");
       } 
       else
       {
           ChangeSides();
       }
   }

   void ChangeSides ()
   {
       playerSide = (playerSide == "X") ? "O" : "X";
       if (playerSide == "X")
       {
           SetPlayerColors(playerX, playerO);
       } 
       else
       {
           SetPlayerColors(playerO, playerX);
       }
   }

   void SetPlayerColors (Player newPlayer, Player oldPlayer)
   {
       newPlayer.panel.color = activePlayerColor.panelColor;
       newPlayer.text.color = activePlayerColor.textColor;
       oldPlayer.panel.color = inactivePlayerColor.panelColor;
       oldPlayer.text.color = inactivePlayerColor.textColor;
   }

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

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

   public void RestartGame ()
   {
       moveCount = 0;
       gameOverPanel.SetActive(false);
       restartButton.SetActive(false);
       SetPlayerButtons (true);
       SetPlayerColorsInactive();

       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;
       }
   }

   void SetPlayerButtons (bool toggle)
   {
       playerX.button.interactable = toggle;
       playerO.button.interactable = toggle;  
   }

   void SetPlayerColorsInactive ()
   {
       playerX.panel.color = inactivePlayerColor.panelColor;
       playerX.text.color = inactivePlayerColor.textColor;
       playerO.panel.color = inactivePlayerColor.panelColor;
       playerO.text.color = inactivePlayerColor.textColor;
   }
}
  • Save the script.
  • Return to Unity.

We need to associate the Player Button references on the Game Controller with the two Player GameObjects.

  • Select the Game Controller GameObject.
  • With the Game Controller GameObject selected,
    • ... drag the Player X GameObject onto the Player X Button field.
    • ... drag the Player O GameObject onto the Player O Button field.

description

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

When we first come to the game board, all of the grid spaces are inactive. We can’t do anything with them. To start the game, we need to select a side by clicking either the "X" or the "O". Once we have chosen our side, the game plays normally. When the game is over, we get our banner displaying the winning conditions and the Restart Button is displayed. If we have a draw, then neither player is highlighted as the winner. When we restart the game, we can choose a new starting side.

The only problem I see here is that when looking at the game for the first time, we don’t know what we are supposed to do. The game board is locked. It’s inactive. If we don’t know that we need to click either the "X" or the "O", we may feel the game is broken and quit.

As the final and last step, let’s add a small descriptive panel informing us to pick a side.

  • Duplicate the Restart Button GameObject in the Hierarchy.
  • Select the Restart Button (1) GameObject.
  • With Restart Button (1) GameObject selected,
    • ... rename the GameObject to "Start Info".
    • ... set the Image component’s Color to blue (0, 204, 204, 255) using the preset.

We have duplicated a UI Button element. We don’t want the button functionality, however. We simply want this UI element to be a display panel with a background and text. Now, just as we added a Button component to a panel by using the Add Component menu, we can also simply remove a Button and its functionality from a GameObject by removing the component.

  • With the Start Info GameObject selected,
    • ... remove the Button component using the context sensitive gear menu.

Now we need to change the Text displayed on the panel.

  • Select the child Text GameObject of the Start Info GameObject.
  • With the Text GameObject selected,
    • ... set the Text property to "X or O?" and on a new line "Choose a side!".
    • ... set the Font Size to 21.

description

All we have to do now is set up this panel so we only see it when we need to choose a side during the waiting to play state of the game.

  • Open the GameController script for editing.
  • Declare a public GameObject variable called "startInfo".
public GameObject startInfo;

This panel will start in an active state. This is how we have it set up in the Inspector and we have not modified this state in Awake. When the player chooses a side and the game starts, we want to deactivate the panel. When the game is over and the player chooses to restart, we want to reactivate it.

  • In StartGame,
    • ... deactivate the startInfo GameObject.
startInfo.SetActive(false);
  • In RestartGame,
    • ... activate the startInfo GameObject.
startInfo.SetActive(true);

The final script should look like this:

GameController

Code snippet

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

[System.Serializable]
public class Player {
   public Image panel;
   public Text text;
   public Button button;
}

[System.Serializable]
public class PlayerColor {
   public Color panelColor;
   public Color textColor;
}

public class GameController : MonoBehaviour {

   public Text[] buttonList;
   public GameObject gameOverPanel;
   public Text gameOverText;
   public GameObject restartButton;
   public Player playerX;
   public Player playerO;
   public PlayerColor activePlayerColor;
   public PlayerColor inactivePlayerColor;
   public GameObject startInfo;

   private string playerSide;
   private int moveCount;

   void Awake ()
   {
       SetGameControllerReferenceOnButtons();
       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 void SetStartingSide (string startingSide)
   {
       playerSide = startingSide;
       if (playerSide == "X")
       {
           SetPlayerColors(playerX, playerO);
       } 
       else
       {
           SetPlayerColors(playerO, playerX);
       }

       StartGame();
   }

   void StartGame ()
   {
       SetBoardInteractable(true);
       SetPlayerButtons (false);
       startInfo.SetActive(false);
   }

   public string GetPlayerSide ()
   {
       return playerSide;
   }

   public void EndTurn ()
   {
       moveCount++;

       if (buttonList [0].text == playerSide && buttonList [1].text == playerSide && buttonList [2].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [3].text == playerSide && buttonList [4].text == playerSide && buttonList [5].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [6].text == playerSide && buttonList [7].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [0].text == playerSide && buttonList [3].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [1].text == playerSide && buttonList [4].text == playerSide && buttonList [7].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [2].text == playerSide && buttonList [5].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [0].text == playerSide && buttonList [4].text == playerSide && buttonList [8].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (buttonList [2].text == playerSide && buttonList [4].text == playerSide && buttonList [6].text == playerSide)
       {
           GameOver(playerSide);
       } 
       else if (moveCount >= 9)
       {
           GameOver("draw");
       } 
       else
       {
           ChangeSides();
       }
   }

   void ChangeSides ()
   {
       playerSide = (playerSide == "X") ? "O" : "X";
       if (playerSide == "X")
       {
           SetPlayerColors(playerX, playerO);
       } 
       else
       {
           SetPlayerColors(playerO, playerX);
       }
   }

   void SetPlayerColors (Player newPlayer, Player oldPlayer)
   {
       newPlayer.panel.color = activePlayerColor.panelColor;
       newPlayer.text.color = activePlayerColor.textColor;
       oldPlayer.panel.color = inactivePlayerColor.panelColor;
       oldPlayer.text.color = inactivePlayerColor.textColor;
   }

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

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

   public void RestartGame ()
   {
       moveCount = 0;
       gameOverPanel.SetActive(false);
       restartButton.SetActive(false);
       SetPlayerButtons (true);
       SetPlayerColorsInactive();
       startInfo.SetActive(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;
       }
   }

   void SetPlayerButtons (bool toggle)
   {
       playerX.button.interactable = toggle;
       playerO.button.interactable = toggle;  
   }

   void SetPlayerColorsInactive ()
   {
       playerX.panel.color = inactivePlayerColor.panelColor;
       playerX.text.color = inactivePlayerColor.textColor;
       playerO.panel.color = inactivePlayerColor.panelColor;
       playerO.text.color = inactivePlayerColor.textColor;
   }
}
  • Save the script.
  • Return to Unity.

Now all we need to do is hook up the Start Info GameObject to the Game Controller.

  • Select the Game Controller GameObject.
  • With the Game Controller GameObject selected,
    • ... drag the Start Info GameObject onto the Start Info field.

description

  • Save the Scene.
  • Enter Play Mode.
  • Test by selecting a side and clicking any of the spaces.

Now we are done.

We start in a state where we are waiting to play. There is a small piece of instructional text informing us that we need to choose a side and two buttons for our starting player to choose the side the want.

When a player chooses a side, the game begins. This removes the instructional text, makes the player buttons non-interactable and enables the game board by making the grid space buttons interactable.

Game Play allows us to choose any of the grid spaces. The button associated with the grid space asks the Game Controller for the current player’s side and sets the grid space with the appropriate text and then sets itself as non-interactable. As a final act, the grid space button hands control back to the Game Controller to check the winning conditions and if the game is not over, the GameController changes sides and the game continues.

When the game is over, we change states again into our game over state. Here we display the winning panel and activate the Restart Button.

Selecting the Restart Button sends us back to our starting state and we are waiting to play, with our instructional text and buttons for our player to choose a side.

description

There is much more that can be done to make this project a more complete game. We leave that up to you. Things you could try might be:

  • A splash screen at the start of the game
  • Counting points - "Who has won the most games?
  • Sound effects
  • Music
  • Animation

There are several points we would like you to take away from this project.

One is that the UI is a versatile toolset and there may be uses for the UI and UI elements that may not have been obvious at first glance.

Another is that you can approach your project by solving one problem at a time. You don't need to know every solution before you start. Break a project down into pieces. If you know you will need a function or script but are not ready to start work on it, make an empty placeholder and continue with your current problem.

Keep your code organized. If there is the need to DoSomething() make a function to do it and call that function. Don't have code, especially duplicate code, scattered all over a project.

You will change and refactor your project as you develop it. Don't be afraid to do so. Hand in hand with solving one problem at a time, you will expand, rewrite and refactor your code many times.

Thanks for reading.

If you have any questions or comments, please see the official forum thread.