Skip to content

Create a Tic-Tac-Toe Game in Xojo – Step-by-Step Tutorial

Tic-tac-toe, a classic two-player strategy game where players take turns marking spaces in a 3×3 grid. The objective is simple: be the first to get three of your symbols (X or O) in a row, either horizontally, vertically, or diagonally.

By the end of this tutorial, you will learn how to:

  • Create a desktop game application in Xojo
  • Use DesktopCanvas for game board rendering
  • Implement game logic and state management
  • Handle user interactions
  • Add visual effects and animations
Modern TicTacToe game in Xojo

Setting Up the Project

Launch Xojo and Create a New Project

  • Open Xojo IDE
  • Select “Desktop” project type
  • Set an Application Name (e.g., “TicTacToe”)
  • Click “Create”

Creating the TicTacToeGame Custom Control

To have a clear and modular code structure, the game logic and user interface will be implemented in a subclassed DesktopCanvas control.

Here are several key advantages for this approach:

  • Encapsulation: It neatly bundles all the game’s logic (like checking for wins or handling player turns) and visual elements (drawing the board, animating moves) into a single, self-contained unit. This makes your code cleaner, easier to understand, and simpler to maintain. You can reuse this control in other projects without rewriting everything.
  • Organization: A custom control promotes better code organization by separating the game’s functionality from the rest of your application’s code. This reduces complexity, especially if your application grows larger and includes other features.
  • Reusability: Once you’ve created the TicTacToeGame control, you can easily reuse it in other Xojo projects. Just drag and drop it onto a window!
  • Abstraction: The custom control provides an abstraction layer. The rest of your application doesn’t need to know the internal workings of the TicTacToe game; it only needs to interact with the control’s interface (like starting a new game or getting the current score). This makes it easier to modify or update the game logic without affecting other parts of your application.

Now, here are the steps to create this custom control based on DesktopCanvas:

  1. Click the “Insert” menu
  2. Select “Class”
  3. Name the class “TicTacToeGame”
  4. Set the “Super” to “DesktopCanvas”

TicTacToeGame Class Structure

First, we start by defining some important constants and properties that will be used by the game class.

Constants

  1. Private Const kBoardSize as Number = 3
    • Defines the grid dimensions (3×3)
    • Used for iterating through board cells
    • Provides flexibility for potential future grid size changes
  2. Private Const kCellsPadding as Number = 40
    • Controls spacing around X and O symbols
    • Ensures symbols don’t touch cell borders
    • Provides visual breathing room in cell drawings

Properties

Game State Properties

  1. Public Property boardState(2,2) As Integer
    • 2D array representing game board
    • Values:
      • 0 = Empty cell
      • 1 = Player X
      • 2 = Player O
  2. Public Property currentPlayer As Integer = 1
    • Tracks current turn
    • 1 = Player X
    • 2 = Player O
  3. Public Property isGameOver As Boolean = False
    • Indicates game completion status
    • Prevents further moves after game ends

Rendering Properties

Public Property CellHeight As Integer
Get
  Return Height / 3
End Get

Set
  
End Set
End Property

Public Property CellWidth As Integer
Get
  Return Width / 3
End Get

Set
  
End Set
End Property
  1. CellHeight, CellWidth As Integer
    • Computed properties
    • Dynamically calculate cell dimensions based on canvas size
    • Divide width/height by 3 for equal grid cells
  2. ColorBoard, ColorX, ColorOAs Color
    • Store color schemes for board elements
    • Support dark/light mode themes

Animation Properties

  1. Public Property animationProgress As Double
    • Tracks symbol drawing animation
    • Ranges from 0 to 1
    • Controls symbol scaling during placement
  2. Public Property animationTimer As Timer
    • Manages animation timing
    • Triggers smooth symbol rendering

Interaction Tracking

  1. Public Property HoverCol As Integer = -1and Public Property HoverCol As Integer = -1
    • Track mouse position over grid
    • Enable hover effect on empty cells
    • Provide visual feedback during gameplay

Scoring Properties

  1. scoreX and scoreOAs Integer
    • Track win counts for each player
    • Updated after each game

The constants and properties defined above will be crucial in the next steps, allowing us to implement the key features: encapsulation of game logic and rendering, flexible customization, responsive dynamic sizing and interactions, and an enhanced user experience through animations and hover effects.


