- PVSM.RU - https://www.pvsm.ru -
В предыдущих уроках мы заложили основу для разработки игры. Мы создали базовый каркас с набором общих процедур, класс для обработки событий, а также класс для работы с поверхностями. В этом уроке мы будем использовать наши наработки и создадим первую игру — крестики-нолики (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).
Изображение поля
Изображение с X и O
Итак, картинки есть, осталось запрограммировать их загрузку. Откройте CApp.h и внесите следующие изменения — удалите тестовую поверхность и объявите 3 новые:
#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:
#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 и напишите:
#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 чтобы у нас не было каких-либо пустых мест в окне. И убедитесь в правильности написания путей до файлов с картинками!
#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 и замените отображение тестовой поверхности на поверхность поля:
#include "CApp.h"
void CApp::OnRender() {
CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0);
SDL_Flip(Surf_Display);
}
Теперь можно скомпилировать игру и наслаждаться «красивейшим игровым полем на планете»! Сейчас важно запомнить 5 простых шагов при работе с поверхностями: объявить, обнулить, загрузить, отрисовать, освободить (заучите эту мантру, ведь в дальнейших ваших разработках это поможет избежать множества ошибок, утечек памяти, тормозов и сбоев в работе вашей игры).
Скорее всего вы также обратили внимание на то, что картинки крестиков-ноликов на розовом фоне и подумали — «А автор что, эмо?». Спешу вас переубедить! Неважно каким цветом мы обозначим фон (просто в этом конкретном примере контраст позволяет точно определить грань между изображением и фоном), ведь впоследствии фон мы «опустим» и сделаем прозрачным с помощью простой и понятной функции SDL_SetColorKey. Так давайте же её и применим! Для этого в файле 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 добавьте функцию:
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);
Ну вот всё и готово, осталось применить функцию к нашим поверхностям. Откройте 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:
#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 и добавьте перечисление под объявлением массива координат ячеек:
enum {
GRID_TYPE_NONE = 0,
GRID_TYPE_X,
GRID_TYPE_O
};
Надеюсь вы хорошо ориентируетесь в коде нашего фрэймворка и точно знаете в какое место какого файла писать предложенный мною код. Просто иногда я привожу код полностью, но могу просто сказать где расположить участок кода, а дальше надеюсь на вашу компетенцию. У нас почти всё готово, осталось предусмотреть функцию для очистки нашего игрового поля. Давайте объявим в CApp.h функцию Reset:
public:
void Reset();
В CApp.cpp пропишите:
void CApp::Reset() {
for(int i = 0;i < 9;i++) {
Grid[i] = GRID_TYPE_NONE;
}
}
Этот цикл пробегается по ячейкам сетки и очищает их, путем установки значения GRID_TYPE_NONE. Нам лучше всего вызывать эту очистку в самом начале запуска игры, поэтому, откройте CApp_OnInit.cpp и внесите следующие изменения:
//...остальной код...
CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);
Reset();
Теперь добавим возможность отрисовки крестиков и ноликов. Определите в CApp.h ещё одну функцию:
void SetCell(int ID, int Type);
Ну и, соответственно в 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 изменяемой ячейки и тип на который нужно изменить её значение. Также реализована примитивная проверка на корректность передаваемых параметров, во избежание крахов игры из-за выхода за границы массива. Переходим непосредственно к отрисовке:
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:
void OnLButtonDown(int mX, int mY);
Теперь измените CApp_OnEvent:
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 и добавьте переменную, отвечающую за игрока:
int CurrentPlayer;
И не забудьте её обнулить в 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/
Нажмите здесь для печати.