📜 ⬆️ ⬇️

Translation SDL Game Framework Series. Part 4 - SDL Tutorial: Tic Tac Toe

In previous lessons, we laid the groundwork for developing the game. We created a basic framework with a set of common procedures, a class for handling events, and a class for working with surfaces. In this lesson we will use our work and create the first game - Tic-tac-toe (Tic Tac Toe). Don't worry, everything will be pretty simple. We take the code written in the previous lessons as a basis.

First of all (are we going to become serious game developers?), We need to make a small statement of work and determine the components of future tic-tac-toe. As we know, in the tic-tac-toe there is a field of 3x3 cells in which two players alternately put X or O. So, we need to prepare 3 images, one for the field, and one for X and O. Note that we do not need to create 9 pictures for X and O , but only one at a time (after all, we can use them as many times as we like). Yes, in essence, you can create only 1 image for X and O (remember about SDL_Rect from the second lesson? Translated, I’ll stick to the original code, but if you're interested, then see my implementation under linux ). We are ready to take the first step. Let's create our field with the size of 300x300 pixels, respectively, the X and O images will be 100x100 pixels in size, i.e. 1/9 of the field. I chose these sizes by chance. As you probably already noticed, my screenshots are not highlighted in high resolution, but because I’m writing from a netbook =). You can use any other sizes (and create Tic Tac Toe HD).


Field image


Image with X and O
')
So, there are pictures, it remains to program their download. Open CApp.h and make the following changes - remove the test surface and declare 3 new ones:

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; public: CApp(); int OnExecute(); public: bool OnInit(); void OnEvent(SDL_Event* Event); void OnExit(); void OnLoop(); void OnRender(); void OnCleanup(); }; #endif 


Almost similar manipulations need to be done in CApp.cpp :

CApp.cpp
 #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(); } 


You've probably already guessed that we, too, will clean all the resulting surfaces, so open CApp_OnCleanup.cpp and write:

CApp_OnCleanup.cpp
 #include "CApp.h" void CApp::OnCleanup() { SDL_FreeSurface(Surf_Grid); SDL_FreeSurface(Surf_X); SDL_FreeSurface(Surf_O); SDL_FreeSurface(Surf_Display); SDL_Quit(); } 


So, the surfaces are set, now is the time to start loading them into memory. Open CApp_OnInit.cpp and make edits there - remove the test surface, add new ones, and also resize the window to 300x300 so that we do not have any empty places in the window. And make sure that the paths to the image files are spelled correctly!

CApp_OnInit.cpp
 #include "CApp.h" bool CApp::OnInit() { if(SDL_Init(SDL_INIT_EVERYTHING) < 0) { return false; } if((Surf_Display = SDL_SetVideoMode(300, 300, 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; } 


Have you noticed that I added ./gfx/ in front of the file names? I argue: during the development of honest and large games, the number of source code files and all kinds of resource files is growing steadily, so it is most convenient to place them in different folders. I think you agree with me =). Now let's display our field on the screen! Open CApp_OnRender.cpp and replace the display of the test surface with the field surface:

CApp_OnRender.cpp
 #include "CApp.h" void CApp::OnRender() { CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0); SDL_Flip(Surf_Display); } 


Now you can compile the game and enjoy "the most beautiful playing field on the planet!" Now it is important to remember 5 simple steps when working with surfaces: declare, reset, load, draw, release (memorize this mantra, because in your further development this will help avoid many mistakes, memory leaks, brakes and malfunctions of your game).
Most likely you also noticed that the pictures of crosses and zeros on a pink background and thought - "And what is the author, emo?". I hasten to convince you! It does not matter what color we denote the background (just in this particular example, the contrast allows us to accurately determine the line between the image and the background), because later we will “lower” the background and make it transparent using the simple and clear SDL_SetColorKey function. So let's apply it! To do this in the CSurface.h file write:

CSurface.h
 #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 


And in CSurface.cpp add a function:

CSurface.cpp
 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; } 


Consider the function in more detail. In addition to the pointer to the original surface, three more variables are transferred to it, each of which is responsible for designating the color component from the RGB format (if someone is not yet up to date: R - red, G - green, B - blue; red, green and blue). Those. if we passed 255, 0, 0 to the function , then the red color would “turn off” from the surface and become transparent.
First, our function checks for the existence of the surface and, if successful, sets the color corresponding to the transferred values ​​(for example, 255, 0, 0 ) in the Surf_Dest surface to be transparent (“turns it off”). The turn has come to understand the signature of the SDL_SetColorKey itself:

 int SDL_SetColorKey(SDL_Surface *surface, Uint32 flag, Uint32 key); 


