SDL Tutorial - Tic Tac Toe
SDL Tutorials November 3rd, 2007Up to this point we have been laying the foundation for developing a game. So far we’ve setup a basic structure to handle common routines, we’ve setup a special class to handle events, and we’ve also setup a class to handle a few surface functions. In this tutorial we’ll take all those things, combine them, and create a tic-tac-toe game. Don’t worry, things should be pretty simple. Use the last tutorial to build off of.
The first thing we are going to need to do is plan our game. From experience, we know that tic-tac-toe has a 3×3 grid, where you place X’s and O’s. So, we know that we will need 3 graphics, one for the grid, one for the X, and one for the O. We don’t need multiples of the X or O, because we can draw them in the program as many times as we like. Lets eliminate this first step. Our grid is going to be 600×600, and our X’s and O’s will be 200×200 (1/3 of the area).



Now that we have our images, we are going to need a way to load them into our program. Open up CApp.h and make some modifications. Remove the Test Surface, and add three new surfaces.
#define _CAPP_H_
#include <SDL.h>
#include "CEvent.h"
#include "CSurface.h"
class CApp : public CEvent {
private:
bool Running;
SDL_Surface* Surf_Display;
private:
SDL_Surface* Surf_Grid;
SDL_Surface* Surf_X;
SDL_Surface* Surf_O;
public:
CApp();
int OnExecute();
public:
bool OnInit();
void OnEvent(SDL_Event* Event);
void OnExit();
void OnLoop();
void OnRender();
void OnCleanup();
};
#endif
Also, open up CApp.cpp and make some modifications. Remove the Test Surface again, and add the three new ones again.
CApp::CApp() {
Surf_Grid = NULL;
Surf_X = NULL;
Surf_O = NULL;
Surf_Display = NULL;
Running = true;
}
int CApp::OnExecute() {
if(OnInit() == false) {
return -1;
}
SDL_Event Event;
while(Running) {
while(SDL_PollEvent(&Event)) {
OnEvent(&Event);
}
OnLoop();
OnRender();
}
OnCleanup();
return 0;
}
int main(int argc, char* argv[]) {
CApp theApp;
return theApp.OnExecute();
}
And, you guessed it, open up CApp_OnCleanup.cpp to make some modifications there too. Just like before, get rid of the Test Surface and add the three new ones.
void CApp::OnCleanup() {
SDL_FreeSurface(Surf_Grid);
SDL_FreeSurface(Surf_X);
SDL_FreeSurface(Surf_O);
SDL_FreeSurface(Surf_Display);
SDL_Quit();
}
Now that we have the surfaces setup, lets load them into memory. Open up CApp_OnInit.cpp, and make some changes. Get rid of the test surface (again), and load the three new ones. Be sure to put the correct filenames. Also, change the dimensions of the window to 600×600, the size of the grid. This make sure we don’t have any blank space around the window we aren’t using.
bool CApp::OnInit() {
if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
return false;
}
if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
return false;
}
if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
return false;
}
if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
return false;
}
if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
return false;
}
return true;
}
You may have noticed a change I made in the filenames. I added ./gfx/ before the filename to specify which folder the graphics are in. As games begin to grow it’s very practical to have all the files in one folder. Because of such, from hereon, all images will be put in the gfx folder.
Now, let’s get the grid showing up on the screen. Open up CApp_OnRender.cpp and change the test surface rendering to make the grid render.
void CApp::OnRender() {
CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0);
SDL_Flip(Surf_Display);
}
Try compiling your program, and if successful, you should see the grid show up. Remember, there are basically 5 steps to using a surface: declare it, set it to NULL, load it, draw it, and then free it. It’s good practice to learn all these 5 steps now, because later on if you neglect one of these steps if can cause problems. For example, neglecting to set a surface to NULL can cause undefined behavior, or neglecting to free a surface can cause a memory leak.
You may have noticed something odd about the graphics we are using, the X and O contain a pink background. There is a reason for it, we are going to implement transparency onto these surfaces. Basically, wherever there is pink, it will show through; we will make the pink color transparent. SDL offers a simple function to do this, SDL_SetColorKey. To implement this, open up CSurface.h so we can add a new function.
#define _CSURFACE_H_
#include <SDL.h>
class CSurface {
public:
CSurface();
public:
static SDL_Surface* OnLoad(char* File);
static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
static bool Transparent(SDL_Surface* Surf_Dest, int R, int G, int B);
};
#endif
Now, to implement this function, open up CSurface.cpp and add the function:
if(Surf_Dest == NULL) {
return false;
}
SDL_SetColorKey(Surf_Dest, SDL_SRCCOLORKEY | SDL_RLEACCEL, SDL_MapRGB(Surf_Dest->format, R, G, B));
return true;
}
Notice 3 extra arguments being passed besides the surface. These are the 3 color values that we want to make transparent, it doesn’t have to be just pink. For instance, it we wanted red to be transparent it would be 255, 0, 0.
This function first checks to see if we have a valid surface. If so, we set a color key (transparency) for a color. The first argument is the surface to apply the color key to, the second is some flags telling SDL how to perform the operation, and the third is the color to make transparent. The flags being applied are basic, the first tells SDL to apply the color key to the source (the surface being passed) and the second tells SDL to try to use RLE acceleration (basically, try to make drawing later on faster). The third argument is a little bit more complex; we are using SDL_MapRGB in order to create a color. SDL_MapRGB takes a surface, and your requested color (R, G, B), and tries to match it as close as it can to that surface. You might be thinking why this is useful. Not all surfaces have the same color palette. Remember the old NES days where there was only a few colors that could be used? Same idea here, SDL_MapRGB takes a color and matches it with the closest color on that surface palette.
Lets apply this new function to our surfaces now, open up CApp_OnInit.cpp and make the following changes:
bool CApp::OnInit() {
if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
return false;
}
if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
return false;
}
if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
return false;
}
if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
return false;
}
if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
return false;
}
CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);
return true;
}
Everything for the surfaces should be set. The next thing we have to do is figure out a way to draw these X’s and O’s. We can’t just draw them everywhere on the grid because they won’t always be in the same spots. What we are going to have to do is make an array of 9 containers, the values in this array will tell us the values for each cell on the grid. So, spot 0 would be the top left, 1 would be the top middle, 2 the top right, 3 the middle left, and so on. Create this array by adding it to CApp.h:
#define _CAPP_H_
#include <SDL.h>
#include "CEvent.h"
#include "CSurface.h"
class CApp : public CEvent {
private:
bool Running;
SDL_Surface* Surf_Display;
private:
SDL_Surface* Surf_Grid;
SDL_Surface* Surf_X;
SDL_Surface* Surf_O;
private:
int Grid[9];
public:
CApp();
int OnExecute();
public:
bool OnInit();
void OnEvent(SDL_Event* Event);
void OnExit();
void OnLoop();
void OnRender();
void OnCleanup();
};
#endif
We know that each cell can have three possible values: Empty, X, and O. These tell us what is currently in that cell. To make things a little neater than having 0, 1, 2 as values, we’ll use an enum instead. If you are umfamiliar with how an enum works, try finding a quick tutorial on them. Just know that GRID_TYPE_NONE = 0, GRID_TYPE_X = 1, and GRID_TYPE_O = 2. Go back to CApp.h and add the following underneath the Grid array:
GRID_TYPE_NONE = 0,
GRID_TYPE_X,
GRID_TYPE_O
};
Note, up to this point I have been displaying practically all the code when I am referring to different files. From hereon, I expect you to know where code goes. Most of the time I will tell you where to place it by, and sometimes I might display all the code.
Now that we have a way to know what a cell is, we’re going to need a way to reset the board. Let’s create a new function at the bottom inside of CApp.h called Reset.
void Reset();
Open up CApp.cpp and add the following function just before int main:
for(int i = 0;i < 9;i++) {
Grid[i] = GRID_TYPE_NONE;
}
}
This loop will set every cell in the grid to GRID_TYPE_NONE, meaning that all the cells are empty. We’re going to have to do this at the very beginning when our program is loaded, so make a call to this function within the CApp_OnInit.cpp file:
CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);
Reset();
So far so good. The next thing we are going to have to do is make the ability to place X’s and O’s on the screen. Lets create a new function that will handle this. Open up CApp.h again and add the function just below Reset.
Now, open back up CApp.cpp and add the function:
if(ID < 0 || ID >= 9) return;
if(Type < 0 || Type > GRID_TYPE_O) return;
Grid[ID] = Type;
}
This function takes two arguments, the first is the cell ID to change, and the second is the type to change it to. We have to conditions here, the first is to make sure we don’t go outside the bounds of the array (if we did it would likely crash our program), and the second is to make sure we passed an appropriate type. Simple enough.
Now, let’s implement a way to draw the X’s and O’. Open up CApp_OnRender and add the following code just after the grid:
int X = (i % 3) * 200;
int Y = (i / 3) * 200;
if(Grid[i] == GRID_TYPE_X) {
CSurface::OnDraw(Surf_Display, Surf_X, X, Y);
}else
if(Grid[i] == GRID_TYPE_O) {
CSurface::OnDraw(Surf_Display, Surf_O, X, Y);
}
}
This is a little bit more complex than we have been used to so far. Firstly, we are looping through each cell in the grid. Next, we are translating that grid ID over to X and Y coordinates. We do this in two different ways. To find X, we take the remainder of i to 3. This will give us 0 when i is 0, 1 when i is 1, 2 when i is 2, 0 when i is 3, and so on. We multiply it by 200 because each cell is 200×200 pixels. To find Y, we divide by 3, this causes Y to be 0 when i is 0, 1, 2, Y to be 1 when i is 3, 4, 5, and so on. We then multiply that by 200 as well. I encourage you to really try to understand what is going on here because this sort of method is used for tile based games.
Next, all we have to do is check the cell type, and then draw the correct surface at that cell.
Now that we have our surfaces drawing, we’ll need a way to communicate from the user to the computer. We’ll use mouse events for this. When the users clicks a cell it will set the cell appropriately. We are going to need to overload one of the CEvent functions for this. Open up CApp.h and add the following function just below OnEvent and next to OnExit.
Now, open up CApp_OnEvent.cpp and add the function:
int ID = mX / 200;
ID = ID + ((mY / 200) * 3);
if(Grid[ID] != GRID_TYPE_NONE) {
return;
}
if(CurrentPlayer == 0) {
SetCell(ID, GRID_TYPE_X);
CurrentPlayer = 1;
}else{
SetCell(ID, GRID_TYPE_O);
CurrentPlayer = 0;
}
}
First, we are doing the reverse of what we did with translating to X and Y from an ID, this time we are translating to an ID. We then make sure that that cell hasn’t already been taken, if it has, we return out of the function. Next, we are checking which players turn it is, set the cell appropriately, and then switch turns. CurrentPlayer is a new variable to specify whose turn it is, so we’ll need to add this. Open up CApp.h and add the variable below the grid array:
Also, set the default value for this variable in CApp.cpp:
CurrentPlayer = 0;
Surf_Grid = NULL;
Surf_X = NULL;
Surf_O = NULL;
Surf_Display = NULL;
Running = true;
}
Try compiling the program and you should have a mostly working version of tic-tac-toe. Congratulations!
This is where you take the rest of it yourself. We have a solid foundation for our game, and most the work done. I encourage you to take this a step further. Try adding a “X Won”, “O Won”, and “Draw” at the end of each game (extra images here). Think, how are you going to check who won (a function for this purpose would fit well)? Try adding a way to reset the game after it’s done. If you are brave, try to add some generic AI that will play against the user. And if you are even braver, try adding the ability to play player vs. player, or player vs. computer.
When you are ready, and have a firm grasp of this tutorial, move on to the next lesson to look at frame animation.
SDL Tutorial - Tic Tac Toe - Tutorial Files:
Win32: Zip, Rar
Linux: Tar (Thanks Gaten)














