SDL: Как создать игру. Базовое окно.
Это вольный перевод и переосмысление статьи, SDL Tutorial Basic за авторством Tim Jones. Хочу выделить, что это не прямой перевод, код не полностью повторяет код оригинала, тем не менее вы можете сверяться и сравнивать с оригинальной статьей. Готовый код я буду выкладывать на Github, откуда вы сможете скачать так же ZIP-архив. Если вы увидите ошибку, неточность, или у вас возникнут проблемы - обращайтесь. Вы можете оставить комментарий или написать мне на почту, она указана внизу страницы. Итак начнем.
Эти обучающие статьи предназначены для тех, у кого уже есть опыт использования C++ или другого языка программирования. Если у вас возникают сложности с пониманием кода, а не концепций, то я настоятельно советую вам сначала прочитать статью, объясняющую основы языка С++. Необязательно знать все о С++, но чем больше - тем проще вам будет в дальнейшем.
В статьях я буду использовать Code::Blocks в качестве IDE и gcc с mingw для компиляции. [Я лично изначально использовал gcc с cygwin, но такой набор потребует включения cygwin1.dll в дистрибутив, что может накладывать лицензионные ограничения. Поэтому позже я установил mingw компилятор из тех же репозиториев cygwin.] Если вы хотите использовать другую IDE и/или компилятор, вы, конечно, можете это сделать, если достаточно уверены, что сможете включить все библиотеки в линковку. Скачать же Code::Blocks можно на официальном сайте http://www.codeblocks.org (скачать версию, включающую MinGW). Рекомендуется использовать стабильную версию, если вы не хотите тратить время на ночные сборки.
Эти статьи в значительной степени сосредоточены вокруг SDL (Simple DirectMedia Layer), 2D кросс-платформенной графической библиотеки. Эта библиотека позволит отрисовывать графику, и делать разные классные вещи, которые потом и помогут нам сделать игру. Вам также надо будет загрузить библиотеку с сайта http://www.libsdl.org. Вам нужен tar-файл в разделе "Development Libraries, Win32" и zip-файл в разделе "Runtime Libraries, Win32". Если вы используете Visual Studio - выберите соответствующий файл вместо MinGW версии. После скачивания вы можете скопировать .dll файлы в папку Windows/system32, таким образом любое SDL приложение сможет его использовать. [Я не стал этого делать, достаточно чтобы SDL.dll и другие необходимые dll файлы находились в той же папке, что и исполняемый файл].
Начните новый проект (Blank project) в Code::Blocks, назовите по желанию и сохраните в удобное для вас место. Откройте tar-файл (который вы скачали из раздела "Development Libraries"), из него нам нужны папки include и lib, распакуйте их в папку вашего проекта. Вернемся обратно в Code::Blocks, кликните правой кнопкой на проект, слева в списке проектов, и в появившемся меню выберите "Build Options". Слева в дереве скорее всего будет имя вашего проекта и два пункта: Debug и Release. Выберите корень дерева, таким образом мы установим настройки сразу для всего проекта. Нас интересуют две вкладки: Search Directories и Linker Settings. В Search Directories по умолчанию выбрана вкладка Compiler, снизу нажмите Add и добавьте 2 папки: include и include/SDL. Этим мы облегчили подключение заголовочных файлов. Теперь во вкладке Linker Settings также нажимаем Add и добавляем файлы: libSDLmain.a и libSDL.dll.a (!Именно в таком порядке!). Этим мы подключили библиотеки SDL в наш проект. Нажимайте ОК и настройка проекта на этом закончена. [Если в будущем у вас возникнут проблемы с '_WinMain@16', то в Linker Settings, справа от библиотек в окне Other linker options допишите -lmingw32. Если возникнет проблема с libgcc_s_dw2-1.dll, допишите там же -static.]
Давайте, наконец, создадим два файла, назовем их App.h и App.cpp, это будет ядром нашей программы. Откройте App.h и добавьте туда следующий код:
#ifndef _APP_H_
#define _APP_H_
#include <SDL.h>
class App {
public:
App();
int Execute();
};
#endif
Теперь в файл App.cpp добавим вот этот код:
#include "App.h"
App::App() {}
int App::Execute() {
return 0;
}
int main(int argc, char* argv[]) {
App App;
return App.Execute();
}
Класс App - это основа всей нашей программы. Давайте сделаем шаг в сторону и я расскажу как игры обычно устроены. Большинство игр состоят из 5 функций, контролирующих игровой процесс. Обычно это:
Initialize Функция обрабатывает загрузку всех данных, текстур, карт, NPC, чего угодно.
Event Функция обрабатывает все поступающие события от мышки, клавиатуры, джойстика и других устройств.
Loop Функция обрабатывает обновление данных, например движение NPC по экрану, уменьшение здоровье персонажа и так далее.
Render Функция занимается отображением всего на экране. Она НЕ обрабатывает манипуляции с данными - этим занимается Loop.
Cleanup Функция просто отключает все использующиеся ресурсы и закрывает игру.
Важно понимать, что игра это просто один большой цикл. Внутри этого цикла случаются события, обновляются данные, рисуются картинки. Поэтому базовая структура игры может иметь следующий вид:
Initialize();
while(true) {
Events();
Loop();
Render();
}
Cleanup();
На каждом шаге цикла мы что-то делаем с данными и соответствующе их отображаем. События используются для манипуляции данными пользователем. Они в целом не являются необходимыми чтобы создать игру, но нужны, для возможности управлять игровым процессом.
Поясним эту мысль на примере. Например, у нас есть наш рыцарь, герой игры. Все, что мы хотим сделать, это просто его перемещать. Если я нажму влево - он пойдет налево. Сейчас мы выясним, как это сделать в цикле. Во-первых, мы знаем, что мы хотим, проверять события (события клавиатуры). Так как события нужны для управления данными, нам нужны будут переменные, которые будут изменяться. Тогда мы можем использовать эти переменные для отображения нашего рыцаря на экране. У нас могло бы быть что-нибудь навроде:
if(Key == LEFT) X--;
if(Key == RIGHT) X++;
if(Key == UP) Y--;
if(Key == DOWN) Y++;
RenderImage(KnightImage, X, Y);
Это работает, потому что каждый цикл проверяется, нажата ли кнопка ВЛЕВО, ВПРАВО и т.д., и если да, то уменьшить или увеличить переменную. Так что, если наша игра идет со скоростью 30 кадров в секунду и мы нажимаем НАЛЕВО, то рыцарь будет двигаться влево со скоростью 30 пикселей в секунду. Если вам еще не удалось разобраться в принципе игрового цикла, то вы скоро с этим справитесь. Цикл необходим, чтобы игра правильно функционировала.
Возвращаясь к нашему концептуальному коду (5 функций), мы можем добавить эти дополнительные функции в наш проект, в файле App.h:
cpp#ifndef _APP_H_
#define _APP_H_
#include <SDL.h>
class App {
private:
bool Running;
public:
App();
int Execute();
public:
bool Init();
void Event(SDL_Event* Event);
void Loop();
void Render();
void Cleanup();
};
#endif
А в файле App.cpp:
#include "App.h"
App::App() {
Running = true;
}
bool App::Init() {
return true;
}
int App::Execute() {
if(Init() == false) {
return -1;
}
SDL_Event Event;
while(Running) {
while(SDL_PollEvent(&Event)) {
Event(&Event);
}
Loop();
Render();
}
Cleanup();
return 0;
}
int main(int argc, char* argv[]) {
App App;
return App.Execute();
}
void App::Event(SDL_Event* Event) {}
void App::Loop() {}
void App::Render() {}
void App::Cleanup() {}
Вы видите несколько новых переменных, но давайте сначала разберемся что же делает этот код. Во-первых, мы пытаемся инициализировать нашу игру, если не вышло - мы возвращаем -1 (код ошибки), тем самым закрывая программу. Если все хорошо, то мы входим в игровой цикл. В цикла мы используем SDL_PollEvent для проверки событий и передаем их по одному в Event. После этого, мы идем в Loop для перемещения данных вокруг, и всего в таком роде, а затем отображаем нашу игру. Повторяем это бесконечно. Если пользователь выходит из игры, мы попадаем в Cleanup, которая освободит используемые ресурсы. Вот так, достаточно просто.
Теперь, давайте посмотрим на SDL_Event и SDL_PollEvent. Первое - это структура, которая содержит информацию о событиях. Вторая функция, которая будет захватывать любые события, ожидающие в очереди. Очередь может состоять из любого количества событий, поэтому мы должны перебрать их по очереди. Например, позволяет нажимает кнопку А и перемещает мышь, в тот момент, когда исполняется Render(). SDL обнаружит это и поставит два события в очередь, по одному на каждое нажатие клавиши и перемещение мыши. Мы получаем эти события из очереди с помощью SDL_PollEvent, а затем передаем его в Event, чтобы обработать соответствующим образом. Когда в очереди нет событий, SDL_PollEvent вернет false, выйдя из цикла обработки событий.
Другая добавленная переменная Running - это наш выход из цикла игры. Если оно false, значит игра закончилась, и надо выйти из программы. Так что, если, например, если пользователь нажимает клавишу ESC мы можем присвоить ей false, и тем самым выйти из игры.
На данном этапе программа должна компилироваться без проблем, правда, вы можете заметить, что ее нельзя закрыть. Даже, вероятно, придется использовать диспетчер задач для завершения программы.
Теперь, когда все настроено, давайте создадим окно, где будет отрисовываться наша игра. Откройте App.h и добавить переменную поверхности SDL:
#ifndef _APP_H_
#define _APP_H_
#include <SDL.h>
class App {
private:
bool Running;
SDL_Surface* Screen;
public:
App();
int Execute();
public:
bool Init();
void Event(SDL_Event* Event);
void Loop();
void Render();
void Cleanup();
};
#endif
Думаю, самое время объяснить что такое SDL поверхность. SDL поверхность это все, что можно нарисовать, и все на чем можно нарисовать. Например, у нас есть лист бумаги, карандаш, и стикеры. Бумаги можно назвать поверхностью. Мы можем рисовать на ней, наклеивать стикеры и прочее. Стикеры тоже можно считать поверхностями, мы можем рисовать и на них, и наклеивать другие стикеры поверх. Таким что, Screen - это просто наш "лист бумаги", на котором мы будем рисовать.
Теперь, давайте отредактируем функцию Init, чтобы инициализировать эту поверхность:
bool App::Init() {
if(SDL_Init(SDL_INIT_EVERYTHING) > 0) {
return false;
}
if((Screen = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
return false;
}
return true;
}
Первое, что нужно сделать, это запустить собственно SDL, чтобы мы могли получить доступ к его функциям. Мы командуем SDL инициализировать все, что доступно; есть различные параметры, которые можно передать, но это на данный момент это не важно. Следующая функция: SDL_SetVideoMode. Она и создаст наше окно и поверхность. Она принимает 4 параметра: ширину окна, высота окна, битовое разрешение (рекомендуется 16 или 32), и различные флаги отображения. Есть довольно много флагов, но тех, которые мы используем, достаточно на данный момент. Первый флаг говорит SDL использовать аппаратную памяти для хранения изображений, а второй говорит SDL использовать двойную буферизацию (это предотвратит мерцание на экране). Еще один флаг, который может заинтересовать вас - это SDL_FULLSCREEN, он запускает приложение в полноэкранном режиме.
Теперь, когда окно настроено, давайте сделаем так, чтобы все работало гладко. Изменим функцию App::Cleanup() и добавим следующее:
void App::Cleanup() {
SDL_Quit();
}
Мы просто выходим из SDL. Надо учесть, что в этой функции, мы будем освобождать использованные поверхности
Чтобы все было аккуратно, надо также инициализировать указатель Screen значением NULL, в конструкторе класса. В App.cpp добавить следующее:
App::App() {
Screen = NULL;
Running = true;
}
Попробуйте скомпилировать проект, посмотрите как он работает. Должно появится пустое окно. Его все еще нельзя закрыть просто так, придется воспользоваться диспетчером задач. Это не очень удобно, поэтому давайте исправим ситуацию. Изменим функцию Event:
void App::Event(SDL_Event* Event) {
if(Event->type == SDL_QUIT) {
Running = false;
}
}
SDL события различаются по типу. Эти типы могут варьироваться от нажатия клавиш, движения мыши, все что нам нужно это проверить его тип. Нужный нам тип - это запрос на закрытие окна (например, когда пользователь нажимает кнопку X). Если это событие произойдет, мы устанавливаем Running = false, тем самым завершив нашу программу. Достаточно просто. Мы рассмотрим больше событий в других статьях.
Если вы хорошо разобрались и понимаете, как работает код в этой статье, вы можете перейти к следующей, чтобы узнать больше о поверхности SDL.