Up 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 3x3 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 600x600, and our X's and O's will be 200x200 (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.
#ifndef _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;
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.
#include "CApp.h"
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.
#include "CApp.h"
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 600x600, the size of the grid. This make sure we don't have any blank space around the window we aren't using.
#include "CApp.h"
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.
#include "CApp.h"
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.
#ifndef _CSURFACE_H_
#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:
bool CSurface::Transparent(SDL_Surface* Surf_Dest, int R, int G, int B) {
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:
#include "CApp.h"
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:
#ifndef _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:
enum {
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.
public:
void Reset();
Open up CApp.cpp and add the following function just before int main:
void CApp::Reset() {
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.
void SetCell(int ID, int Type);
Now, open back up CApp.cpp and add the function:
void CApp::SetCell(int ID, int Type) {
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:
for(int i = 0;i < 9;i++) {
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 200x200 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.
void OnLButtonDown(int mX, int mY);
Now, open up CApp_OnEvent.cpp and add the function:
void CApp::OnLButtonDown(int mX, int mY) {
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:
int CurrentPlayer;
Also, set the default value for this variable in CApp.cpp:
CApp::CApp() {
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.
First of all: GREAT tutorials, REALLY helpful. However I have a problem, I guess it's more with C++ than SDL but hopefully you will find a solution.
Here's some code:
CApp.h:
enum GameStatus {
playing = 0,
won1,
won2,
draw
};
GameStatus status;
///////////E O F ///////////////
CApp.cpp:
#include "CApp.h"
///[...]
CApp::CApp() {
//[..]
status = playing;
///[...]
}
////////////E O F/////////////
CApp_OnRender.cpp:
#include "CApp.h"
///[...]
if(status == playing){///instructions }
//////////E O F//////////////////
Problem is, even though I set up my status to playing in the constructor of the CApp, in the onRender functions - it's not on playing. Why? I can put even in OnExecute status = playing; but still - not helping. If I put status = playing; in the onRender.cpp IT WORKS, but it's not what I want to do.
Any suggestions - anyone? ;(
Email (required, not published):
Website:
No, you do not have to specify anything within SDL for PNG transparency, it's automatically picked up. However, when you load them, be sure to use SDL_DisplayFormatAlpha instead of SDL_DisplayFormat.
Email (required, not published):
Website:
Quick question, when working with png files, do you need to specify the transparent color at all if you save that information in the png itself? In your later tutorials when you make the switch to SDL_image, it seems that SDL_SetColorKey makes no difference if it is called or not. Can you elaborate?
Email (required, not published):
Website:
Thanks for the contribution!
Email (required, not published):
Website:
AI.cpp:
#include "CApp.h"
void CApp::AI_Loop(){
if (Number_Of_Players < 2) {
//The computer takes O.
while(CurrentPlayer == 1){
if (Grid_count > 8) {
break;}
if (Grid_count > 2) {
if ((Grid[0] != GRID_TYPE_NONE) && (Grid[0] == Grid[4]) && (Grid[8] == GRID_TYPE_NONE))
{
Move(8);
return;}
else if ((Grid[4] != GRID_TYPE_NONE) && (Grid[4] == Grid[8]) && (Grid[0] == GRID_TYPE_NONE))
{
Move(0);
return;}
else if ((Grid[2] != GRID_TYPE_NONE) && (Grid[2] == Grid[4]) && (Grid[6] == GRID_TYPE_NONE))
{
Move(6);
return;}
else if ((Grid[6] != GRID_TYPE_NONE) && (Grid[4] == Grid[6]) && (Grid[2] == GRID_TYPE_NONE))
{
Move(2);
return;}
else if ((Grid[0] != GRID_TYPE_NONE) && (Grid[0] == Grid[8]) && (Grid[4] == GRID_TYPE_NONE))
{
Move(4);
return;}
else if ((Grid[2] != GRID_TYPE_NONE) && (Grid[2] == Grid[6]) && (Grid[4] == GRID_TYPE_NONE))
{
Move(4);
return;}
else{
for (int ID = 0; ID < 7; ID++){
int row = ID / 3;
int col = ID % 3;
if (col == 0) {
if ((Grid[ID] != GRID_TYPE_NONE) && (Grid[ID] == Grid[ID + 1]) && (Grid[ID+2] == GRID_TYPE_NONE))
{
Move(ID+2);
return;}
else if ((Grid[ID+1] != GRID_TYPE_NONE) && (Grid[ID+1] == Grid[ID+2]) && (Grid[ID] == GRID_TYPE_NONE))
{
Move(ID);
return;}
else if ((Grid[ID +2] != GRID_TYPE_NONE) && (Grid[ID] == Grid[ID+2]) && (Grid[ID+1] == GRID_TYPE_NONE))
{
Move(ID+1);
return;}
}
else if (row == 0) {
if ((Grid[ID] != GRID_TYPE_NONE) && (Grid[ID] == Grid[ID+3]) && (Grid[ID+6] == GRID_TYPE_NONE))
{
Move(ID+6);
return;}
else if ((Grid[ID+3] != GRID_TYPE_NONE) && (Grid[ID+3] == Grid[ID+6]) && (Grid[ID] == GRID_TYPE_NONE))
{
Move(ID);
return;}
else if ((Grid[ID+6] != GRID_TYPE_NONE) && (Grid[ID] == Grid[ID+6]) && (Grid[ID+3] == GRID_TYPE_NONE))
{
Move(ID+3);
return;}
}
}
}
srand(time(NULL));
for (int i = rand() % 9; i<9; i++){
if(Grid[i] == GRID_TYPE_NONE){
Move(i);
break;
}
else{ i = rand() % 9;}
}
}
//Should happen when all else fails. Replace with C rand function later.
else {
for (int i = rand() % 9; i<9; i++)
{
if(Grid[i] == GRID_TYPE_NONE){
Move(i);
break;
}
else { i = rand() % 9;}
}
}
}
}
}
I added a variable Number_Of_Players to CApp. To switch players during gameplay, there's this function (added to OnEvent):
void CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
if (sym == SDLK_1) {
Number_Of_Players = 1;
}
if (sym == SDLK_2) {
Number_Of_Players = 2;
}
if (sym == SDLK_ESCAPE) {
OnExit();}
else;
}
And Move looks like this:
void CApp::Move(int Where) {
if (CurrentPlayer == 1)
{ SetCell(Where, GRID_TYPE_O);
CurrentPlayer = 0;
}
else if (CurrentPlayer == 0)
{ SetCell(Where, GRID_TYPE_X);
CurrentPlayer = 1;
}
++Grid_count;
}
I added these to the end of CApp::Reset()
EndGame = false;
Grid_count = 0;
Winner = 0;
Where EndGame is used to check several things, Grid_count is the number of pieces in the grid, and Winner is TIE, PLAYER_ONE, or PLAYER_TWO (using an enum). I probably added a bit more than this, so sorry if anything turned out unclear. It might be a little sloppy in places, but it works! You can try it from here: http://superamateur.web44.net/Tic%20Tac%20Toe%20Tut.zip
Email (required, not published):
Website:
Email (required, not published):
Website:
Email (required, not published):
Website:
Bit of help please.
Many thanks!!
Email (required, not published):
Website:
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!
Email (required, not published):
Website:
...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!
Email (required, not published):
Website:
Email (required, not published):
Website: