Я поискал по хабру перевод уроков с этого сайта, но нашлось только одно упоминание, да и то — в комментариях:
Потому и решил исправить положение, попытался дополнить и разнообразить примеры своими наработками, а заодно попрактиковался в переводе. Также, поскольку моей любимой ОС сначала была WinXP а теперь Ubuntu, постараюсь сделать кроссплатформенные примеры, захватив как можно больше нюансов настройки для этих платформ. В данной серии уроков рассматривается создание фрэймфорка, достаточного для начала разработки 2D игр.
Что из этого получилось
«Все мы с чего-то начинали»
Эти уроки позволяют научиться программировать с использованием библиотеки SDL тем людям, которые уже имеют какой-либо опыт программирования на ЯП C++ или других языках (прим. переводчика: существует множество привязок SDL к другим ЯП, список которых приведен здесь и мне кажется он далеко не полный). Если по мере чтения уроков у вас будут возникать какие-то сложности с пониманием самого кода и используемых концепций (не разрабатываемой игры, а именно программного кода), рекомендую сначала ознакомиться с нашими Уроками по программированию на C++. Необязательно читать все эти уроки, но каждый из них, в дальнейшем, поспособствует более глубокому пониманию того что мы будем создавать.
Используемые инструменты
В уроках в качестве IDE будет использоваться Code::Blocks (Далее CB) включающий в себя GCC и MinGW для компиляции проектов. Вы же можете использовать свои любимые IDE и компиляторы, но это может выйти вам боком если у вас недостаточно опыта в подключении библиотек. Для начала необходимо скачать CB отсюда выбрав бинарную сборку, включающую в себя MinGW, например codeblocks-12.11mingw-setup.exe. Рекомендуется использовать стабильную версию, дабы сэкономить время.
Как вы уже поняли основное внимание будет сконцентрировано вокруг SDL (Simple DirectMedia Layer) — кроссплатформенной библиотеки для 2D отображения графики. Дальше идет много слов про то какая библиотека замечательная (ну это и так известно), много слов про настройку (которая у меня почему-то не завелась), извинения автора за копирование в своих примерах заголовочных фалов SDL в каталог примера и прочее. Как и обещал, напишу немного отсебятины, как можно настроить связку CB&SDL на Windows и на Ubuntu. Также буду использовать относительный путь до заголовочных файлов в папке с MinGW (ну просто мне так удобнее).
Настройка
- Скачать свежий CB с включенным MinGW с официального сайта, установить MinGW в папку с CB;
- Скачать свежие библиотеки разработки SDL (в разделе Downloads выбрать секцию Development Libraries, подсецкию Win32 и файл SDL-devel-X.X.X-mingw32.tar.gz где X.X.X — номер текущей версии);
- Распаковать архив во временную папку (после распаковки появится папка SDL-X.X.X);
- Содержимое этой папки полностью переместить в папку, в которую ранее устанавливали MinGW;
- Скопировать SDL.dll из подпапки bin папки MinGW в системные папки C:Windows и C:WindowsSystem32;
- Открыть CB, создать новый пустой проект;
- Добавить файлы CApp.h и CApp.cpp с содержанием, указанным чуть ниже под спойлерами;
- В меню Project>Build options, в диалоге выбрать вкладку Linker settings;
- В Link libraries нажать add и ввести mingw32;SDLmain;SDL;
- (возможно придется изменить CApp.h и написать SDL/SDL.h вместо SDLSDL.h если компилятор будет ругаться на отсутствие заголовочных файлов)
- sudo apt-get install codeblocks libsdl-ttf2.0-0 libsdl-ttf2.0-dev libsdl-image1.2 libsdl-image1.2-dev libsdl-mixer1.2 libsdl-mixer1.2-dev libsdl1.2-dev libsdl1.2debian-all libgl1-mesa-dev libglu1-mesa-dev libglut3-dev xorg-dev libtool gforth
- Для любителей пособирать руками
- Открыть CB, создать новый пустой проект;
- Добавить файлы CApp.h и CApp.cpp с содержанием, указанным чуть ниже под спойлерами;
- В меню Project>Build options, в диалоге выбрать вкладку Linker settings;
- В Other linker options ввести -lSDLmain -lSDL;
- (возможно придется изменить CApp.h и написать SDL/SDL.h вместо SDLSDL.h если компилятор будет ругаться на отсутствие заголовочных файлов)
Исходный код
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL/SDL.h>
class CApp {
public:
CApp();
int OnExecute();
};
#endif
#include "CApp.h"
CApp::CApp() {
}
int CApp::OnExecute() {
return 0;
}
int main(int argc, char* argv[]) {
CApp theApp;
return theApp.OnExecute();
}
Игровая концепция
Немного отступлю от самого программирования и отмечу, что создаваемый класс CApp задает поведение нашей программы. Подавляющее большинство игр состоит, в основном из 5 функций, вызываемых в главном цикле работы программы, которые и обрабатывают весь игровой процесс. Краткое описание каждой из них:
Функция инициализации
Обрабатывает все загрузки данных, будь то текстуры, карты, персонажи, или любые другие (аудиовидео к примеру).
Обработчик событий
Обрабатывает все входящие сообщения от мыши, клавиатуры, джойстиков, или других устройств, либо программных таймеров.
Игровой цикл
Обрабатывает все обновления процесса, такие как смена координат толпы противников, движущихся по экрану и стремящихся уменьшить ваше здоровье, или что-то иное (расчет появления бонусов, обнаружение столкновений).
Отрисовка сцен
Занимается отображением рассчитанных и подготовленных в предыдущей функции сцен на экран, и соответственно никаких манипуляций с данными (окромя вывода) производить не обязана.
Очистка памяти
Просто удаляет все загруженные ресурсы (карты, изображения, модели) из ОЗУ, и обеспечивает корректное завершение игры.
Важно чтобы вы понимали, что игры выполняются в одном цикле. В рамках этого цикла мы обрабатываем события, обновления данных и визуализацию изображений. Таким образом, основная структура игры может рассматриваться так:
Initialize();
while(true) {
Events();
Loop();
Render();
}
Cleanup();
Как видно по такой схеме, сначала происходит инициализация, затем в каждой итерации цикла происходит обработка событий, манипуляция с данными и, соответственно, отрисовка, после чего программа корректно завершается, выгружая данные. Иногда для создания игры обработка событий не требуется, но необходима, когда вы хотите, чтобы пользователь мог манипулировать данными (например, перемещением персонажа).
Поясню эту мысль на примере. Скажем, у нас есть герой игры, назовем его DonkeyHot. Все, что мы хотим сделать, это просто заставить его перемещаться по экрану (т.е. если мы нажмем стрелку влево, он идет налево и т.д.). Мы должны выяснить, как это сделать в цикле. Во-первых, мы знаем, что нам нужно проверить определенное сообщение (в данном случае сообщение с клавиатуры о нажатии клавиши). Поскольку мы уже знаем что обработчик событий нашей игры отвечает за обновление данных (изменение координат DonkeyHot'a в частности), то нам, соответственно, эти данные и нужно изменить. Затем остается вывести на экран нашего DonkeyHot'a с обновленными значениями координат. Можно представить так:
if(Key == LEFT) X--;
if(Key == RIGHT) X++;
if(Key == UP) Y--;
if(Key == DOWN) Y++;//... где-то в коде ...
RenderImage(DonkeyHotImage, X, Y);
Это работает следующим образом: в цикле проверяется было ли нажатие клавиши LEFT, RIGHT и т.д., и если было, то мы уменьшаем или увеличиваем переменную отвечающую за координату X либо Y. Так что, если наша игра идет с частотой 30 кадров в секунду, и мы нажмем клавишу LEFT, наш DonkeyHot будет двигаться влево со скоростью 30 пикселей в секунду (про FPS отлично написано здесь). Если вы всё ещё не понимаете устройство игрового цикла, то не волнуйтесь — скоро поймете.
Много воды утекло с тех пор как мы отошли от проекта в CB и начали постигать устройство типичного игрового цикла. Настало время к нему вернуться и добавить следующие файлы со следующим содержанием:
#include "CApp.h"
bool CApp::OnInit() {
return true;
}
#include "CApp.h"
void CApp::OnEvent(SDL_Event* Event) {
}
#include "CApp.h"
void CApp::OnLoop() {
}
#include "CApp.h"
void CApp::OnRender() {
}
#include "CApp.h"
void CApp::OnCleanup() {
}
Открыть CApp.h и CApp.cpp, и привести их к такому виду:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL.h>
class CApp {
private:
bool Running;
public:
CApp();
int OnExecute();
public:
bool OnInit();
void OnEvent(SDL_Event* Event);
void OnLoop();
void OnRender();
void OnCleanup();
};
#endif
#include "CApp.h"
CApp::CApp() {
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();
}
Вы видите некоторые новые переменные и методы, но давайте посмотрим, что происходит в первую очередь. Во-первых, мы пытаемся инициализировать нашу игру, если инициализация не произошла возвращаем -1 (код ошибки), тем самым обеспечивая завершение игры. Если все хорошо, мы переходим в игровой цикл. В рамках цикла игры мы используем SDL_PollEvent для проверки сообщений, и передаем их по одному, в метод OnEvent. Затем, мы переходим в OnLoop для манипуляции с данными, а потом отрисовываем на экран нашу игру. Повторяем этот цикл до тех пор пока не будет встречено сообщение которое означает что пользователь выходит из игры, после получения такого сообщения (например пользователь нажал крестик или клавишу ESC) мы переходим в OnCleanup для очистки ресурсов занимаемых нашей игрой. Всё просто.
Теперь, давайте посмотрим на SDL_Event и SDL_PollEvent. Первое, это структура, которая содержит информацию о сообщениях. Второе — функция, которая будет выбирать сообщения из очереди сообщений. Очередь может содержать любое количество сообщений, именно по этой причине мы и используем цикл чтобы перебрать их все. Так, например, предположим, что пользователь нажимает и перемещает мышь во время выполнения функции OnRender. SDL обнаружит это и поместит два события в очереди, одно на клавишу и одно на перемещение мыши. Мы можем выбрать это сообщение из очереди с помощью SDL_PollEvent, а затем передать его в метод OnEvent в котором его соответствующим образом нужно обработать. Когда событий в сообщений нет, SDL_PollEvent вернет false, таким образом, выходя из цикла обработки очереди сообщений.
Далее мы рассмотрим переменную Running. Она контролирует выход из основного цикла игры. Когда этот параметр станет false, мы будем завершать программу, и передавать управление функции выхода из программы. Так, например, если пользователь нажимает клавишу ESC, мы можем установить эту переменную в false, тем самым обозначая выход из игры.
На данном этапе вы можете попробовать скомпилировать проект, но поскольку у нас нет никакой обработки сообщений, вам скорее всего придется использовать подручные средства для завершения программы.
Теперь, когда все настроено, давайте начнем с создания окна нашей игры, которое и будет отображаться. Откройте CApp.h и добавьте переменную типа SDL_Surface. Всё должно выглядеть так:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL.h>
class CApp {
private:
bool Running;
SDL_Surface* Surf_Display;
public:
CApp();
int OnExecute();
public:
bool OnInit();
void OnEvent(SDL_Event* Event);
void OnLoop();
void OnRender();
void OnCleanup();
};
#endif
Я полагаю, сейчас самое время, чтобы объяснить что такое SDL_Surface. SDL_Surface это структура в которой мы отрисовываем кадр нашей игры и которую отображаем после отрисовки. Предположим, у нас есть доска, мелок, и куча стикеров, так вот SDL_Surface это наша «доска» (отображаемая поверхность) мы можем сделать с ней всё что угодно: наклеить на ней стикеры, нарисовать на ней. В свою очередь, стикеры тоже можно представить как SDL_Surface: мы можем рисовать на них и клеить другие стикеры поверх. Таким образом, Surf_Display это просто наша девственно чистая доска, на которой мы и будем рисовать и клеить стикеры.
Теперь, давайте откроем CApp_OnInit.cpp чтобы уже создать эту поверхность:
#include "CApp.h"
bool CApp::OnInit() {
if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
return false;
}
if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
return false;
}
return true;
}
Первое, что мы сделали — инициализировали SDL, так что теперь мы можем получить доступ к его функциям. Параметр SDL_INIT_EVERYTHING означает инициализацию всех подсистем библиотеки (существуют и другие параметры, например SDL_INIT_AUDIO,SDL_INIT_VIDEO, но пока мы не будем заострять на них внимание). Следующая функция, SDL_SetVideoMode, создает наше окно, и базовую поверхность. Она принимает 4 параметра: ширину окна, высоту окна, количество используемых бит (рекомендуется 16 или 32), и флаги отображения перечисляющиеся через оператор "|". Есть и другие флаги, но нам пока хватит и этих. Первый флаг указывает SDL использовать аппаратное ускорение, а второй — использовать двойную буферизацию (что немаловажно, если вы не хотите в итоге получить мерцающий экран). Другой флаг, который может заинтересовать вас сейчас — SDL_FULLSCREEN, он переключает окно в полноэкранный режим.
Теперь, когда инициализация пройдена, самое время позаботиться об очистке. Откройте CApp_OnCleanup.cpp и добавьте следующее:
#include "CApp.h"
void CApp::OnCleanup() {
SDL_Quit();
}
Так мы завершаем работу с SDL. Вы должны принять к сведению, что в этой функции освобождаются все поверхности и ресурсы, используемые игрой.
Рекомендуется также установить Surf_Display в NULL в конструкторе класса, во избежании неприятных моментов. Откройте CApp.cpp и добавьте следующее:
CApp::CApp() {
Surf_Display = NULL;
Running = true;
}
Попробуйте скомпилировать проект и посмотреть на его работу. Должно запуститься ваше первое, созданное с помощью SDL, окошко (правда пока пустое). Однако вы по прежнему не можете пользоваться внутренними функциями завершения работы программы и поэтому вам опять потребуются подручные средства.
Последний штрих — у нас есть созданное окно, а теперь нужен способ, чтобы закрыть его. Откройте CApp_OnEvent.cpp и добавьте следующее:
#include "CApp.h"
void CApp::OnEvent(SDL_Event* Event) {
if(Event->type == SDL_QUIT) {
Running = false;
}
}
Структура SDL_Event разбита на типы. Эти типы могут варьироваться от нажатия клавиш до движений мыши, а в данном примере как раз и происходит проверка типа события. В CApp_OnEvent.cpp мы ожидаем сообщение о закрытии (конкретно — когда пользователь нажмет крестик в заголовке нашего окна) и если это происходит, то переменной Running присваивается значение false, игра заканчивает свой главный цикл, окончен бал, погасли свечи. В следующих уроках мы рассмотрим обработку сообщений более подробно.
Вот и всё! Теперь у вас есть хороший каркас для создания игр. Даже круто было бы создать на основе нашего проекта шаблон для CB. Вот только рассказывать про то как это можно сделать не буду, не стесняйтесь немного погуглить (погуглил в справке: File->Save project as template...).
Если вы достаточно хорошо усвоили материал этого урока, переходите к следующему чтобы узнать больше о поверхностях.
P.S. Ещё один момент, после вопроса товарища Fedcomp мне тоже стало любопытно почему же автор использует классы для создания экземпляра игры?
Мое мнение на этот счет: возможность изменения разрешения без перезапуска игры, ведь если мыслить логически, нам потребуется только переинициализировать поверхность без необходимости заново загружать ресурсы. Ну или схожая задача.
Хотелось бы увидеть в комментариях размышления по этому поводу. Спасибо всем, кто дочитал до конца!
Автор: m0sk1t