Whose turn is it?

Checked with version: 5.3

-

Difficulty: Beginner

At first glance, it’s difficult to know whose turn it is to play.

To show whose turn it is, we will create a small panel with an "X" and an "O" that will change color, indicating the current player.

  • Create a UI Panel element in the scene by using Create > UI > Panel
  • With the Panel GameObject selected,
    • ... rename the GameObject "Player X".
    • ... reset the RectTransform using the context sensitive gear menu.
    • ... drag the panel to (-206, 330).

description

It’s worth noting that, if we’ve accurately followed the tutorial so far, the RectTransform Position (-206, 330) should line up with the edge of the Board panel and align with our Restart Button.

  • With the Player X GameObject selected,
    • ... set the *Color *to very dar blue (33, 44, 55, 255) using the preset.
    • ... add a UI Text element as a child.

Note: it’s best to use the Context Menu by right clicking while the Player X GameObject is selected and selecting UI > Text. It is also worth noting how easily we can add to and modify our UI Elements by adding new UI Elements as children.

  • Select the Text GameObject that is a child of the Player X GameObject.
  • With this Text GameObject selected,
    • ... set the Anchor, Pivot and Position to stretch.
    • ... set the Text property to "X".
    • ... set the Font Size to 86.
    • ... set the Alignment to middle/center.
    • ... set the Color to blue (0, 204, 204, 255) using the preset.
  • Duplicate the Player X GameObject.
  • Select the Player X (1) GameObject and rename it "Player O".
  • With the Player O GameObject selected,
    • ... change the RectTransform Position X to 206 (not -206).
    • ... set the Text property to "O".

We are setting both of the player panels to the same dark color scheme with a dark blue background and blue text as we will modify the current active panel to a new, brighter color scheme in code. At this point, "X" is the default starting side. We could have set the player panel for "X" to the brighter color scheme to start with, but we will see further on in this lesson that this could limit our possibilities, especially if we let the player choose their starting side.

description

To make things easier on ourselves, at this stage it might be best to describe the player display in a generic way that we can use in our GameController script. We can do this be creating a class that holds all of the relevant details for our player display. This Player class will hold the definition of the player icon panel, including the background and text.

  • Open the GameController script for editing.
  • At the top of the script, under the namespace definitions but before the GameController class definition,
    • ... create a new public class called "Player".
  • In Player,
    • ... define a new public Image variable called "panel".
    • ... define a new public Text variable called "text".

We will also need to alternate between an active color scheme and an inactive color scheme. These could be setup directly in the GameController script, but, as we have done with the Player class, it might be tidier to define our color schemes in a similar way by creating a PlayerColor class.

  • At the top of the script, under the namespace definitions but before the GameController class definition,
    • ... create a new public class called "PlayerColor".
  • In PlayerColor,
    • ... define a new public Color variable called "panelColor".
    • ... define a new public Color variable called "textColor".

At this point these two classes will not be visible in the Inspector Window even if we include an instance of them in our script. We will need these visible in the Inspector so we can set their properties in an easy manner. For these two classes to be visible in the Inspector, they need to be serialized by Unity. We can serialize these classes by using the [System.Serializable] attribute. For more information please see the page on the [System.Serializable] attribute and the page on Serialization in Unity.

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

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

In the body of the GameController class, we will need 2 new instances of Player and two new instances of PlayerColor.

  • Define a new public Player variable called "playerX".
  • Define a new public Player variable called "playerO".
  • Define a new public PlayerColor variable called "activePlayerColor".
  • Define a new public PlayerColor variable called "inactivePlayerColor".
public Player playerX;
public Player playerO;
public PlayerColor activePlayerColor;
public PlayerColor inactivePlayerColor;

At this stage, it’s best to check that everything is working and the serialization has been set up correctly.

  • Save the script.
  • Return to Unity.

We should now see Player X, Player O, Active Player Color and Inactive Player Color in the Inspector Window.

