- PVSM.RU - https://www.pvsm.ru -

Перевод SDL Game Framework Series. Часть 4 — SDL Tutorial: Tic Tac Toe

В предыдущих уроках мы заложили основу для разработки игры. Мы создали базовый каркас с набором общих процедур, класс для обработки событий, а также класс для работы с поверхностями. В этом уроке мы будем использовать наши наработки и создадим первую игру — крестики-нолики (Tic Tac Toe). Не волнуйтесь, все будет довольно просто. За основу возьмем код написанный в предыдущих уроках.

В первую очередь (мы же собираемся стать серьезными разработчиками игр?) нужно составить небольшое ТЗ и определить составляющие будущих крестиков-ноликов. Как мы знаем, в крестиках-ноликах существует поле 3x3 клетки, в которые два игрока поочередно ставят X или O. Итак, нам нужно подготовить 3 изображения, одно для поля, и по одному для X и O. Заметьте, что нам не нужно создавать 9 картинок для X и O, а всего по одному (ведь мы можем использовать их столько раз сколько захотим). Да по существу то можно создать всего 1 изображение для X и O (помните про SDL_Rect из второго урока? В переводе я буду придерживаться оригинального кода, но если вам будет интересно, то посмотрите также мою реализацию под linux). Мы готовы сделать первый шаг. Наше поле давайте создадим размером 300x300 пикселей, соответственно изображения X и O сделаем размером 100x100 пикселей, т.е. 1/9 поля. Я выбрал такие размеры неслучайно. Как вы наверное уже заметили, мои скриншоты не выделяются высоким разрешением, а всё потому что пишу я с нетбука =). Вы же можете использовать любые другие размеры (и создать Tic Tac Toe HD).

Перевод SDL Game Framework Series. Часть 4 — SDL Tutorial: Tic Tac Toe
Изображение поля

Перевод SDL Game Framework Series. Часть 4 — SDL Tutorial: Tic Tac Toe
Изображение с X и O

Итак, картинки есть, осталось запрограммировать их загрузку. Откройте CApp.h и внесите следующие изменения — удалите тестовую поверхность и объявите 3 новые:

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

Практически аналогичные манипуляции нужно проделать и в 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();
}

Вы уже наверное догадались, что очищать мы тоже будем все получившиеся поверхности, поэтому откройте CApp_OnCleanup.cpp и напишите:

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

Итак, поверхности настроены, теперь самое время приступить к их загрузке в память. Откройте CApp_OnInit.cpp и сделайте там правки — удалите тестовую поверхность, добавьте новые, а также измените размеры окна на 300x300 чтобы у нас не было каких-либо пустых мест в окне. И убедитесь в правильности написания путей до файлов с картинками!

CApp_OnInit.cpp

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

Вы заметили, что я добавил ./gfx/ перед именами файлов? Аргументирую: во время разработки серъёзных и больших игр количество файлов исходного кода и всяческих файлов ресурсов неуклонно растет, поэтому удобнее всего располагать их в разных папках. Думаю вы со мной согласитесь =). Теперь давайте выведем наше поле на экран! Откройте CApp_OnRender.cpp и замените отображение тестовой поверхности на поверхность поля:

CApp_OnRender.cpp

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

Теперь можно скомпилировать игру и наслаждаться «красивейшим игровым полем на планете»! Сейчас важно запомнить 5 простых шагов при работе с поверхностями: объявить, обнулить, загрузить, отрисовать, освободить (заучите эту мантру, ведь в дальнейших ваших разработках это поможет избежать множества ошибок, утечек памяти, тормозов и сбоев в работе вашей игры).
Скорее всего вы также обратили внимание на то, что картинки крестиков-ноликов на розовом фоне и подумали — «А автор что, эмо?». Спешу вас переубедить! Неважно каким цветом мы обозначим фон (просто в этом конкретном примере контраст позволяет точно определить грань между изображением и фоном), ведь впоследствии фон мы «опустим» и сделаем прозрачным с помощью простой и понятной функции SDL_SetColorKey. Так давайте же её и применим! Для этого в файле CSurface.h напишите:

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

