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

Original author: Tim Jones
  • Transfer
  • Tutorial
In previous lessons, we laid the foundation for game development. We created a basic framework with a set of general procedures, a class for handling events, and a class for working with surfaces. In this lesson we will use our best practices and create the first game - Tic Tac Toe. Do not worry, everything will be quite simple. Let's take the code written in previous lessons as the basis.

First of all (are we going to become serious game developers?) You need to draw up a small technical specification and determine the components of future tic-tac-toe. As we know, in the noughts and crosses there is a field 3x3 cells, in which two players alternately put X or About . 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 (because we can use them as many times as we want). Yes, in fact, you can create only 1 image for X and O (remember about SDL_Rect from the second lesson? In translation, I will stick to the original code, but if you are interested, then also see my implementation for linux ). We are ready to take the first step. Let's create our field with a size of 300x300 pixels, respectively, images X and Omake it 100x100 pixels in size, i.e. 1/9 of the field. I chose such sizes for a reason. As you probably already noticed, my screenshots do not stand out in high resolution, but because I write from a netbook =). You can use any other sizes (and create Tic Tac Toe HD).


Image of the field


Image from X and O

So, there are pictures, it remains to program their download. Open CApp.h and make the following changes - delete the test surface and announce 3 new ones:

CApp.h
#ifndef _CAPP_H_
    #define _CAPP_H_
#include 
#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 the same 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 probably already guessed that we will also 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 up, now it's time to start loading them into memory. Open CApp_OnInit.cpp and make changes there - delete the test surface, add new ones, and also resize the window to 300x300 so that we do not have any empty spaces in the window. And make sure the spelling of the paths to the image files is correct!

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;
}


Did you notice that I added ./gfx/ in front of the file names? I argue: during the development of serious and big games, the number of source code files and all kinds of resource files is growing steadily, therefore it is most convenient to place them in different folders. I think you will 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’s important to remember 5 simple steps when working with surfaces: declare, reset, load, draw, free (learn this mantra, because in your future development it will help to avoid many errors, memory leaks, brakes and malfunctions of your game).
Most likely you also drew attention to the fact that the pictures of tic-tac-toe on a pink background and thought - “And the author is that, emo?”. I hasten to convince you! It doesn’t matter what color we designate the background (just in this specific example, the contrast allows us to precisely 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 
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 the 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;
}


Let's consider the function in more detail. In addition to the pointer to the source surface, three more variables are transferred to it, each of which is responsible for the designation of the color component from the RGB format (if someone else is not in the know: R - red, G - green, B - blue; red, green and blue colors). Those. if we passed 255, 0, 0 to the function , then the red 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 transmitted values ​​(for example 255, 0, 0 ) in the Surf_Dest surface to transparent ("turns it off"). Now it’s time to figure out the signature of SDL_SetColorKey itself :

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

  • The first parameter defines the surface to which the function is applied;
  • The second parameter sends flags that mean: SDL_SRCCOLORKEY - indicates that the color transmitted by the third parameter ( Uint32 key ) should be turned off in the surface , SDL_RLEACCEL - allows the use of RLE image compression technology to accelerate the display of the surface. If we pass “0” instead of flags, then in this way we can clear the “turn-off” color value previously set on this surface;
  • The third parameter sets the “off” color value calculated using SDL_MapRGB , which searches for the most suitable color in the format of the transmitted surface (you know that color formats are different from each other, and this function helps us to “make friends” of them).

Well, that's all and done, 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 surfaces are tuned, the thing is small - draw them. First of all, we need an array of nine components that will store the type of area for displaying 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 upper right, etc. to the lower right. You need to register this array in CApp.h :

CApp.h
#ifndef _CAPP_H_
    #define _CAPP_H_
#include 
#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 are aware that each cell of our grid can be either empty or contain a cross or a zero. In order not to use all sorts of " magic numbers ", it is most reasonable for us to use the so-called enum, more about which can be read here . Thus, we will assign meaningful names to our cell states and we will always know that GRID_TYPE_NONE = 0, GRID_TYPE_X = 1, and GRID_TYPE_O = 2. Go back to CApp.h and add an enumeration under 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 for sure where in which file to write the code I proposed. It’s just that sometimes I give the code in its entirety, but I can just say where to place the code section, and then I hope for your competence. Almost everything is ready for us, 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, write:

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


This loop runs through the grid cells and clears them by setting the value GRID_TYPE_NONE . We are best off calling 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 accordingly 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 to be changed and the type by which its value needs to be changed. A primitive check is also implemented for the correctness of the transmitted parameters, in order to avoid game crashes due to going beyond the boundaries of the array. We proceed directly to the rendering:

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);
    }
}


Here a little more complicated. 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 for displaying the cross and the 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 a remainder and multiply again by 200, getting 0 for 0, 1 and 2, etc. After checking the type of the rendered cell, we display it. Now we just have to redefine 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 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 whether the cell that the player clicked is already filled, and if not, fill it with a picture of a cross or a zeros (depending on the player), and then switch the players (i.e. if the player’s identifier is CurrentPlayer = 1 the next button press will be interpreted as a click by a player with identifier 0). Open CApp.h and add the variable responsible for the player:

CApp.h
int CurrentPlayer;


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

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


That's it! The game is finally ready, so let's play! Compile and enjoy! Congratulations! You have come a long way and deserve a rest.
In the future, you can experiment and add inscriptions at the end of the game “X Won”, “O Won”, implement the game modes personVS person, personVS computer, etc. Go ahead, you already have the basis for the game!

And here is a video of the working tic-tac-toe:


My version on GitHub'e .

Source Code Links:


Links to all lessons:

Also popular now: