DEV Community

Cover image for Create a Tic Tac Toe Game in Unity
Md Maruf Howlader
Md Maruf Howlader

Posted on • Edited on

Create a Tic Tac Toe Game in Unity

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.

  1. Folder Structure:
    • Under the Assets folder, create a folder named TicTacToe. Inside this folder, create:
      • Scripts
      • Prefabs
      • Resources
      • Scenes
    • Create a new Scene and save it under the Scenes folder.

Image description

  1. Resources Folder:

    • Inside the Resources folder, add 3 square images: x.png (X icon), o.png (O icon), and b.png (empty image with a white background). Download the images from here and drag them into the Resources folder.
  2. 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, and UIBehaviour.cs. We’ll explain why we need each of these scripts later.
  • 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.

  1. 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.
  2. 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

Image description

  1. Vertical Layout Group:
    • Inside the Grid, create another Panel. Rename it Vertical 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)

Image description

  1. Row Panel:
    • Inside the Vertical Layout Group, create another Panel and name it Row.
    • Add a Horizontal Layout Group component and enable:
      • Spaceing 10
      • Control Child Size (Width and Height)
      • Child Force Expand (Width and Height)

Image description

  1. Cell Button:
    • Inside the Row panel, right-click and add a UI -> Image. Rename it Cell Button.
    • From Inspector window, click Add-Component, add a Button component to the Cell Button.

Image description


Step 3: Prefab Creation and Replication

  1. Create a Prefab:

    • Drag the Cell Button from the Hierarchy into your project’s Prefab folder to create a prefab.
  2. Duplicate Buttons:

    • In the Row panel, duplicate the Cell Button twice.

Image description

  • Duplicate the Row panel twice to create three rows. You now have a 3x3 grid with 9 buttons.

Image description

  1. Naming Cells:
    • Rename each button: Cell Button 0, Cell Button 1, ..., Cell Button 8.

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. SetValue(int newValue): Updates the cell's value (X, O, or blank) and triggers the OnValueChanged event to notify listeners.

  2. SetResult(bool isWin): Marks the cell as part of a winning combination or not, disables interactivity, and triggers the OnGameFinished event.

  3. 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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Methods of UICellBehaviour.cs

  1. Awake(): Loads X, O, and blank sprites.
  2. Start(): Adds the click listener for the button.
  3. OnEnable() / OnDisable(): Subscribes/unsubscribes from cell state events.
  4. OnValueChanged(): Updates the button's image based on the cell’s value.
  5. OnGameFinished(): Changes the cell color after a win or loss.
  6. OnButtonClick(): Updates the cell's value when clicked.

Assigning ScriptableObjects to the UICellBehaviour

  1. Cell ScriptableObject Instances:
    • Create 9 instances of Cell ScriptableObjects (named Cell 0 through Cell 8). Assign unique IDs (0 to 8).

Image description

  1. Attach the Script:

    • Drag and drop the UICellBehaviour.cs script onto the Cell 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.
  2. 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.

Image description

  1. Assign ScriptableObjects:
    • For each UICellBehaviour, assign the corresponding Cell ScriptableObject:
      • Assign Cell 0 to Cell Button 0, Cell 1 to Cell Button 1, and so on until Cell 8.

Image description


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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Components of TurnManager.cs:

  • xUserTurn: A boolean variable that tracks whose turn it is. If true, it's X’s turn; if false, 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 from PersistentMonoSingleton, 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:

  1. Track Cell States: Manages the state of all cells using a list of Cell ScriptableObjects.
  2. Win Condition Logic: Defines the possible winning combinations (rows, columns, diagonals) and checks if a player has achieved a win.
  3. 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()
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Methods of BoardManager.cs

  1. Start(): Initializes the game by calling ResetGame() to set all cells to their default state.
  2. OnEnable() / OnDisable(): Subscribes/unsubscribes from the Cell's OnValueChanged event to monitor cell changes.
  3. CheckWinCondition(): Checks for a winning combination or a tie and triggers the game finished event accordingly.
  4. 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:

  1. Display Game Status: Updates the UI to show whether the game is ongoing, won, or ended in a draw.
  2. Restart Game: Manages the restart button, allowing the player to reset the game.
  3. 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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Methods of UIBehaviour.cs

  1. OnEnable() / OnDisable(): Subscribes/unsubscribes from the BoardManager's OnGameFinished and OnReset events to update the UI when the game ends or resets.

  2. Start(): Adds a listener to the restart button to reset the game when clicked.

  3. HandleGameFinished(): Updates the game status text and shows the result panel when the game ends.

  4. 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

  1. Create a Panel:

    • Under Canvas, create a new panel named UI Behaviour. Set the Rect Transform to Stretch for both pivot and position.
  2. Attach the UIBehaviour Script:

    • Attach the UIBehaviour.cs script to the UI Behaviour panel.
  3. Create Child Components:

    • Under UI Behaviour, create a child panel named Container. 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.
  4. 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.

This setup will allow you to display the game status and reset the game when needed.

Image description


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)