description

Let's set up the two Player properties and the two PlayerColor properties while we are here in the Unity Editor.

  • Select the GameController GameObejct.
  • With the Game Controller selected,
    • ... open the dropdown for Player X.
    • ... assign the Player X Panel property to the Player X GameObject
    • ... assign the Player X Text property to the Text GameObject associated with the Player X GameObject

description

  • With the Game Controller selected,
    • ... open the dropdown for Player O.
    • ... assign the Player O Panel property to the Player O GameObject
    • ... assign the Player O Text property to the Text GameObject associated with the Player O GameObject
    • ... open the Active Player Color drop down.
    • ... set the Panel Color to blue (0, 204, 204, 255) using the preset.
    • ... set the Text Color to pink (255, 0, 102, 255) using the preset.
    • ... open the Inactive Player Color drop down.
    • ... set the Panel Color to very dark blue (33, 44, 55, 255) using the preset.
    • ... set the Text Color to blue (0, 204, 204, 255) using the preset.

description

Now we have an easy and clear definition of both of our Player panels and our Player Colors in the Inspector.

There are a few advantages to this approach over simply declaring all of these eight properties in the GameController script directly. First, it organizes our code; like with like. The Player and Player Color are defined by the same properties, so they each share a class. By having an instance of a class in our script, the properties of that class show up as sub-properties in the Inspector with a collapsible container. This means we can group our properties in a way that is clearly organized in the Inspector Window and we can collapse the drop down view in the Inspector, hiding the bulk of properties after we have defined if we don’t need them visible.

This is this Inspector Window with all of the elements open:

description

... and this is the same Inspector Window with all of the elements closed:

description

The next big advantage we get is control over our code when we try to manipulate these properties in our script. As we have defined our Player as a class with properties, we can pass the entire Player around as a single unit, rather than trying to pass the individual properties themselves. We will see this more clearly as be begin to set up the code we need to set the colors on these panels.

  • Open the GameController script for editing.

To set the colors of the the panels we will create a new generic function that will change the colors of the player panels when we change sides.

  • Create a new function that returns void called "SetPlayerColors " that has two Player parameters called "newPlayer"and "oldPlayer".
void SetPlayerColors (Player newPlayer, Player oldPlayer)
{

}
  • In the SetPlayerColors function,
    • ... set the newPlayer’s colors to the active color.
    • ... set the oldPlayer’s colors to the inactive color.
newPlayer.panel.color = activePlayerColor.panelColor;
newPlayer.text.color = activePlayerColor.textColor;
oldPlayer.panel.color = inactivePlayerColor.panelColor;
oldPlayer.text.color = inactivePlayerColor.textColor;

It is worth noting here that we are sending the entire Player definition to SetPlayerColor as an argument and then assigning values to the sub-properties based on the Player passed to the function. If we had not defined the Player and PlayerColor in their own classes, we would have had more complicated code. In the very least, the signature of the function would have required each of the properties as a separate parameter, such as SetPlayerColors (Image newPlayerImage, Text newPlayerText, Image oldPlayerImage, Text oldPlayerText). Our current style is much more efficient and far easier to read and understand.

This is the generic functional code that will set the colors on the player panels. It simply assigns colors to the Players in the order we send them. It will need to be called every time we change sides. To do that, we will need to add code to the function ChangeSides that will send the newPlayer and the oldPlayer to SetPlayerColor depending upon whose turn it is; where if the playerSide is X we send playerX and playerO - in that order, or else we send playerO and playerX. This is because if, in ChangeSides, our playerSide is "X", then playerX is the newPlayer and "O" is the oldPlayer, and vice versa.

  • In the ChangeSides function,
    • ... add an if/else statement, where the logic checks if playerSide is "X".
if (playerSide == "X")
{
    
} 
else
{
    
}
  • In the ChangeSides function,
    • ... when the if is true, call SetPlayerColors with playerX as the new player.