Event Definitions

To make sure our game class is complete, we will create two custom event definitions that will be used later throughout the game.

GameStatus Event

Event GameStatus(info As String, playerTurn As String = "", scoreX As Integer, scoreO As Integer)
  • Purpose: Tracks and communicates the current state of the game
  • Parameters:
    1. info: A string describing the current game status
    2. playerTurn: Optional parameter indicating which player’s turn it is
    3. scoreX: Current score for Player X
    4. scoreO: Current score for Player O

GameOver Event

Event GameOver(result As String, scoreX As Integer, scoreO As Integer)
  • Purpose: Signals the conclusion of the game with a winner or draw
  • Parameters:
    1. result: A string describing the game’s final outcome (e.g., “X Wins”, “O Wins”, “Draw”)
    2. scoreX: Final score for Player X
    3. scoreO: Final score for Player O

Event Handlers in Tic-Tac-Toe Game

Closing Event

Sub Closing() Handles Closing
  // This event ensures that the animation timer is properly disabled and its handler is removed to prevent memory leaks or unexpected behavior.
  // Clean up the animation timer when the control is closing
  If animationTimer <> Nil Then
    animationTimer.Enabled = False
    RemoveHandler animationTimer.Action, AddressOf AnimationStep
    animationTimer = Nil
  End If
End Sub
  • Ensures clean resource management
  • Disables and removes timer to prevent memory leaks
  • Called when the control is being destroyed

MouseDown Event

Function MouseDown(x As Integer, y As Integer) Handles MouseDown as Boolean
  // This event handles mouse click actions on the game board.
  // It checks if the game is over; if not, it calculates the row and column of the click.
  // If the clicked cell is empty, it records the player's move, checks for a winner, and toggles the current player.
  
  // Handle mouse clicks on the game board
  If isGameOver Then
    Return True
  End If
  
  // Calculate the row and column based on the click position
  Var row As Integer = y \ CellHeight
  Var col As Integer = x \ CellWidth
  
  // If the clicked cell is empty, make a move
  If boardState(row, col) = 0 Then
    boardState(row, col) = currentPlayer
    StartAnimation(row, col)
    
    // Reset hover position after a move
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the affected cell
    Refresh(col * CellWidth, row * CellHeight, CellWidth, CellHeight)
    
    // Check for a winner or a draw
    Var winner As Integer = CheckWinner()
    If winner > 0 Then
      UpdateScore(winner)
      GameStatus("Player " + PlayerSymbol(winner) + " wins!", scoreX, scoreO)
      isGameOver = True
      GameOver("Player " + PlayerSymbol(winner) + " wins!", scoreX, scoreO)
      
      // Refresh the entire board to show the winning line
      Refresh(True)
    ElseIf Me.IsBoardFull() Then
      GameStatus("It's a draw!", scoreX, scoreO)
      isGameOver = True
      GameOver("Draw", scoreX, scoreO)
    Else
      // Switch to the other player
      currentPlayer = If(currentPlayer = 1, 2, 1)
      GameStatus("Player " + PlayerSymbol(currentPlayer) + "'s turn", PlayerSymbol(currentPlayer), scoreX, scoreO)
    End If
  End If
  
  Return True
End Function
  • Handles player moves
  • Validates move legality
  • Checks for win/draw conditions
  • Switches players

MouseExit and MouseMove Events

Sub MouseExit() Handles MouseExit
  // This event is triggered when the mouse cursor exits the game board area.
  // It clears the hover effect to avoid leaving any visual artifacts on the board when the mouse is moved away.
  
  If hoverRow >= 0 And hoverRow < kBoardSize And hoverCol >= 0 And hoverCol < kBoardSize Then
    Var oldHoverRow As Integer = hoverRow
    Var oldHoverCol As Integer = hoverCol
    
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the cell that was previously hovered
    Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
  End If