А в CSurface.cpp добавьте функцию:

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

Рассмотрим функцию более подробно. Кроме указателя на исходную поверхность ей передается ещё три переменных, каждая из которых отвечает за обозначение цветовой составляющей из формата RGB (если кто ещё не в курсе: R — red, G — green, B — blue; красный, зеленый и синий цвета). Т.е. если бы мы передали в функцию 255, 0, 0 то из поверхности «выключился» бы и стал прозрачным красный цвет.
Сперва наша функция производит проверку на существование поверхности и в случае успеха устанавливает соответствующий переданным значениям цвет (например 255, 0, 0) в поверхности Surf_Dest прозрачным («выключает» его). Настал черед разобраться в сигнатуре самой SDL_SetColorKey [1]:

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

  • Первый параметр определяет поверхность к которой применяется функция;
  • Вторым параметром передаются флаги, означающие: SDL_SRCCOLORKEY — указывает что в поверхности нужно выключить цвет, передаваемый третьим параметром (Uint32 key), SDL_RLEACCEL — разрешает использовать технологию сжатия изображений RLE [2] для ускорения отображения поверхности. Если мы передаем вместо флагов «0», то таким образом можем очистить значение «выключаемого» цвета, ранее установленное в этой поверхности;
  • Третий параметр задает значение «выключаемого» цвета, вычисляемое с помощью SDL_MapRGB [3], которая ищет максимально подходящий цвет в формате передаваемой поверхности (вы же знаете, что цветовые форматы отличаются друг от друга, а эта функция сильно помогает нам их «сдружить»).

Ну вот всё и готово, осталось применить функцию к нашим поверхностям. Откройте CApp_OnInit.cpp и напишите:

CApp_OnInit.cpp

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

Вот и настроены все поверхности, дело за малым — отрисовать их. Прежде всего нам нужен массив из девяти составляющих, которые будут хранить тип области для отображения крестика или нолика. Нулевой элемент будет хранить тип для верхнего левого квадранта, первый — для верхнего среднего, второй — для правого верхнего и т.д. до нижнего правого. Нужно прописать этот массив в 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

Мы в курсе что каждая ячейка нашей сетки может быть либо пустой, либо содержать крестик или нолик. Чтобы не использовать всякие "магические числа [4]", нам разумнее всего использовать т.н. перечисление (enum), подробнее про которое можно прочитать здесь [5]. Таким образом мы назначим нашим состояниям ячейки осмысленные имена и всегда будем знать что GRID_TYPE_NONE = 0, GRID_TYPE_X = 1, и GRID_TYPE_O = 2. Вернитесь к CApp.h и добавьте перечисление под объявлением массива координат ячеек:

CApp.h

enum {
    GRID_TYPE_NONE = 0,
    GRID_TYPE_X,
    GRID_TYPE_O
};

Надеюсь вы хорошо ориентируетесь в коде нашего фрэймворка и точно знаете в какое место какого файла писать предложенный мною код. Просто иногда я привожу код полностью, но могу просто сказать где расположить участок кода, а дальше надеюсь на вашу компетенцию. У нас почти всё готово, осталось предусмотреть функцию для очистки нашего игрового поля. Давайте объявим в CApp.h функцию Reset:

CApp.h

public:
    void Reset();

В CApp.cpp пропишите:

CApp.cpp

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

Этот цикл пробегается по ячейкам сетки и очищает их, путем установки значения GRID_TYPE_NONE. Нам лучше всего вызывать эту очистку в самом начале запуска игры, поэтому, откройте CApp_OnInit.cpp и внесите следующие изменения:

CApp_OnInit.cpp

//...остальной код...
 
CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);
 
Reset();

Теперь добавим возможность отрисовки крестиков и ноликов. Определите в CApp.h ещё одну функцию:

CApp.h

void SetCell(int ID, int Type);

Ну и, соответственно в 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;
}

