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
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:
- Click the “Insert” menu
- Select “Class”
- Name the class “TicTacToeGame”
- 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
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
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
Public Property boardState(2,2) As Integer
- 2D array representing game board
- Values:
- 0 = Empty cell
- 1 = Player X
- 2 = Player O
Public Property currentPlayer As Integer = 1
- Tracks current turn
- 1 = Player X
- 2 = Player O
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
CellHeight
,CellWidth As Integer
- Computed properties
- Dynamically calculate cell dimensions based on canvas size
- Divide width/height by 3 for equal grid cells
ColorBoard
,ColorX
,ColorO
As Color
- Store color schemes for board elements
- Support dark/light mode themes
Animation Properties
Public Property animationProgress As Double
- Tracks symbol drawing animation
- Ranges from 0 to 1
- Controls symbol scaling during placement
Public Property animationTimer As Timer
- Manages animation timing
- Triggers smooth symbol rendering
Interaction Tracking
Public Property HoverCol As Integer = -1
andPublic Property HoverCol As Integer = -1
- Track mouse position over grid
- Enable hover effect on empty cells
- Provide visual feedback during gameplay
Scoring Properties
scoreX
andscoreO
As 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:
info
: A string describing the current game statusplayerTurn
: Optional parameter indicating which player’s turn it isscoreX
: Current score for Player XscoreO
: 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:
result
: A string describing the game’s final outcome (e.g., “X Wins”, “O Wins”, “Draw”)scoreX
: Final score for Player XscoreO
: 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:
- Drag and Drop: Drag the
TicTacToeGame
icon (it should look like a small canvas) from the Navigator (the left side of the IDE) onto yourWindow1
or any other window you want to use.- A
TicTacToeGame
instance will appear on the window. You can resize and position it as needed.
- A
- 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 theWindow1.Open
event:// Assuming 'TicTacToeGame1' is the name of your control instance on the window
TicTacToeGame1.NewGame
This will call theNewGame
method of your custom control, setting up the board and starting a new game. - 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!