End Sub
Sub MouseMove(x As Integer, y As Integer) Handles MouseMove
  // This event handles mouse movement over the game board.
  // It updates the hover effect when the mouse moves to a new cell, providing visual feedback.
  
  If Not isGameOver Then
    Var newHoverRow As Integer = y \ CellHeight
    Var newHoverCol As Integer = x \ CellWidth
    
    If newHoverRow <> hoverRow Or newHoverCol <> hoverCol Then
      Var oldHoverRow As Integer = hoverRow
      Var oldHoverCol As Integer = hoverCol
      
      hoverRow = newHoverRow
      hoverCol = newHoverCol
      
      // Refresh the old hover cell (if it was valid)
      If oldHoverRow >= 0 And oldHoverRow < 3 And oldHoverCol >= 0 And oldHoverCol < 3 Then
        Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
      End If
      
      // Refresh the new hover cell
      Refresh(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Provides visual hover feedback
  • Tracks mouse movement across grid
  • Refreshes only changed cells

Opening Event

Sub Opening() Handles Opening
  // This event initializes the board colors based on the current system theme (dark mode or light mode) and starts a new game.
  
  // Set up colors of the board lines, for the X's and O's
  If Color.IsDarkMode = True Then
    ColorBoard = Color.RGB(178, 161, 149)
    ColorX = Color.RGB(228, 182, 88)
    ColorO = Color.RGB(253, 161, 97)
  Else
    ColorBoard = Color.RGB(130, 110, 92)
    ColorX = Color.RGB(228, 182, 88)
    ColorO = Color.RGB(253, 161, 97)
  End If
  
  NewGame()
End Sub
  • Initializes color theme
  • Supports dark and light modes
  • Starts a new game automatically

Paint Event

Sub Paint(g As Graphics, areas() As Rect) Handles Paint
  // This event is responsible for drawing the game board, hover effects, player symbols (X's and O's), and the winning line.
  // It is called whenever the game board needs to be redrawn.
  
  // Draw the board
  g.DrawingColor = ColorBoard
  g.DrawLine(Width/3, 0, Width/3, Height)
  g.DrawLine(2*Width/3, 0, 2*Width/3, Height)
  g.DrawLine(0, Height/3, Width, Height/3)
  g.DrawLine(0, 2*Height/3, Width, 2*Height/3)
  
  // Draw hover effect
  DrawHoverEffect(g)
  
  // Draw X's and O's
  g.PenSize = 8
  For row As Integer = 0 To 2
    For col As Integer = 0 To 2
      If boardState(row, col) = 1 Then
        DrawX(g, row, col)
      ElseIf boardState(row, col) = 2 Then
        DrawO(g, row, col)
      End If
    Next
  Next
  
  // Draw winning line if the game is over
  If isGameOver Then
    DrawWinningLine(g)
  End If
End Sub
  • Renders game board
  • Draws grid lines
  • Manages visual game state
  • Supports dynamic rendering

Animation and Drawing Methods

1. AnimationStep

Public Sub AnimationStep(sender As Timer)
  // This method is called by the animation timer (animationTimer property) to progress the animation of a newly placed symbol.
  // It increments the animation progress and stops the timer once the animation is complete.
  
  // Progress the animation
  animationProgress = animationProgress + 0.1
  If animationProgress >= 1 Then
    animationTimer.Enabled = False
    animationProgress = 1
  End If
  
  // Refresh only the cell being animated
  Refresh(lastPlayedCol * CellWidth, lastPlayedRow * CellHeight, CellWidth, CellHeight)
  
  // If the animation is complete, stop the timer
  If animationProgress >= 1 Then
    animationTimer.Enabled = False
  End If
End Sub
  • Manages symbol placement animation
  • Gradually scales symbol from 0 to 1
  • Updates only the recently played cell

2. DrawX and DrawO

Sub MouseExit() Handles MouseExit
  // This event is triggered when the mouse cursor exits the game board area.
  // It clears the hover effect to avoid leaving any visual artifacts on the board when the mouse is moved away.
  
  If hoverRow >= 0 And hoverRow < kBoardSize And hoverCol >= 0 And hoverCol < kBoardSize Then
    Var oldHoverRow As Integer = hoverRow
    Var oldHoverCol As Integer = hoverCol
    
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the cell that was previously hovered
    Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
  End If
End Sub
Sub MouseMove(x As Integer, y As Integer) Handles MouseMove
  // This event handles mouse movement over the game board.
  // It updates the hover effect when the mouse moves to a new cell, providing visual feedback.
  
  If Not isGameOver Then
    Var newHoverRow As Integer = y \ CellHeight
    Var newHoverCol As Integer = x \ CellWidth
    
    If newHoverRow <> hoverRow Or newHoverCol <> hoverCol Then
      Var oldHoverRow As Integer = hoverRow
      Var oldHoverCol As Integer = hoverCol
      
      hoverRow = newHoverRow
      hoverCol = newHoverCol
      
      // Refresh the old hover cell (if it was valid)
      If oldHoverRow >= 0 And oldHoverRow < 3 And oldHoverCol >= 0 And oldHoverCol < 3 Then
        Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
      End If
      
      // Refresh the new hover cell
      Refresh(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Draws X and O symbols with animation
  • Centers symbol in cell
  • Scales symbol based on animation progress

3. DrawHoverEffect

Public Sub DrawHoverEffect(g As Graphics)
  // This method draws a semi-transparent hover effect over the cell that the mouse is currently hovering over.
  // It only draws the effect if the game is not over and the hovered cell is empty.
  
  If Not isGameOver And hoverRow >= 0 And hoverRow < 3 And hoverCol >= 0 And hoverCol < 3 Then
    If boardState(hoverRow, hoverCol) = 0 Then
      g.DrawingColor = Color.RGB(255, 255, 255, 250) // Semi-transparent white
      g.FillRectangle(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Provides visual feedback on hoverable cells
  • Applies semi-transparent white overlay
  • Only affects empty, unplayed cells

4. DrawWinningLine

Public Sub DrawWinningLine(g As Graphics)
  // This method draws a semi-transparent green line over the winning combination on the board if a player has won.
  
  // Draw the winning line if there is a winner
  If winningLine.Count = 6 Then
    g.DrawingColor = Color.RGB(0, 255, 0, 128) // Semi-transparent green
    g.PenSize = 2
    
    Var startX As Integer = winningLine(1) * cellWidth + cellWidth / 2
    Var startY As Integer = winningLine(0) * cellHeight + cellHeight / 2
    Var endX As Integer = winningLine(5) * cellWidth + cellWidth / 2
    Var endY As Integer = winningLine(4) * cellHeight + cellHeight / 2
    
    g.DrawLine(startX, startY, endX, endY)
  End If
End Sub
  • Draws a semi-transparent green line
  • Highlights the winning combination

Game Logic Methods

1. CheckWinner

Public Function CheckWinner() As Integer
  // This method checks the board for a winner by evaluating rows, columns, and diagonals.
  // It returns the winning player (1 or 2) or 0 if there is no winner.
  
  
  // Check rows
  For i As Integer = 0 To kBoardSize - 1
    If boardState(i, 0) <> 0 And boardState(i, 0) = boardState(i, 1) And boardState(i, 1) = boardState(i, 2) Then
      winningLine = Array(i, 0, i, 1, i, 2)
      Return boardState(i, 0)
    End If
  Next
  
  // Check columns
  For j As Integer = 0 To 2
    If boardState(0, j) <> 0 And boardState(0, j) = boardState(1, j) And boardState(1, j) = boardState(2, j) Then
      winningLine = Array(0, j, 1, j, 2, j)
      Return boardState(0, j)
    End If
  Next
  
  // Check diagonals
  If boardState(0, 0) <> 0 And boardState(0, 0) = boardState(1, 1) And boardState(1, 1) = boardState(2, 2) Then
    winningLine = Array(0, 0, 1, 1, 2, 2)
    Return boardState(0, 0)
  End If
  
  If boardState(0, 2) <> 0 And boardState(0, 2) = boardState(1, 1) And boardState(1, 1) = boardState(2, 0) Then
    winningLine = Array(0, 2, 1, 1, 2, 0)
    Return boardState(0, 2)
  End If
  
  winningLine.ResizeTo(-1)
  Return 0 // No winner yet
End Function
  • Scans board for winning combinations
  • Returns winning player or 0
  • Stores winning line coordinates

2. IsBoardFull

Public Function IsBoardFull() As Boolean
  // This method checks if the board is completely filled with no empty cells.
  // It returns true if the board is full, otherwise false.
  
  // Check if the board is full (no empty cells)
  For i As Integer = 0 To kBoardSize - 1
    For j As Integer = 0 To kBoardSize - 1
      If boardState(i, j) = 0 Then
        Return False
      End If
    Next
  Next
  Return True
End Function
  • Checks if all cells are occupied
  • Determines if game is a draw

3. NewGame

Public Sub NewGame()
  // This method resets the game state to start a new game.
  // It clears the board, resets the current player to Player 1, and updates the game status.
  
  // Reset the game state for a new game
  For i As Integer = 0 To 8
    boardState(i \ 3, i Mod 3) = 0
  Next
  currentPlayer = 1
  isGameOver = False
  animationProgress = 1
  If animationTimer <> Nil Then
    animationTimer.Enabled = False
  End If
  GameStatus("Player " + PlayerSymbol(currentPlayer) + "'s turn", scoreX, scoreO)
  winningLine.ResizeTo(-1)
  Refresh()
End Sub
  • Resets game to initial state
  • Clears board
  • Resets player turn

4. UpdateScore

Public Sub UpdateScore(winner As Integer)
  // This method updates the score for the winning player by incrementing the respective score counter.
  
  // Update the score for the winning player
  If winner = 1 Then
    scoreX = scoreX + 1
  ElseIf winner = 2 Then
    scoreO = scoreO + 1
  End If
End Sub
  • Increments score for winning player

Utility Methods

1. PlayerSymbol

Public Function PlayerSymbol(player As Integer) As String
  // This method returns the symbol ('X' or 'O') corresponding to the player number (1 or 2).
  Return If(player = 1, "X", "O")
End Function
  • Converts player number to symbol

2. StartAnimation

Public Sub StartAnimation(row As Integer, col As Integer)
  // This method initializes and starts the animation for a newly placed symbol,
  // by setting the target cell and resetting the animation progress.
  
  // Start the animation for a newly placed symbol
  lastPlayedRow = row
  lastPlayedCol = col
  animationProgress = 0
  
  If animationTimer = Nil Then
    animationTimer = New Timer
    animationTimer.Period = 16 // equivalent of 60 FPS
    AddHandler animationTimer.Action, AddressOf AnimationStep
  End If
  
  animationTimer.Enabled = True
  animationTimer.RunMode = Timer.RunModes.Multiple
End Sub
  • Initializes symbol animation
  • Sets up timer for smooth rendering

Usage Instructions

To use the TicTacToeGame control in your Xojo project:

  1. Drag and Drop: Drag the TicTacToeGame icon (it should look like a small canvas) from the Navigator (the left side of the IDE) onto your Window1 or any other window you want to use.
    • A TicTacToeGame instance will appear on the window. You can resize and position it as needed.
  2. Initialize: While not strictly required, you might want to initialize the game in the Window1.Open event handler. This ensures the game is ready to play as soon as the window opens. You can do this by adding the following code to the Window1.Open event:
    // Assuming 'TicTacToeGame1' is the name of your control instance on the window
    TicTacToeGame1.NewGame
    This will call the NewGame method of your custom control, setting up the board and starting a new game.
  3. Run the Project: Run your Xojo project. You should see the TicTacToe board on your window, ready to play!

I highly suggest downloading the complete TicTacToe Xojo project for the game. If you find anything unclear, refer to this tutorial for explanations. Download the Xojo project.

Next Steps:

Congratulations! You’ve successfully built a fully functional, modern tic-tac-toe game in Xojo. This project is a great foundation for exploring more advanced game development concepts, animations and how to make custom UI elements based on Xojo’s powerful DesktopCanvas control.

Now that you understand the basics, here are some ideas to take your TicTacToe game to the next level:

  • Artificial Intelligence (AI): Implement a simple AI opponent so players can play against the computer. You could start with a random move generator and then explore more sophisticated algorithms like Minimax. Xojo already provides fully working AI integration examples.
  • Different Game Modes: Add options for different board sizes (e.g., 4×4, 5×5) or variations of tic-tac-toe.
  • Online Multiplayer: Enable players to challenge each other online using Xojo’s networking capabilities.
  • Enhanced UI/UX: Improve the user interface with custom graphics, sound effects, and a more polished look and feel. Consider adding a timer.
  • Go Multiplatform: Port the TicTacToeGame class to Mobile and Web projects.

We encourage you to experiment, get creative, and explore these possibilities. Share your creations and connect with other Xojo developers in the forums to learn and grow together. Happy coding!

Gabriel is a digital marketing enthusiast who loves coding with Xojo to create cool software tools for any platform. He is always eager to learn and share new ideas!