November 15th, 2007 at 7:19 am
Very nice tutorial. I like your style - it’s really easy to follow along.
Keep up the good work!
January 11th, 2008 at 6:34 pm
Great work man! Looking forward to more!
February 23rd, 2008 at 5:33 am
Awesome work man! Really helpful! Keep it up
February 28th, 2008 at 12:04 am
Linux source: http://gaten.homelinux.net/sdltuts/tic_tac_toe.tar.gz
March 28th, 2008 at 4:07 am
gaten down?
March 28th, 2008 at 5:52 pm
Sorry about that, it’s all fixed now. Thanks for letting me know.
April 5th, 2008 at 7:24 pm
Excellent tutorial. These are extremely helpful, keep it up!
May 6th, 2008 at 7:07 pm
I’m running into a problem with the transparencies for this tutorial.
After searching online, it seems that i need to make the call to SDL_SetColorKey(…) (in our transparent function) before the call to SDL_DisplayFormatAlpha(..) (in the OnLoad function).
Noting this, would it be best to make another OnLoad function that takes a transparent value and calls SDL_SetColorKey(…) just before the call to SDL_DisplayFormatAlpha?
also, i’m curious if anyone knows why i need to do this compared to everyone else.
May 7th, 2008 at 7:30 pm
The reason you need to call SDL_SetColorKey before SDL_DisplayFormatAlpha, is because the SDL_SetColorKey will make an alpha (transparency layer) on your surface. Otherwise, you would use SDL_DisplayFormat for surfaces without an alpha layer. With that in mind, I recommend dumping SDL_SetColorKey all together, as it is not necessary if you use PNG files. Take a look at my SDL_image side tutorial, and by making alpha layers in a paint program in the PNG directly, you won’t have to mess with it using SDL_SetColorKey. Also, by using PNGs you can have the alpha layer be any value you want (such as 50% transparent). Which is a good way to make lights. I hope that helps.
May 8th, 2008 at 2:20 pm
Thank you Tim. That makes sense. In fact i think that’s why i was experiencing my original problem.
I had done your SDL_Image tutorial before i attempted this tic-tac-toe . So when i did this tutorial i used png files and the SDL_DisplayFormatAlpha, and didn’t notice that you were still using BMP files and the SDL_DiaplyFormat call.
So the way i understand it now is that you can call our transparency function on surfaces AFTER the SDL_DisplayFormat call (if used), but when using SDL_DisplayFormatAlpha we must make the SDL_SetColorKey call BEFORE.
But, as you are saying, just using an image that already has the alpha channel set and calling SDL_DisplayFormatAlpha is our best method.
May 16th, 2008 at 10:54 am
Yep! It’s best to just do away with the SDL_SetColorKey, and just use PNGs, you’ll save yourself some extra coding.
June 2nd, 2008 at 5:56 am
Hi!
Could someone post correct function witch would check if anyone won ?
I try do it by simple if conditions but it doesn’t work (game exit when first move is made, witch should happen only when someone wins).
June 2nd, 2008 at 2:01 pm
I’ll try writing one now.
June 2nd, 2008 at 2:29 pm
Here’s my attempt (which works):
To the class definition of CApp, add the following attributes:
private int WinsX;
private int WinsY;
and the following methods:
public void SetWinner();
public void CongratulateWinner(int);
In CApp::CApp(), set WinsX and WinsY to 0.
In CApp::OnLoop(), call the function SetWinner().
Create the function CApp::SetWinner() under CApp::SetCell(). Here is the code for it:
void CApp::SetWinner() {
int Winner = 0;
if (Grid[0] == Grid[1] && Grid[1] == Grid[2] && Grid[2] != GRID_TYPE_NONE) {
Winner = Grid[0];
}
if (Grid[3] == Grid[4] && Grid[4] == Grid[5] && Grid[5] != GRID_TYPE_NONE) {
Winner = Grid[3];
}
if (Grid[6] == Grid[7] && Grid[7] == Grid[8] && Grid[8] != GRID_TYPE_NONE) {
Winner = Grid[6];
}
if (Grid[0] == Grid[3] && Grid[3] == Grid[6] && Grid[6] != GRID_TYPE_NONE) {
Winner = Grid[0];
}
if (Grid[1] == Grid[4] && Grid[4] == Grid[7] && Grid[7] != GRID_TYPE_NONE) {
Winner = Grid[1];
}
if (Grid[2] == Grid[5] && Grid[5] == Grid[8] && Grid[8] != GRID_TYPE_NONE) {
Winner = Grid[2];
}
if (Grid[0] == Grid[4] && Grid[4] == Grid[8] && Grid[8] != GRID_TYPE_NONE) {
Winner = Grid[0];
}
if (Grid[2] == Grid[4] && Grid[4] == Grid[6] && Grid[6] != GRID_TYPE_NONE) {
Winner = Grid[2];
}
if (Winner != 0) CongratulateWinner(Winner);
}
It creates a variable Winner which is set to 0 by default, so that if no winner is chosen (i.e. if the game is still going on and no one has three in a row), nothing happens (by default). Then it checks each of the rows and columns and diagonals, and if there is a three in a row somewhere of something that is NOT GRID_TYPE_NONE (i.e. an empty space), it sets Winner to whatever is in one of those occupied grids, and at the end calls the function CApp::CongratulateWinner(), with a parameter of whoever the Winner was (either 1 or 2, which are X and O respectively).
Create the function CApp::CongratulateWinner() under CApp::SetWinner(), and here’s the code:
void CApp::CongratulateWinner(int Winner) {
if (Winner == 1) {
WinsX++;
Reset();
} else {
WinsO++;
Reset();
}
}
It simply checks who the Winner was from CApp::SetWinner() and increments their score up by one, and then resets the board. Easy enough.
Finally, if you want, you can modify CApp::OnExit(), after the Running = false; line, to display an image, display text, output to a text file, or scream the name of (with sound haha) the TOTAL winner (whoever had the highest score in terms of WinsX and WinsY).
June 3rd, 2008 at 2:16 pm
Hey these r great im learning alot best tut`s around but i have ran into a problem with this one i get an error message like:
\CApp.cpp `amp’ undeclared (first use this function)
i done all the other tutorials and didnt have any problems
anyone help me? please
June 3rd, 2008 at 2:27 pm
I think you accidentally putting in amp instead of the ampersand sign &.
June 3rd, 2008 at 2:29 pm
Just thought I would mention this, if you are having trouble with code in some area, download the project files, and compare with that code. I always ensure that the code I put online compiles, so if you cannot get it to compile than you most likely mistyped something somewhere. Sometimes you’ll have just a simple syntax error, like putting a + instead of a *, or forgetting a character. Be careful when typing your code.
June 3rd, 2008 at 3:43 pm
Nice reference to [one of] my mistake[s] earlier
June 3rd, 2008 at 4:58 pm
oooh thanks i got it fixed i was copy and pasting the code but this line failed while(SDL_PollEvent(&Event)) { i changed it to while(SDL_PollEvent(&Event)) { and it worked fine thank you for your help
June 4th, 2008 at 2:10 am
THX Arseniy.
I have one more question why:
Grid[2] == Grid[4] && Grid[4] == Grid[6] works fine, but this:
Grid[2] == Grid[4] == Grid[6] don’t ??
June 4th, 2008 at 6:47 am
Arseniy, Haha, yes.
irishstevie, No problem. For people reading out there, he had an & amp; instead of a single &.
June 4th, 2008 at 6:50 am
przemo_li,
Grid[2] == Grid[4] == Grid[6] is made up of two interconnected logical operators, but C++ doesn’t allow that; you have to put two together with &&, ||, etc.
June 14th, 2008 at 10:39 am
my game works fine when i run it from inside codeblocks, but if i just run the executable the game window disappears almost instantly, can anyone tell me what i am doing wrong?
June 18th, 2008 at 5:02 am
Hello and thank you for these excellent tutorials.
I’ve been following these tutorials for some weeks now and here is my contribution :
http://www.zaroui.net/myCase.zip
The archive is in 7z format so just change the extension to get it open.
I contains VS2005 source code, project file, gfx stuff and a compiled win32 version.
This checks for game end and highlights the winning line. I also has basic buttons to choose the players (not yet implemented).
WIP for the IA part, I’ll post soon my brainy TicTacToe.
Yassine - France
June 18th, 2008 at 10:20 am
hans, your codeblocks compiles to a subdirectory called “release” or “debug” and when you run from codeblocks it uses paths relative to source code location and not executable location.
So to get your compiled file to work outside codeblocks, just copy it to where your .cpp files are.
Yassine
June 18th, 2008 at 3:00 pm
Hi,
one more beginner here (beginner in programming in general)!
i’ve also tried to make function to report winner and ofcourse something is wrong
first in CApp class i added:
SDL_Surface* X_wins;
SDL_Surface* O_wins;
and function:
int CheckWinner();
which looks something like:
int CApp::CheckWinner (){
if (Grid[0]==Grid[1] && Grid[1]==Grid[2] && Grid[0]!=GRID_TYPE_NONE) return Grid[0];
if (Grid[3]==Grid[4] && Grid[4]==Grid[5] && Grid[3]!=GRID_TYPE_NONE) return Grid[3];
if (Grid[6]==Grid[7] && Grid[7]==Grid[8] && Grid[6]!=GRID_TYPE_NONE) return Grid[6];
if (Grid[0]==Grid[3] && Grid[3]==Grid[6] && Grid[0]!=GRID_TYPE_NONE) return Grid[0];
if (Grid[1]==Grid[4] && Grid[4]==Grid[7] && Grid[1]!=GRID_TYPE_NONE) return Grid[1];
if (Grid[2]==Grid[5] && Grid[5]==Grid[8] && Grid[2]!=GRID_TYPE_NONE) return Grid[2];
if (Grid[0]==Grid[4] && Grid[4]==Grid[8] && Grid[0]!=GRID_TYPE_NONE) return Grid[0];
if (Grid[2]==Grid[4] && Grid[4]==Grid[6] && Grid[2]!=GRID_TYPE_NONE) return Grid[2];
return 0;
}
and in OnRender i added something like this:
if (CApp::CheckWinner()== GRID_TYPE_X) {
CSurface::OnDraw(Surf_Display, X_wins, 75, 100);
}else
if (CApp::CheckWinner()== GRID_TYPE_O) {
CSurface::OnDraw(Surf_Display, O_wins, 75, 100);
}
i’ve also added loading images X_wins and O_wins in OnInit
it does not work, so my question wold be if this could work like this at all (calling the function within OnRender and drawing appropriate image)?
June 18th, 2008 at 4:15 pm
Why are you using the :: qualifier to call it
if (CApp::CheckWinner()== GRID_TYPE_X)
Is it a static function?
June 19th, 2008 at 2:02 am
sorry, i forgot deleting that, just ignore it.
…that was not originaly there, i was checking for posible mistakes so i was just trying it out (even though i knew it wouldn’t help)
(btw- sorry for my not so good english)
June 19th, 2008 at 2:32 am
:)))
…it is working (for now)!
i checked that the names of images (while loading) were correct like 10 times and of course they were still wrong
they are okey now! finaly!
June 20th, 2008 at 6:48 am
Yassine Zaroui,
Thanks for your contribution and helping others out! Seems that people have been busy posting this last week.
cassiopeia,
Glad that you got it working; and keep on programming!