Introduction to Building a Tic Tac Toe Game in Unity
In this tutorial, you'll learn how to create a simple Tic Tac Toe game in Unity. We’ll guide you through setting up the game’s UI, handling player interactions, and implementing the game logic.
By the end, you'll have a working game where players take turns marking the grid, and the game detects wins or draws. We’ll also cover resetting the game and updating the UI with game results.
This guide is perfect for beginners or those looking to enhance their Unity skills with a fun project. Let’s get started!
Step 1: Project Setup
Create a new Unity 2D project.
-
Folder Structure:
- Under the
Assets
folder, create a folder namedTicTacToe
. Inside this folder, create:- Scripts
- Prefabs
- Resources
- Scenes
- Create a new Scene and save it under the
Scenes
folder.
- Under the
-
Resources Folder:
- Inside the Resources folder, add 3 square images:
x.png
(X icon),o.png
(O icon), andb.png
(empty image with a white background). Download the images from here and drag them into the Resources folder.
- Inside the Resources folder, add 3 square images:
-
Script Folder:
- Create several scripts by right-clicking in the Scripts folder and selecting
Create > C# Script
. Name the scripts as follows:Cell.cs
,UICellBehaviour.cs
,BoardManager.cs
,TurnManager.cs
, andUIBehaviour.cs
. We’ll explain why we need each of these scripts later.
- Create several scripts by right-clicking in the Scripts folder and selecting
- PersistentMonoSingleton.cs: Download here and drop it into the Scripts folder. This class will help us make a class both persistent (accessible globally across scenes) and singleton (only one instance exists in the entire game). This class is not directly related to gameplay but is used to manage global game objects.
Step 2: UI Setup in Unity
Now, let’s plan the visual representation of the Tic Tac Toe game using Unity’s Canvas system. The following steps will guide you through setting up the 3x3 grid and linking the logic to the visual elements.
-
Create a Canvas:
- Open the newly created scene and switch to 2D mode in the Scene view.
- Right-click in the Hierarchy and select UI -> Canvas.
-
Create a Grid Panel:
- Right-click on the Canvas and create a new Panel. Rename it
Grid
. - Set the Rect Transform to Stretch and add an Aspect Ratio Fitter component. Set:
- Aspect Mode: Fit in Parent
- Aspect Ratio: 1
- Right-click on the Canvas and create a new Panel. Rename it
-
Vertical Layout Group:
- Inside the
Grid
, create another Panel. Rename itVertical Layout Group
. - Set its Rect Transform to Stretch.
- Add a Vertical Layout Group component and enable:
- Spacing: 10
- Control Child Size (Width and Height)
- Child Force Expand (Width and Height)
- Inside the
-
Row Panel:
- Inside the
Vertical Layout Group
, create another Panel and name itRow
. - Add a Horizontal Layout Group component and enable:
- Spaceing 10
- Control Child Size (Width and Height)
- Child Force Expand (Width and Height)
- Inside the
-
Cell Button:
- Inside the
Row
panel, right-click and add a UI -> Image. Rename itCell Button
. - From Inspector window, click Add-Component, add a Button component to the
Cell Button
.
- Inside the
Step 3: Prefab Creation and Replication
-
Create a Prefab:
- Drag the
Cell Button
from the Hierarchy into your project’s Prefab folder to create a prefab.
- Drag the
-
Duplicate Buttons:
- In the
Row
panel, duplicate theCell Button
twice.
- In the
- Duplicate the
Row
panel twice to create three rows. You now have a 3x3 grid with 9 buttons.
-
Naming Cells:
- Rename each button:
Cell Button 0
,Cell Button 1
, ...,Cell Button 8
.
- Rename each button:
Note: In the Scene view, set the camera background to Solid Color and choose the color you want for the board background.
Managing the Cell State with Cell.cs
The Cell.cs
script is responsible for managing the state of each individual cell in the Tic Tac Toe game. It stores the cell's current value, controls its interactivity, and communicates changes to other parts of the game via events.
Key Responsibilities of Cell.cs
:
- Track Cell State: It keeps track of whether the cell is blank, marked with X, or marked with O.
- Handle Interactivity: It determines whether the cell is clickable based on the current game state.
- Trigger Game Events: It raises events when the cell’s value changes or when the game finishes, allowing other game components to respond accordingly.
using System;
using UnityEngine;
namespace com.marufhow.tictactoe.script
{
[CreateAssetMenu(fileName = "Cell", menuName = "Game/Cell")]
public class Cell : ScriptableObject
{
public int Id;
public int Value; // 0: blank, 1: X, 2: O
public bool IsInteractive { get; private set; }
public event Action<int, int> OnValueChanged;
public event Action<bool> OnGameFinished;
public void SetValue(int newValue)
{
Value = newValue;
OnValueChanged?.Invoke(Id, Value);
}
public void SetResult(bool isWin)
{
OnGameFinished?.Invoke(isWin);
IsInteractive = false;
}
public void Reset()
{
IsInteractive = true;
SetValue(0);
}
}
}
Script Breakdown:
- Id: A unique identifier for each cell.
- Value: Tracks whether the cell is blank, marked with X, or marked with O.
- IsInteractive: Controls whether the cell is clickable.
- OnValueChanged and OnGameFinished: Events that notify the game when a cell's value changes or when the game finishes.
Key Methods of Cell.cs
SetValue(int newValue)
: Updates the cell's value (X, O, or blank) and triggers theOnValueChanged
event to notify listeners.SetResult(bool isWin)
: Marks the cell as part of a winning combination or not, disables interactivity, and triggers theOnGameFinished
event.Reset()
: Resets the cell's value to blank and makes it interactive again for a new game.
Connecting the UI with UICellBehaviour.cs
The UICellBehaviour.cs
script links the cell’s logic to its visual representation in the Unity UI. It handles player input, updates the button's visual state, and reacts to changes in the cell's value during gameplay.
Key Responsibilities of UICellBehaviour.cs
:
- Update Cell Appearance: It ensures the UI reflects the current state of the cell, displaying either X, O, or blank based on the player's action.
- Handle Player Interaction: It listens for clicks on the cell button and updates the cell value based on whose turn it is.
- Respond to Cell Events: It listens for changes in the cell’s state and updates the UI accordingly, including handling game results (win or loss).
using UnityEngine;
using UnityEngine.UI;
namespace com.marufhow.tictactoe.script
{
public class UICellBehaviour : MonoBehaviour
{
[SerializeField] private Cell cell;
[SerializeField] private Button button;
[SerializeField] private Image image;
[SerializeField] private Color defaultColor;
[SerializeField] private Color winColor;
[SerializeField] private Color failedColor;
private Sprite xImage;
private Sprite oImage;
private Sprite blankImage;
private void Awake()
{
xImage = Resources.Load<Sprite>("x");
oImage = Resources.Load<Sprite>("o");
blankImage = Resources.Load<Sprite>("b");
}
private void Start()
{
button.onClick.AddListener(OnButtonClick);
}
private void OnEnable()
{
cell.OnValueChanged += OnValueChanged;
cell.OnGameFinished += OnGameFinished;
}
private void OnDisable()
{
cell.OnValueChanged -= OnValueChanged;
cell.OnGameFinished -= OnGameFinished;
}
private void OnValueChanged(int cell, int newValue)
{
image.sprite = newValue == 1 ? xImage : oImage;
if (newValue != 0) return; // game restart
image.sprite = blankImage;
image.color = defaultColor;
}
private void OnGameFinished(bool isGameWin)
{
image.color = isGameWin ? winColor : failedColor;
}
private void OnButtonClick()
{
if(!cell.IsInteractive) return;
if (cell.Value == 0) // Only change if blank
{
bool isXTurn = TurnManager.Instance.GetTurn();
int newValue = isXTurn ? 1 : 2;
cell.SetValue(newValue);
}
}
}
}
Key Methods of UICellBehaviour.cs
-
Awake()
: LoadsX
,O
, and blank sprites. -
Start()
: Adds the click listener for the button. -
OnEnable()
/OnDisable()
: Subscribes/unsubscribes from cell state events. -
OnValueChanged()
: Updates the button's image based on the cell’s value. -
OnGameFinished()
: Changes the cell color after a win or loss. -
OnButtonClick()
: Updates the cell's value when clicked.
Assigning ScriptableObjects to the UICellBehaviour
-
Cell ScriptableObject Instances:
- Create 9 instances of
Cell
ScriptableObjects (namedCell 0
throughCell 8
). Assign unique IDs (0 to 8).
- Create 9 instances of
-
Attach the Script:
- Drag and drop the
UICellBehaviour.cs
script onto theCell Button
in the Inspector for the first button. - Assign the serialized fields in
UICellBehaviour
:- Button: The button component itself.
-
Image: The image component of the
Cell Button
. - Colors: Assign the desired colors for default, win, and failed states.
- Cell: Leave empty for now.
- Drag and drop the
-
Prefab Overrides:
- Go to the Inspector, click on Override -> Apply All to apply these settings to the prefab. The rest of the prefabs will inherit this configuration automatically.
-
Assign ScriptableObjects:
- For each
UICellBehaviour
, assign the correspondingCell
ScriptableObject:- Assign
Cell 0
toCell Button 0
,Cell 1
toCell Button 1
, and so on untilCell 8
.
- Assign
- For each
Managing Turns with TurnManager
The TurnManager script is responsible for alternating between players (X and O) during the game. It ensures that players take turns one after another by switching a boolean variable. This script uses the PersistentMonoSingleton
to ensure that only one instance of the TurnManager
exists and persists across scenes, if needed.
Key Responsibilities of TurnManager.cs
:
- Handle Player Turns: Alternates the turn between player X and player O.
using com.marufhow.tictactoe.script.utility;
using UnityEngine;
namespace com.marufhow.tictactoe.script
{
public class TurnManager : PersistentMonoSingleton<TurnManager>
{
private bool xUserTurn;
private void Start()
{
xUserTurn = true; // X player starts first
}
public bool GetTurn()
{
bool turn = xUserTurn;
xUserTurn = !xUserTurn; // Switch turns between X and O
return turn;
}
protected override void Initialize()
{
// Any additional initialization can be placed here
}
}
}
Key Components of TurnManager.cs
:
xUserTurn: A boolean variable that tracks whose turn it is. If
true
, it's X’s turn; iffalse
, it's O’s turn.GetTurn(): This method returns the current player’s turn (X or O) and then toggles the turn for the next player.
How to Implement:
- Create an empty GameObject in the Scene and name it
TurnManager
. - Assign the
TurnManager.cs
script to this GameObject. Since the class inherits fromPersistentMonoSingleton
, it will automatically become a globally accessible singleton, and only one instance of it will exist throughout the game.
Managing the Tic Tac Toe Game Logic with BoardManager
The BoardManager script handles the overall game logic, including win conditions and resetting the game. It interacts with the Cell
ScriptableObjects to manage the game state.
Key Responsibilities of BoardManager.cs
:
-
Track Cell States: Manages the state of all cells using a list of
Cell
ScriptableObjects. - Win Condition Logic: Defines the possible winning combinations (rows, columns, diagonals) and checks if a player has achieved a win.
- Game Events: Triggers events when the game ends (either win or draw) and when the board is reset.
using System;
using System.Collections.Generic;
using com.marufhow.tictactoe.script.utility;
using UnityEngine;
namespace com.marufhow.tictactoe.script
{
public class BoardManager : PersistentMonoSingleton<BoardManager>
{
public event Action<int, bool> OnGameFinished;
public event Action OnReset;
[SerializeField] private List<Cell> cellsList = new();
private readonly int[][] winConditions =
{
new[] { 0, 1, 2 }, // Row 1
new[] { 3, 4, 5 }, // Row 2
new[] { 6, 7, 8 }, // Row 3
new[] { 0, 3, 6 }, // Column 1
new[] { 1, 4, 7 }, // Column 2
new[] { 2, 5, 8 }, // Column 3
new[] { 0, 4, 8 }, // Diagonal 1
new[] { 2, 4, 6 } // Diagonal 2
};
private void Start()
{
ResetGame(); // game start point
}
private void OnEnable()
{
foreach (var cell in cellsList) cell.OnValueChanged += CheckWinCondition;
}
private void OnDisable()
{
foreach (var cell in cellsList) cell.OnValueChanged -= CheckWinCondition;
}
private void CheckWinCondition(int cellId, int value)
{
foreach (var condition in winConditions)
if (cellsList[condition[0]].Value == value &&
cellsList[condition[1]].Value == value &&
cellsList[condition[2]].Value == value &&
value != 0)
{
cellsList[condition[0]].SetResult(true);
cellsList[condition[1]].SetResult(true);
cellsList[condition[2]].SetResult(true);
for (var i = 0; i < cellsList.Count; i++)
if (i != condition[0] && i != condition[1] && i != condition[2])
cellsList[i].SetResult(false);
OnGameFinished?.Invoke(value, true);
return;
}
var allCellFilled = true;
foreach (var cell in cellsList)
if (cell.Value == 0)
{
allCellFilled = false;
break;
}
if (allCellFilled)
{
foreach (var cell in cellsList) cell.SetResult(false);
OnGameFinished?.Invoke(0, false); // Tie
}
}
public void ResetGame()
{
foreach (var cell in cellsList) cell.Reset();
OnReset?.Invoke();
}
protected override void Initialize()
{
}
}
}
Key Methods of BoardManager.cs
-
Start()
: Initializes the game by callingResetGame()
to set all cells to their default state. -
OnEnable()
/OnDisable()
: Subscribes/unsubscribes from theCell
'sOnValueChanged
event to monitor cell changes. -
CheckWinCondition()
: Checks for a winning combination or a tie and triggers the game finished event accordingly. -
ResetGame()
: Resets all cells and triggers the reset event to restart the game.
How to Implement:
Create an empty GameObject in the Scene and name it BoardManager.
Assign the BoardManager.cs script to this GameObject. Since the class inherits from PersistentMonoSingleton, it will automatically become a globally accessible singleton, and only one instance of it will exist throughout the game.
Managing the User Interface with UIBehaviour
The UIBehaviour script handles the user interface, updates the game status, manages the restart button, and displays game results (win or draw).
Key Responsibilities of UIBehaviour.cs
:
- Display Game Status: Updates the UI to show whether the game is ongoing, won, or ended in a draw.
- Restart Game: Manages the restart button, allowing the player to reset the game.
- Respond to Game Events: Listens for events from the BoardManager to update the UI when the game finishes or resets.
using com.marufhow.tictactoe.script.utility;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace com.marufhow.tictactoe.script
{
public class UIBehaviour : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI gameStatusText;
[SerializeField] private Button restartButton;
[SerializeField] private GameObject containerPanel;
private void OnEnable()
{
BoardManager.Instance.OnGameFinished += HandleGameFinished;
BoardManager.Instance.OnReset += ResetUI;
}
private void OnDisable()
{
BoardManager.Instance.OnGameFinished -= HandleGameFinished;
BoardManager.Instance.OnReset -= ResetUI;
}
private void Start()
{
restartButton.onClick.AddListener(RestartGame);
}
private void HandleGameFinished(int value, bool isWin)
{
if (isWin)
{
string winner = value == 1 ? "X" : "O";
gameStatusText.text = $"{winner} Player Wins!";
}
else
{
gameStatusText.text = "It's a Draw! Try Again.";
}
containerPanel.SetActive(true);
}
private void ResetUI()
{
gameStatusText.text = "";
containerPanel.SetActive(false);
}
private void RestartGame()
{
BoardManager.Instance.ResetGame();
}
}
}
Key Methods of UIBehaviour.cs
OnEnable()
/OnDisable()
: Subscribes/unsubscribes from theBoardManager
'sOnGameFinished
andOnReset
events to update the UI when the game ends or resets.Start()
: Adds a listener to the restart button to reset the game when clicked.HandleGameFinished()
: Updates the game status text and shows the result panel when the game ends.ResetUI()
: Clears the game status and hides the result panel to prepare for a new round.
5. RestartGame()
: Calls BoardManager.ResetGame()
to restart the game.
Game UI Setup
-
Create a Panel:
- Under Canvas, create a new panel named
UI Behaviour
. Set the Rect Transform to Stretch for both pivot and position.
- Under Canvas, create a new panel named
-
Attach the UIBehaviour Script:
- Attach the
UIBehaviour.cs
script to theUI Behaviour
panel.
- Attach the
-
Create Child Components:
- Under
UI Behaviour
, create a child panel namedContainer
. Set its Rect Transform to Stretch. - Inside the
Container
panel, create a TextMeshProUGUI and a Button for displaying the game status and restarting the game.
- Under
-
Assign Serialized Fields:
- In UIBehaviour.cs, assign the following fields in the Inspector:
-
Container: Assign the
Container
panel. -
Status Text: Assign the
TextMeshProUGUI
text. - Restart Button: Assign the Button.
-
Container: Assign the
- In UIBehaviour.cs, assign the following fields in the Inspector:
This setup will allow you to display the game status and reset the game when needed.
Congratulations! You've successfully created a fully functional Tic Tac Toe game in Unity. You’ve learned how to design the game’s UI, manage cell interactions using ScriptableObjects, implement win conditions with the BoardManager, and update the UI to show the game status.
With this foundation, you can further enhance your game by adding features like sound effects, animations, or even AI for single-player mode. This project demonstrates how to combine game logic and UI elements in Unity, and you’re now ready to apply these skills to more complex games.
You can check out this GitHub repository here.
You can also play the game here.
Top comments (0)