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

в 19:10, , рубрики: c++, game development, Gamedev, sdl, переводы

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

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

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

Ну вот всё и готово, осталось применить функцию к нашим поверхностям. Откройте 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

Мы в курсе что каждая ячейка нашей сетки может быть либо пустой, либо содержать крестик или нолик. Чтобы не использовать всякие "магические числа", нам разумнее всего использовать т.н. перечисление (enum), подробнее про которое можно прочитать здесь. Таким образом мы назначим нашим состояниям ячейки осмысленные имена и всегда будем знать что 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.

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

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

Автор: m0sk1t

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js