Well, that's all ready, it remains to apply the function to our surfaces. Open CApp_OnInit.cpp and write:

CApp_OnInit.cpp
 #include "CApp.h" bool CApp::OnInit() { if(SDL_Init(SDL_INIT_EVERYTHING) < 0) { return false; } if((Surf_Display = SDL_SetVideoMode(300, 300, 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; } 


So all the surfaces are set up, things are easy to draw. First of all, we need an array of nine components that will store the type of the area to display a cross or zero. The zero element will store the type for the upper left quadrant, the first for the upper middle, the second for the right upper, etc. to the bottom right. You need to register this array in CApp.h :

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 of our grid can be either empty or contain a cross or a toe. In order not to use all sorts of " magic numbers ", we are wise to use the so-called enumeration (enum), more about which you can read here . Thus, we will assign meaningful names to our cell states and will always know that GRID_TYPE_NONE = 0, GRID_TYPE_X = 1, and GRID_TYPE_O = 2. Go back to CApp.h and add the enumeration below the declaration of the cell coordinate array:

CApp.h
 enum { GRID_TYPE_NONE = 0, GRID_TYPE_X, GRID_TYPE_O }; 


I hope you are well versed in the code of our framework and know exactly where the file to write the code I proposed. Just sometimes I quote the code completely, but I can just say where to place a piece of code, and then I hope for your competence. We have almost everything ready, it remains to provide a function for cleaning our playing field. Let's declare the reset function in CApp.h :

CApp.h
 public: void Reset(); 


In CApp.cpp list:

CApp.cpp
 void CApp::Reset() { for(int i = 0;i < 9;i++) { Grid[i] = GRID_TYPE_NONE; } } 


This cycle runs through the grid cells and clears them by setting the value of GRID_TYPE_NONE . It’s best for us to call this cleanup at the very start of the game, so, open CApp_OnInit.cpp and make the following changes:

CApp_OnInit.cpp
 //... ... CSurface::Transparent(Surf_X, 255, 0, 255); CSurface::Transparent(Surf_O, 255, 0, 255); Reset(); 


Now add the ability to draw crosses and zeros. Define another function in CApp.h :

CApp.h
 void SetCell(int ID, int Type); 


Well and, respectively, in CApp.cpp :

CApp.cpp
 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, as we see, takes 2 arguments - the ID of the cell being changed and the type to which its value should be changed. Also implemented a primitive check for the correctness of the passed parameters in order to avoid crashes of the game due to the output of the array. Go directly to the drawing:

CApp_OnRender.cpp
 for(int i = 0;i < 9;i++) { int X = (i % 3) * 100; int Y = (i / 3) * 100; 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); } } 


It's a little more complicated here. First, we start a cycle in which we run through all the cells in the grid of the playing field, and then, depending on the type of ID, we display a cross or a zero. And we find the coordinates to display the cross and zero as follows: for the X-coordinate, divide the iterator by 3 with the remainder and multiply by 200 (the size of one displayed cell), eventually getting 0 for i = 0, 1 for 1, 2 for 2, 0 for 3, etc .; for the Y-coordinate, divide the iterator by 3 without remainder and multiply again by 200, getting 0 for 0, 1 and 2, etc. After checking the type of cell being drawn, we display it. Now we just have to override the event handling function and add the variable responsible for the player. Open CApp.h and add the OnLButtonDown function just below the OnEvent function:

CApp.h
 void OnLButtonDown(int mX, int mY); 


Now change the CApp_OnEvent :

CApp_OnEvent.cpp
 void CApp::OnLButtonDown(int mX, int mY) { int ID = mX / 100; ID = ID + ((mY / 100) * 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; } } 


With this code, we check to see if the player’s cell has already been filled in, and if not, fill it with a cross or zero (depending on the player), and then switch players (i.e. if the player ID is CurrentPlayer = 1 the next button click will be interpreted as a player click with ID 0). Open CApp.h and add a variable responsible for the player:

CApp.h
 int CurrentPlayer; 


And do not forget to reset it in CApp.cpp :

CApp.cpp
 CApp::CApp() { CurrentPlayer = 0; Surf_Grid = NULL; Surf_X = NULL; Surf_O = NULL; Surf_Display = NULL; Running = true; } 


Well that's all! The game is finally ready, so let's play! Compile and enjoy! Congratulations! You have come a long way and deserved rest.
In the future, you can experiment and add inscriptions at the end of the game “X Won”, “O won”, implement the game modes for man VS man, man VS computer, etc. Dare, because you already have the basis for the game!

But the video of working tic-tac-toe


My version on GitHub'e .

Links to source code:


Links to all lessons:

Source: https://habr.com/ru/post/167443/


All Articles