SetPlayerColors(playerX, playerO);
  • In the ChangeSides function,
    • ... when the if is false, in the else block, call SetPlayerColors with playerO as the new player.
SetPlayerColors(playerO, playerX);

We now need to set the color scheme for the starting side to the activePlayerColor. At this point, "X" is the starting side by default, so we can simply set the player panel for "X" as the newPlayer.

  • In Awake,
    • ... add a call to SetPlayerColors with playerX as the new player.
SetPlayerColors(playerX, playerO);

Lastly, when we restart the the game, we want to set "X" to have the activePlayerColor.

  • In RestartGame,
    • ... add a call to SetPlayerColors with playerX as the new player.
SetPlayerColors(playerX, playerO);

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

[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();
       playerSide = "X";
       gameOverPanel.SetActive(false);
       moveCount = 0;
       restartButton.SetActive(false);
       SetPlayerColors(playerX, playerO);
   }

   void SetGameControllerReferenceOnButtons ()
   {
       for (int i = 0; i < buttonList.Length; i++)
       {
           buttonList[i].GetComponentInParent().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";
       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!");
       } 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);
       SetPlayerColors(playerX, playerO);
       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().interactable = toggle;
       }
   }
}
  • Save the script.
  • Return to Unity.
  • Enter Play Mode.
  • Test by clicking any of the spaces.

When we enter Play Mode, we should see the "X" panel highlight with the bright color scheme indicating that "X" is the current player. When we click on a space, the space should be assigned to "X", and the "O" panel should now have the bright color scheme. When a game is complete, either a win or a draw, the restart button should appear.

There is a little glitch, however. Dare we say a bug?

When one player wins, the other side’s panel gets highlighted:

description

Even though "X" wins, the "O" panel is highlighted. This is a small issue, but, as we discussed earlier, we are in the polish phase for this project and we need everything as perfect as possible.

Why is this happening?

Let’s look at the code.

  • Open the *GameController *script for editing.

If we look at the EndTurn function, we will see that all of the code is being executed every time the function is being called. What’s happening here is that after incrementing the moveCount, the function is checking every if statement and then at the end of the function, the function is calling ChangeSides. We only want to call ChangeSides if the the game is not over, not every time we end a turn.

One of the simplest solutions to this issue is to change these individual if statements into a single series of if/else if’s. This way we won’t have to check every line of code if one of the earlier lines tests true. This also means we can leave the last line of code as a simple else, so we will only call ChangeSides if there is no win nor a draw.

  • In EndTurn,
    • ... leave the first if unchanged.
    • ... change all of the remaining if’s to else if, including the test on moveCount.
    • ... before ChangeSides, add an else.

With this logic, we check all of the possible win conditions. If one of the win conditions tests true, we call game over and get our of the if/else logic. If none of the win conditional prove true, including a draw, we then change sides.

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

[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();
       playerSide = "X";
       gameOverPanel.SetActive(false);
       moveCount = 0;
       restartButton.SetActive(false);
       SetPlayerColors(playerX, playerO);
   }

   void SetGameControllerReferenceOnButtons ()
   {
       for (int i = 0; i < buttonList.Length; i++)
       {
           buttonList[i].GetComponentInParent().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);
       } 
       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!");
       } 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);
       SetPlayerColors(playerX, playerO);
       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().interactable = toggle;
       }
   }
}
  • Save the script.
  • Return to Unity.
  • Enter Play Mode.
  • Test by clicking any of the spaces.

Now when we enter Play Mode, we should see the "X" panel highlight with the bright color scheme indicating that "X" is the current player. When we click on a space, the space should be assigned to "X", and the "O" panel should now have the bright color scheme. When a game is complete, either a win or a draw, the restart button should appear. Now, however, when one player wins, the their side’s panel stays highlighted:

description

In the next lesson we will explore how to choose either "X" or "O" as the starting side.

Related tutorials

Related documentation