Эта функция, как мы видим, принимает 2 аргумента — ID изменяемой ячейки и тип на который нужно изменить её значение. Также реализована примитивная проверка на корректность передаваемых параметров, во избежание крахов игры из-за выхода за границы массива. Переходим непосредственно к отрисовке:

CApp_OnRender.cpp

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

Тут немного посложнее. Сначала мы запускаем цикл в котором бегаем по всем ячейкам в сетке игрового поля, а потом, в зависимости от типа ID, отображаем крестик либо нолик. А находим мы координаты для отображения крестика и нолика следующим образом: для X-координаты делим итератор на 3 с остатком и умножаем на 200 (размер одной отображаемой ячейки), в итоге получая 0 для i = 0, 1 для 1, 2 для 2, 0 для 3 и т.д.; для Y-координаты делим итератор на 3 без остатка и умножаем опять на 200, получая 0 для 0, 1 и 2, и т.д. После проверки типа отрисовываемой ячейки, мы её отображаем. Теперь нам остается только переопределить функцию обработки событий и добавить переменную, отвечающую за игрока. Откройте CApp.h и добавьте функцию OnLButtonDown чуть ниже функции OnEvent:

CApp.h

void OnLButtonDown(int mX, int mY);

Теперь измените CApp_OnEvent:

CApp_OnEvent.cpp

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

Этим кодом мы выполняем проверку на то, заполнена ли уже ячейка на которую нажал игрок, и если нет, то заполняем её картинкой крестика или нолика (в зависимости от игрока), а затем переключаем игроков (т.е. если идентификатор игрока — CurrentPlayer = 1 следующее нажатие кнопки будет интерпретировано как нажатие игроком c идентификатором 0). Откройте CApp.h и добавьте переменную, отвечающую за игрока:

CApp.h

int CurrentPlayer;

И не забудьте её обнулить в CApp.cpp:

CApp.cpp

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

Ну вот и всё! Наконец-то игра готова, так что let's play! Откомпилируйте и наслаждайтесь! Поздравляю вас! Вы прошли долгий путь и заслужили отдых.
В перспективе вы можете поэкспериментировать и добавить надписи в конце игры «X Выиграл», «O выиграл», реализовать режимы игры человекVSчеловек, человекVSкомпьютер и т.д. Дерзайте, ведь основа для игры у вас уже есть!

А вот видео работающих крестиков-ноликов:

Mой вариант на GitHub'e [6].

Ссылки на исходный код:

Ссылки на все уроки:

Автор: m0sk1t

Источник [12]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-3/27004

Ссылки в тексте:

[1] SDL_SetColorKey: http://www.libsdl.org/docs/html/sdlsetcolorkey.html

[2] RLE: http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%B4%D0%BB%D0%B8%D0%BD_%D1%81%D0%B5%D1%80%D0%B8%D0%B9

[3] SDL_MapRGB: http://www.libsdl.org/docs/html/sdlmaprgb.html

[4] магические числа: http://ru.wikipedia.org/wiki/%D0%9C%D0%B0%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D1%87%D0%B8%D1%81%D0%BB%D0%BE_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

[5] здесь: http://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D1%87%D0%B8%D1%81%D0%BB%D1%8F%D0%B5%D0%BC%D1%8B%D0%B9_%D1%82%D0%B8%D0%BF

[6] на GitHub'e: https://github.com/m0sk1t/TicTacToe

[7] Win32: http://www.sdltutorials.com/Data/Posts/103//sdl-tutorial-tic-tac-toe.zip

[8] *nix: http://www.sdltutorials.com/Data/Posts/103//sdl-tutorial-tic-tac-toe.tar.gz

[9] Разработка игрового фрэймворка. Часть 1 — Основы SDL: http://habrahabr.ru/post/166875/

[10] Разработка игрового фрэймворка. Часть 2 — Координаты и отображение: http://habrahabr.ru/post/167035/

[11] Разработка игрового фрэймворка. Часть 3 — События: http://habrahabr.ru/post/167245/

[12] Разработка игрового фрэймворка. Часть 4 — Крестики-Нолики: http://habrahabr.ru/post/167443/