Супрунов Александр
Стрелялка за выходные
(Опубликовано в журнале Linux
Format, номера 98,100/101,103)- ЧАСТЬ 1 Скучаете по R-Type и River Raid и готовы сделать что-то сами, но не знаете, с чего начать? Александр Супрунов покажет путь, доступный даже новичку!
Признаюсь сразу – это самый странный урок из всех, что вас ожидают. Я, конечно, постараюсь скрасить его различными интересными отступлениями, но уж ничего не поделаешь – в нем вы начнете учиться с конца. В качестве положительного момента отмечу, что изложенных в нем сведений должно хватить для того, чтобы начать писать компьютерные игры для Linux и, таким образом, значительно увеличить их число в нашей любимой ОС. Мы будем использовать язык C/C++, но, чтобы следовать курсу, вам потребуются только базовые знания.
Основной игровой библиотекой в Linux, пожалуй, следует назвать SDL. Скажу больше – написав игру с использованием SDL, вы без каких-либо изменений сможете откомпилировать ее для Linux, Windows, Mac OS X, Amiga Pegasos и даже наладонников, а возможностей библиотеки хватило для портирования Heroes of Might and Magic 3. Я не сомневаюсь, что ваши планы более грандиозны, но SDL способна реализовать и их. В крайнем случае (для повышения быстродействия и скоростных операций масштабирования, вращения и прозрачности) всегда можно подключить OpenGL.
При всех достоинствах SDL – это конструктор, позволяющий собрать действительно удобный инструмент. Например, вместо следующего кода, выводящего спрайт на экран стандартными средствами SDL –
SDL_Rect shadow;
shadow.x=(Sint16)x;
shadow.y=(Sint16)y;
SDL_SetColorKey(img,SDL_SRCCOLORKEY,SDL_MapRGB(img->format,255,0,255));
SDL_BlitSurface(img,0,screen,&shadow);
мне кажется более логичным написать:
sprite(номер, x, y);
Если вам тоже импонирует эта мысль, читайте дальше – мы доведем SDL именно до такой степени. При этом исходный код ваших игр едва ли превысит 20–30 КБ и будет кристально понятным даже новичкам.
Мы поместим на игровое поле крошечный кораблик, храбро «выносящий» с экрана полчища врагов (будут ли ужасные БОССы и различные типы оружия – зависит от вас), реализуем многоплановый параллаксный скроллинг, эффекты прозрачности и другие интересные вещи. Подобный тип игр малораспространен на платформе Linux, так давайте разберем по шагам все моменты, которые требуются для создания игры и, следуя им, напишем новую!
Приготовим рабочее место
GNU/Linux – свободная, бесплатная (другими словами – доступная любому человеку) операционная система и обладающие теми же преимуществами средства разработки, входящие в состав любого популярного дистрибутива, а также текстовый редактор (Kate, vi, mcedit – по вкусу). Помимо этого, необходимы библиотеки (в том числе и версии для разработчиков с суффиксом «devel») SDL (для графики), SDL_mixer (для звука) и SDL_ttf (для вывода сообщений) – их можно установить через менеджер пакетов вашей системы.
Взгляните на врезку Что нам потребуется? и убедитесь, что указанные в ней компоненты присутствуют и готовы к работе. Я специально не стал упоминать популярные IDE типа KDevelop – при всем своем удобстве они достаточно громоздки и скрывают суть происходящих процессов. Для компиляции нашего кода потребуется написать лишь крошечный make-файл (назовите его Makefile).
TARGET = ingame.run
CFLAGS= `sdl-config --cflags`
LIBS = `sdl-config --libs`
-lSDL_ttf -lSDL_mixer
CC=g++
all:
$(CC) -o $(TARGET) starfighter.cpp $(LIBS)
strip $(TARGET)
./$(TARGET)&
Переменная TARGET задает имя исполняемого файла, который будет получен в результате компиляции. CFLAGS содержит флаги, необходимые любому SDL-приложению – в данном случае мы получаем их командой sdl-config. В поле LIBS указываются требуемые библиотеки. Если понадобиться добавить еще одну, например, SDL_image, следует просто дописать -lимя_библиотеки в конце этой строки. Переменная CC содержит команду для вызова компилятора C++.
Строки, следующие за all: – это те самые правила, по которым будет происходить сборка. Думаю, вы уже догадались, как интерпретировать строку $(CC) -o $(TARGET) starfighter.cpp $(LIBS)
а если нет, сделаю подсказку – $(var) подставляет в строку значение переменной var. Что же касается необязательной команды strip $(TARGET), то она очищает получившийся исполняемый файл от ненужной служебной информации. Наконец, последняя строчка
./$(TARGET)&
после каждой компиляции запускает игру на выполнение, чтобы вы могли видеть результат. Ее можно удалить, но на мой взгляд, это удобно.
Общий цикл разработки нашей игры теперь будет выглядеть следующим образом:
- Открыть текстовый редактор и набрать код программы.
- Запустить терминал, перейти в каталог с игрой и набрать make.
- Оценить результат.
- Завершить программу, нажав клавишу Escape.
- Перейти к пункту 1.
...и так до тех пор, пока оценка, выставленная в пункте 3, не достигнет «хорошо» или «отлично».
Приступим к делу
Ну-с, теперь мы готовы начать программирование. И в первую очередь следует подключить файл ingame.h – он содержит все основные функции, которые потребуются нам при написании игры (см. «Основные функции»). Простейшая программа, не создающая ничего, помимо пустого окна (но уже умеющая вычислять FPS и реагировать на клавишу Escape) будет иметь следующий вид:
-
#include "ingame.h"
-
-
int main(int n, char **s)
-
{
-
screen(1024, 768);
-
while (GAME)
-
{
-
fx();
-
}
-
return 0;
-
}
Давайте разберемся, что здесь происходит. Функция screen(), вызванная в строке 5, устанавливает полноэкранный режим 1024x768 (кстати, как мы вскоре увидим, здесь можно указать и любое другое значение). В строке 6 запускается главный цикл игры, обрабатывающий события, поступающие от пользователя. Этим занимается вспомогательная функция fx(), определенная в ingame.h – она отслеживает нажатие на клавиши, вычисляет значение FPS и так далее. Если пользователь нажимает Escape, функция fx() сбрасывает флаг GAME возвращает управление, после чего главный цикл, а вместе с ним и наша программа, завершаются.
Разместим первый спрайт
Теперь немного усложним программу – заставим ее выводить на экран картинку, сохраненную в формате BMP. Изображение можно взять с LXFDVD или подготовить самостоятельно в графическом/трехмерном редакторе. Я, например, создал боевой космический истребитель в Blender. Учтите только, что для правильного отображения нашим движком, прозрачные участки картинки должны иметь цвет (255,0,255) в RGB-нотации.
#include “ingame.h”
int main(int n, char **s)
{
screen(500, 700);
loadsprite(1, “ship.bmp”);
while (GAME)
{
sprite(1,250,650);
fx();
}
return 0;
}
По сравнению с предыдущим случаем добавился вызов функции loadsprite(), загружающей спрайт ship.bmp в память и присваивающей ему номер 1. Это число может изменяться в пределах от 0 до 999 – более высокие значения зарезервированы ingame.h для служебных целей. При этом количество копий каждого спрайта на экране практически неограничено. Спрайт выводится на экран функцией sprite(), которой следует передать номер и координаты точки, в которой осуществляется отображение. Для вашего удобства в ingame.h уже определены такие переменные, как x, y, x2 и y2, поэтому вы можете просто использовать их в коде. Например, если вы измените
sprite(1,250,650);
на
x=250;
y=650;
...
sprite(1,x,y);
все будет работать, как раньше.
Итак, с выводом одной картинки мы разобрались. Программирование игр – не жонглирование, и обращаться с несколькими спрайтами ненамного сложнее:
screen(800,600);
loadsprite(1,”luna.png”);
loadsprite(2,”ship.png”);
loadsprite(3,”fire.png”);
while (GAME)
{
sprite(1, 150 , 0);
sprite(3, 250 , 300);
sprite(2, 350 , 500);
fx();
}
С назначением переменных UP (вверх), DOWN (вниз) и FIRE (огонь) вы теперь можете разобраться самостоятельно.
Ключ на старт!
Чтобы находящийся на экране неподвижный корабль начал движение, в программу необходимо добавить всего две строчки. Вот так:
screen(500, 700);
loadsprite(1, “ship.bmp”);
x=250;
y=650;
while (GAME)
{
sprite(1,x,y);
if (LEFT) {x=x-2;}
if (RIGHT) {x=x+2;}
fx();
}
Чем больше будет изменение координаты, тем быстрее будет перемещаться корабль по экрану. И наооборот, если приращение координаты установить равным 0,1 или 0,01, ваш скоростной истребитель будет ползти, как старый имперский сухогруз.
Что дальше?
Прошло каких-то несколько минут, а вы уже имеете работающую демо-версию новой игры. Есть что показать друзьям, поэтому самое время решительно сказать: «Стоп!». Все должно развиваться по плану.
Начнем с уже озвученной фабулы. Мы управляем небольшим космическим истребителем. Цель: уничтожить враждебных пришельцев, основавших цитадель на Обратной стороне Луны. Соответственно, до этой самой Луны необходимо добраться, поэтому игровой процесс целесообразно разделить на 3 этапа:
- Первый – добраться до Луны через открытый космос. Этому, очевидно, будут мешать вражеские перехватчики.
- Второй этап происходит над Лунной поверхностью. Это повод реализовать фантастический многоплановый паралаксный cкролинг! Нашими врагами будут все те же истребители, а также пушки, вмонтированные в скалы.
- Третий этап – битва с БОССом. В роли «великого и ужасного» будет выступать Гигантский корабль-матка.
- Конец. Враг разгромлен, но одна шлюпка выскользнула за пределы оцепления и скрылась в звездных просторах. Продолжение следует?
Конечно, вы вольны придумать свою сюжетную линию, но главное здесь – увидеть примерную структуру игры. И могу вас заверить – если все сделать грамотно, то играть будет весьма увлекательно. А что самое важное в космических стрелялках? Конечно же, красивые взрывы, поэтому на красочных эффектах тоже экономить не стоит.
Все это здорово, но мы упустили одну маленькую деталь. Чтобы создать по-настоящему высококлассную игру, необходимо придумать изюминку, свойственную только ей. Поэтому сидите и думайте, изредка поглядывая в зеркало. Как только размер головы станет чуть больше перезревшего арбуза – бегите к друзьям и продолжайте мозговой штурм. И тогда...
Не забывайте и о факторе внезапности, а так же физических законах. Где-то на полпути к Луне кораблю может повстречаться метеоритный поток. Некоторые метеориты будут настолько велики, что окажутся способны своим импульсом изменить направление движения истребителя. Но именно в этом астероидном поле устроили засаду мириады вражеских перехватчиков – земному пилоту придется ой как не сладко! Однако всему этому придется подождать до следующего выпуска «Игростроя»... LXF
Основные функции
В файле ingame.h определен ряд функций, делающих программирование игр простым и понятным даже для новичков. Все они являются обертками над соответствующими функциями библиотеки SDL.
- screen (ширина экрана , высота экрана) Устанавливает экранное разрешение.
Пример: Вызов screen(640,480) переключит монитор в режим 640x480.
- loadsprite (номер ячейки , “название файла”) Загружает спрайт (рисунок) в память и связывает его с указанными номером.
Пример: loadsprite(5, “cat.bmp”) загружает файл cat.bmp с изображением кота и ассоциирует его с номером 5.
- sprite (номер ячейки, x, y, [a]) Отображает ранее загруженный в память спрайт на экране. Функция может быть вызвана только после screen().
Пример: sprite (5, 100,150) разместит кота на экран в точке с координатами x=100, y=150: Четвертый необязательный параметр регулирует прозрачность спрайта и меняется в пределах от 0 (непрозрачный) до 255 (полностью прозрачный).
- colorfon(R, G, B); Закрашивает фон экрана произвольным цветом. R,G,B – значения его красной, зеленой и синей составляющей, соответственно.
- loadmusic (название файла, номер ячейки с музыкой); Загружает в память музыку в формате MID, MOD, XM, IT, WAV и т.д.
Пример: loadmusic (“sintez.mod”, 5);
- music (номер ячейки с музыкой); Проигрывает ранее загруженный музыкальный ролик.
Пример: music (5);
- loadsound (название файла, номер ячейки со звуком); Загружает в память звук в формате WAV.
Пример: loadsound (“boom.wav”,1);
- sound (номер ячейки со звуком); Проигрывает ранее загруженный звук.
Пример: sound (1);
- box(объект1, координата Х объекта1, Y объекта1, объект2, координата Х объекта2, координата Y объекта2, );
Простая проверка столкновения объекта1 и объекта2 (учитывается только пересечение прямоугольников, ограничивающих спрайт). Пример:
if (box(1, bomb_x, bomb_y, 2, ship_x, ship_y) ) {
sound(“boom.wav”); }
- fx(); Основная и обязательная функция. Обрабатывает события, поступающие от пользователя и формирует изображение на экране вашего монитора.
Пример: while (GAME) { fx(); }
Предопределенные переменные
В ingame.h определен целый ряд переменных, которые с большой вероятностью понадобятся вам при разработке своей игры.
Следует отметить, что движок работает с числами с плавающей точкой. Это значит, что мы можем перемещать спрайт не только на целое число пикселей (скажем, 1), но и на дробное – например 0.02. Это дает возможность выводить очень медленно перемещающиеся спрайты (например, далекие облака).
Список предопределенных переменных:
- x, y Координаты спрайта первого игрока (тип float)
- x2, y2 Координаты спрайта второго игрока (тип float)
- LEFT, RIGHT, UP, DOWN Флаг, равный 1, если нажата клавиша «стрелка влево/вправо/вверх/вниз» или «огонь» (пробел), соответственно, и 0 в противном случае
- GAME Флаг, сбрасываемый в 0, если пользователь нажал клавишу Escape
Исходные тексты
ЧАСТЬ 2
- Сегодня Александр Супрунов расскажет о столкновении объектов, анимации, звуке фоновом и нефоновом – в общем, обо всем том, без чего немыслима красивая игра. Время завершать начатое!
Летел корабль в пустоте. Вдруг – трах, бах!!! Шальной метеорит врезался в борт. Вот это безобразие и называется коллизией. А представьте, если бы ее не было! И метеорит бы мимо пролетел, и... Впрочем, и мы бы провалились сквозь землю. И если в реальной жизни коллизии (столкновения объектов) происходят сплошь и рядом, то и в играх о них нельзя забывать, а значит, потребуется написать хорошую функцию, способную их отследить. Конечно, такая функция в нашей библиотеке уже есть – называется она box(). Вовсе, кстати, не потому, что похожа на одноименный вид спорта (и Патрик, хоть он и..., здесь тоже ни при чем), а из-за метода определения столкновения: берется квадрат (бокс) спрайта и проверяется, не пересекается ли он с таким же квадратом другого спрайта. Данная процедура называется «проверка по боксу». Еще бывает попиксельная, но это уж на крайний случай – слишком много она потребляет ресурсов. Синтаксис вызова box() таков:
box(номер первого спрайта, x-координата первого спрайта, y-координата первого спрайта, номер второго спрайта, x-координата второго спрайта, y-координата второго спрайта);
Обратите внимание, что вы можете выводить на экран надписи и цифры с помощью функции print().
Функция возвращает 1, если столкновение произошло и 0 – в противном случае. Вот как можно применить ее на практике:
#include “ingame.h”
int main(int n, char **s)
{
int asteroid_x=100;
int asteroid_y=150;
x=250; y=650;
screen(500, 700);
loadsprite(1, “ship.bmp”);
loadsprite(2, “asteroid.bmp”);
while (GAME) {
sprite(1,x,y);
sprite(2, asteroid_x, asteroid_y);
if (LEFT){x=x-2;}
if (RIGHT){x=x+2;}
if (UP){y=y-2;}
if (DOWN){y=y+2;}
if (box (1, x, y, 2, asteroid_x, asteroid_y)) {
print (“BOOM”,200,300);
}
fx();
}
return 0;
}
Здесь мы устанавливаем разрешение 500x700 (как вы, надеюсь, помните, вызов screen() должен быть первым в вашей программе), загружаем спрайты ship.bmp (корабль) и asteriod.bmp (астероид) в слоты 1 и 2, соответственно, затем, в цикле, отрисовываем их на экране, позволяя изменять координаты корабля клавишами управления курсором, и печатаем громкое “BOOM”, если столкновение имеет место. Выход из игры, как и раньше, происходит по нажатию Esc. Напомню, что переменные x и y – встроенные, определять их заново не нужно. Результат работы программы можно видеть на рисунке.
Если вы пытались уклониться от астероида чересчур активно (он, конечно, неподвижен, но, как утверждал сперва Галилей, а потом Эйнштейн, все относительно), то не могли не заметить, что наша игра обладает одним маленьким недостатком – корабль легко исчезает за пределы видимости. Так происходит потому, что со временем значения координат x и y становится слишком большими и выходят за рамки отведенных 500 х 700 пикселей. Чтобы исправить ошибку, можно, например, добавить сразу после блока инструкций if следующий код:
if (x<=0){x=0;}
if (x>=500){x=500;}
if (y<=0){y=0;}
if (y>=700){y=700;}
Происки врагов
Если вам понадобится вывести на экране несколько одинаковых спрайтов, не вздумайте загружать их в разные слоты! Функция sprite() вполне справляется с отображением одной и той же картинки в точках с разными координатами.
Инопланетные захватчики – это не мирные астероиды, и у кораблей, которые будут нас атаковать, возможны различные траектории движения. Какие именно – зависит от вас. Например, большой бомбардировщик может медленно перемещаться по диагонали экрана, методично сбрасывая смертельно опасные, но неповоротливые бомбы, а легкие истребители будут выделывать петли. Иными словами, траектории движения удобно связать с типом противника.
Здесь, конечно, не помешает учебник математики, но особо усердствовать не стоит: если в знаменитой Galaga траектории достаточно сложны, это еще не значит, что существует прямая зависимость между их хитроумностью и увлекательностью игры. Попробуйте, для начала, ограничиться теми элементами, которые будут отвлекать вас на дополнительные действия. Например: вы увидели вражеский корабль и начинаете палить по нему из всех орудий. Если бы все было так просто, то секунду спустя его обломки уже затерялись бы в безбрежных просторах мирового эфира. Но вражеский корабль – крепкий орешек: он тоже делает выстрелы, и вам приходится уворачиваться от летящих снарядов, а тут еще этот шальной метеор, вынырнувший неизвестно откуда – и вот вам Game Over, которого никто не ждал. А сколько я там очков набрал? 4100? О, уже больше, чем в прошлый раз. А смогу больше? Собственно, это и называется «увлекательный геймплэй».
Приведем несколько стандартных траекторий движения вражеских объектов для игр, подобных нашей.
1 Сверху вниз по прямой:
sprite(10, evil_x, evil_y);
evil_y++;
2 По диагонали слева направо (или наоборот – замените в последней строке знак «плюс» на «минус»).
sprite(10, evil_x, evil_y);
evil_y++; evil_x++;
Попробуйте также изменять приращения evil_y и evil_x (например: evil_y+=3;). Интересной траектории можно добиться, добавив сюда фактор случайности (evil_x+=rand() %3;), а также внезапную смену направления движения.
3 Движение по кругу. Это более сложный случай, но, в целом, формулы для координат имеют следующий вид: x = rcos(α) + x0, y = rsin(α) + y0, где r – радиус окружности, α – угол в радианах. Для использования тригонометрических функций (cos(), sin()) необходимо подключить заголовочный файл math.h и задействовать библиотеку libm (добавьте -lm к командной строке gcc).
Попробуйте угадать, как будет двигаться объект в этом примере:
float evil_x=300;
float evil_y=0;
float radius=150;
float a;
while (GAME) {
sprite(1, radius*sin(a)+evil_x,radius*cos(a)+evil_y);
a=a+0.01;
if (a>=3.14){a=0.;}
evil_y+=0.5;
fx();
}
Вражеский пилот явно ас: его корабль движется практически случайным образом. Да, поразить такую цель будет непросто... А если вас смущает загадочное число 3,14 – то это просто примерное значение «пи» или число радиан, соответствующее 180 градусам. Обратите внимание на то, что увеличивать координаты можно не только на 1, 2 и так далее, но и на дробные числа – например на 0,5, как в этом примере.
Что за штука... велосити?
Велосити (англ. velocity – скорость) – вероятно, один из самых чудесных способов сделать управление более реалистичным. Что мы имеем сейчас? Нажимаем клавишу – корабль движется, отпускаем – останавливается. Но в реальном мире тела обладают инерционностью, и, как с детства учат нас правила безопасного поведения на дорогах, ни одна машина не может затормозить мгновенно. Это тем более верно для космического корабля, на который не действуют силы трения, но... мы отвлеклись.
Реализовать такое поведение в игре достаточно просто. В момент нажатия клавиши в функции изменения координат необходимо увеличить и значение велосити, а при отпускании клавиши – уменьшить его. Например, так:
int dir=1;
if (LEFT){velocity=4.5; dir=1; x-=3;}
if (RIGHT){velocity=4.5; dir=-1; x+=3;}
if (velocity>0) {
velocity-=0.1;
} else {
velocity=0;dir=0;
}
x=x-velocity*dir;
Переменная dir просто задает направление движения: +1 – налево, -1 – направо.
Обратите внимание на последнюю строчку – именно она обеспечивает «тормозной путь» после отпускания клавиши, а заодно и меняет знак приращения в зависимости от значения dir. Физическая модель заключена в блоке if () {...} else {}: если мы не поддерживаем нужную скорость, удерживая клавишу нажатой, она (за счет действия сил трения, надо думать) постепенно уменьшается. Поскольку трение в космосе не слишком велико, коэффициент затухания можно выбрать малым – подберите его опытным путем, чтобы играть было интересно.
Оживляем персонажей
Анимация – один из ключевых аспектов, на который нужно обратить самое пристальное внимание. Конечно, можно создать игру, где будут только статические персонажи – но ведь это же несерьезно!
Эффект анимации, как известно, создается последовательным отображением незначительно отличающихся друг от друга кадров. Таким образом мы можем украсить игру роскошными взрывами. Если вы внимательно читали предыдущие номера LXF, то надеюсь, не пропустили руководство по работе с 3D-редактором Blender – здесь он окажет вам неоценимую помощь.
Ниже представлен пример, демонстрирующий основы анимации. В данном случае ролик формируется из четырех кадров:
#include “ingame.h”
int main(int n, char **s) {
screen(640,480);
loadsprite (10,”01.png”);
loadsprite (11,”02.png”);
loadsprite (12,”03.png”);
loadsprite (13,”04.png”);
int i=10;
while (GAME){
sprite(i, 200 , 200);
i++;
if (i==14){i=10;}
fx();
}
return 0;
}
Кадры занимают слоты с 10 по 13. Эффект достигается циклической сменой номера текущего спрайта, хранящегося в переменной i. Но попробуйте запустить пример – и вы увидите одну неприятную деталь: кадры сменяют друг друга слишком быстро. Избавиться от нее можно одним способом – введя искусственную задержку. В простейшем случае это достигается так:
int anim=10;
int c=0;
while (GAME){
c++;
if (c==20){anim++; c=0;}
if (anim==14){anim=10;}
sprite(anim, 200 , 200);
fx();
}
Теперь номер текущего спрайта хранится в anim, а c – это счетчик для замедления. Кадр не меняется до тех пор, пока c не достигнет определенного значения – в нашем примере, 20. Изменяя данный порог, можно управлять скоростью анимации, но она будет аппаратнозависимой: при переходе на более мощный компьютер «человечки на экране начнут смешно махать руками» (если вы когда-либо играли в Диггера на 80386DX-2, вы меня поймете). Более правильным способом будет привязать задержку к абсолютному интервалу времени – скажем, менять кадр каждую 1/24 секунды. Этого можно добиться с помощью функции SDL_GetTicks(), возвращающей число миллисекунд, прошедших с момента инициализации библиотеки, и я оставляю данный вопрос вам на самостоятельное изучение.
Трели летнего утра
До сих пор наш игровой процесс протекал на равномерно закрашенном черном фоне. Для космической стрелялки это, может, и не плохо, но для большинства других игр не подходит. Поэтому в файле ingame. h определена функция colorfon(), принимающая три параметра: значения красной (R), зеленой (G) и синей (B) составляющей цвета фона. Подсмотреть значения RGB для интересующего вас цвета можно в палитре любого графического редактора, будь то KPaint или GIMP.
Если в качестве фонового рисунка разместить полупрозрачный спрайт, совпадающий по размерам с окном игры, то, динамически меняя цвет фона, можно добиться интересных эффектов: вечерней зари или рассвета. Главное здесь – ваша фантазия. Видели голубую пыль в космическом облаке в StarFighter на LXFDVD? Она получается именно так.
Не меньшее значение, чем красивый фон, имеет и звуковое оформление. Для этих целей предусмотрена функция loadsound(имя_файла, номер_слота), загружающая любой звук в формате wav в один из 500 доступных слотов. Воспроизвести звук можно функцией sound(), принимающей единственный аргумент – номер слота. Давайте добавим к игре рев идущих на форсаж моторов и грохот выстрелов (кстати, а вы знали, что в безвоздушном пространстве звук не передается? Да? Ну тогда считайте все это «литературным приемом»).
#include “ingame.h”
int main(int n, char **s)
{
screen(1024, 768);
loadsound(“boom.wav”,15)
while (GAME) {
if (LEFT) {sound(15);}
fx();
}
return 0;
}
Подобным образом вы можете озвучить все действия в игре. А как же обстоит дело с фоновой музыкой? И на этом фронте у нас все в порядке. Функция loadmusic(название_файла, номер_слота) загружает мелодию в память. Поддерживаются форматы mid, mod, xm, it, s3m, wav и другие. Функция music(номер_слота) воспроизводит загруженную ранее мелодию, но имейте в виду – вызывать ее необходимо вне главного цикла, примерно так:
#include “ingame.h”
int main(int n, char **s)
{
screen(1024, 768);
loadmusic(“level1.mod”,1)
music(1);
while (GAME) {
fx();
}
return 0;
}
Все вместе
Вот мы и подошли к тому моменту, когда осталось только одно – сесть и написать игру. Никакие отговорки, как вы понимаете, теперь не помогут. В ваших силах создать программу с отличным звуком и графикой. Файл ingame.h, который мы использовали во всех наших примерах, был написана автором специально для статей этой серии и распространяется на условиях GPLv2. Все входящие в него функции абсолютно прозрачны и доступны для редактирования и изменения.
В ingame.h не предусмотрена обработка ошибок. Это сделано по следующим причинам: во-первых, дополнительные проверки сделали бы код более громоздким и менее понятным, что для обучающего материала неприемлемо. На мой взгляд, проще потом внести пару собственных строчек. Во-вторых, игровая программа не сможет быть запущена, если какие-либо файлы, входящие в комплект, повреждены или отсутствуют. Но, положив руку на сердце, скажите, действительно ли вы хотите, чтобы кто-то запустил вашу игру с поврежденной или недостающей графикой?
Я также могу предположить, что у особенно заинтересовавшегося читателя возникнет желание разобрать файл ingame.h на кусочки, дабы понять, как же все устроено. Тогда вам придется углубиться в таинства библиотеки SDL – а это настолько же большая тема, насколько коротка наша серия. И сейчас, думаю, пришло время подумать о создании игр и других вещах: о башмаках и сургуче, капусте, королях, и почему, как суп в котле, кипит вода в морях. Да, не забывайте присылать ссылки на сделанные вами игры на letters@linuxformat.ru – возможно, мы даже разместим наиболее удачные экземпляры на одном из LXFDVD!
Наш движок изнутри
- ЧАСТЬ 3
- Пользоваться инструментом, не понимая, как он работает – не дело для настоящего линуксоида. Разберитесь в механике Ingame вместе с Александром Супруновым – а заодно познакомьтесь с основами SDL.
Движок Ingame, которым мы пользовались на протяжении двух последних уроков, является набором оберток над функциями библиотеки Simple DirectMedia Layer (SDL), доступной для Linux, Windows, Mac OS X и множества других систем, включая даже AmigaOS. И сегодня мы попробуем разобраться в том, что происходило все это время «за кулисами».
SDL написана на языке C, поэтому все объекты, которыми она оперирует, представлены в виде структур. Имена этих структур начинаются с префикса SDL_, а центральное место среди них занимает SDL_Surface – это так называемая «экранная поверхность», на которой можно размещать изображения. Структура SDL_Surface имеет поля w и h, задающие высоту и ширину поверхности, а также поле format. Указатель на основной игровой экран, определенный в файле ingame.h, имеет тип SDL_Surface * и имя display.
Начало всех начал
Как вы уже знаете, каждая программа, использующая Ingame, начинается с вызова функции screen(). Ее прототип выглядит так:
void screen(int w, int h);
В первую очередь, screen() выполняет инициализацию SDL посредством вызова:
SDL_Init (SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO);
где объединенные при помощи операции «логическое ИЛИ» флаги имеют следующий смысл:
- SDL_INIT_VIDEO – активировать возможность работы с графикой.
- SDL_INIT_TIMER – активировать возможность использования встроенного в SDL таймера.
- SDL_INIT_AUDIO – активировать возможность проигрывания звуковых данных.
Затем инициализируется поверхность display. Это делается следующим образом:
display = SDL_SetVideoMode (w, h, 0, SDL_SWSURFACE | SDL_ANYFORMAT);
Функция SDL_SetVideoMode() принимает в качестве параметров ширину, высоту экрана, количество бит на пиксель, а также различные ключи, в том числе:
- SDL_SWSURFACE – предписывает использовать программную отрисовку графики. Режим нагружает центральный процессор, но совместим практически со всеми компьютерами.
- Его антипод, SDL_HWSURFACE, задействует аппаратное ускорение, что может как повысить, так и понизить быстродействие конечной программы.
- SDL_DOUBLEBUF – включает режим двойной буферизации, позволяет получить более «плавную» графику.
- SDL_FULLSCREEN – активирует полноэкранный режим.
Прошу обратить внимание на третий параметр: количество бит на пиксель. Он может принимать значения 8, 16, 24, 32 или 0, что соответствует глубине цвета, установленной в системе по умолчанию. Последний вариант наиболее переносим. Однажды я наблюдал повреждение графики в SDL-игре, запущенной на Amiga Pegasos. Выяснилось, что причиной была жестко зашитая глубина цвета (16) – замена ее на 0 решила проблему.
Следующим шагом мы устанавливаем заголовок окна:
SDL_WM_SetCaption («Linux GAMES», NULL);
а затем инициализируем библиотеку SDL_ttf
TTF_Init();
и загружаем шрифт, который будет использоваться в игре для вывода различных сообщений
fnt = TTF_OpenFont(«font.ttf», 20);
Функция TTF_OpenFont() принимает два аргумента (имя файла со шрифтом и размер в пунктах) и возвращает указатель на структуру TTF_Font, который мы сохраняем в глобальной переменной fnt.
С графикой разобрались; остался звук. За него отвечает библиотека SDL_mixer, которая инициализируется вызовом:
Mix_OpenAudio (44100, MIX_DEFAULT_FORMAT, 2, 2024);
Первый аргумент – частота дискретизации звука. Второй является стандартным ключом – просто запомните его. Далее указывается количество звуковых каналов (разумеется, «стерео») и размер буфера, отводимого под звуковые данные. Если вдруг в одно непрекрасное утро вы услышите, что звук начал «спотыкаться» – увеличьте последнее число, например, в два раза.
Две заключительных строки функции screen() –
frames = 0;
then = SDL_GetTicks();
имеют отношение к подсчету и ограничению FPS. Это необходимо для того, чтобы программа выполнялась с одинаковой скоростью на любых компьютерах. Мы инициализируем счетчик кадров frames и сохраняем текущее значение таймера в глобальной переменной then, имеющей тип Uint32.
Добавим игроков
Итак, экран готов – настало время загрузить спрайты. Для этих целей в ingame.h используется структура WiHi, содержащая указатель на соответствующий объект SDL_Surface.
По умолчанию резервируется место под 1900 объектов WiHi, причем все номера, начиная с 1000-го, используются движком для внутренних целей. При необходимости, можно увеличить число доступных спрайтов, просто изменив размерность массива.
Для загрузки спрайтов в формате BMP в Ingame предусмотрена функция loadsprite(), принимающая в качестве аргументов номер ячейки (num), в которую будет загружена картинка и имя файла (name). loadsprite() – обертка над двумя стандартными SDL-функциями: SDL_LoadBMP() и SDL_DisplayFormat().
В принципе, здесь можно ограничиться всего одним вызовом:
pic[num].tmp=SDL_LoadBMP(name);
но с точки зрения производительности рациональнее будет сразу же преобразовать спрайт в пиксельный формат дисплея (например, если оригинальная картинка использует 24 бита на пиксель, а на экране – всего 16, глубина цвета должна быть понижена). В итоге тело функции loadsprite() примет вид:
pic[num].tmp=SDL_DisplayFormat(SDL_LoadBMP(name));
Функция sprite(num, x, y), как вы, надеюсь, помните, выводит спрайт с номером num в точке с координатами (x,y). Происходит это следующим образом: для изображения устанавливается «цветовой ключ» (значение RGB, которое SDL будет считать прозрачным), а затем спрайт просто переносится в нужную точку экрана.
За прозрачность спрайта «отвечает» функция SDL_SetColorKey():
SDL_SetColorKey(pic[num].tmp,SDL_SRCCOLORKEY | SDL_RLEACCEL,
SDL_MapRGB(pic[num].tmp->format,255,0,255));
Флаг SDL_SRCCOLORKEY указывает, что «цветовой ключ» (последний аргумент функции) следует считать прозрачным, SDL_RLEACCEL включает RLE-оптимизацию (при этом группы одинаковых пикселей кодируются по принципу число_пикселей X значение, что ускоряет копирование), а вызов SDL_MapRGB() возвращает значение «ключа» в требуемом формате (напомню, что Ingame считает прозрачными пиксели цвета (255,0,255).
Собственно копирование спрайта осуществляется функцией SDL_BlitSurface():
SDL_BlitSurface(pic[num].tmp,0,display,&shadow);
Первый аргумент (pic[num]. tmp) – исходная поверхность, второй – область исходного изображения, подлежащая копированию (мы передаем здесь NULL, что означает «вся поверхность»). Остальные два параметра имеют тот же смысл для целевой поверхности. shadow – переменная типа SDL_Rect, представляющая собой прямоугольник; координаты левого верхнего угла которого как раз равны x и y.
Как сказать: «Game over»?
Помимо спрайтов, на экране время от времени нужно отображать и текстовые сообщения. Для этих целей в Ingame предназанчена функция print(сообщение, координата_x, координата_y). Она начинается с определения двух переменных
SDL_Color color = {255,255,255,0};
SDL_Rect dest= {(Sint16)x, (Sint16)y,0,0};
Первая из них, типа SDL_Color, задает цвет символов (белый), а вторая определяет точку, в которой будет выводиться сообщение. Текст надписи растеризуется на служебной поверхности (помните, все спрайт-слоты выше 1000 заняты Ingame для внутренних нужд) функцией TTF_RenderText_Blended():
pic[1000].tmp = TTF_RenderText_Blended(fnt, txt, color);
Назначение ее аргументов, думаю, должно быть ясно из их имен. Можно также использовать более скоростной вариант – TTF_RenderText_Solid(), но он проигрывает _Blended() по красоте вывода.
Остается только скопировать сообщение на экран при помощи уже известной нам функции SDL_BlitSurface():
SDL_BlitSurface( pic[1000].tmp, NULL, display, &dest );
и освободить память:
SDL_FreeSurface( pic[1000].tmp );
Движущая сила
Итак, библиотека инициализирована, спрайты отрисованы; настало время заставить их двигаться. За это и многое другое отвечает функция fx(), реализующая основной цикл игры. Она требует предварительного объявления ряда глобальных переменных:
SDL_Event event;
Uint8* keys;
event – специальная переменная событийного типа (зачем она нужна, будет ясно ниже), а keys содержит номера нажатых клавиш.
Мы также вводим ограничение FPS 75-ю кадрами в секунду.
#define FPS_LIMIT 75
Функция fx() отслеживает нажатие клавиш, ограничивает количество кадров, выводимых на экран монитора, очищает его перед отрисовкой очередной сцены, организует механизм «велосити» для движения объектов и меняет местами основной и теневой экраны, то есть реализует двойную буферизацию изображения. Разберем эти действия по шагам.
fx() начинается с опроса SDL на предмет произошедших событий. Этим занимается функция SDL_PollEvent(), которая принимает указатель на переменную событийного типа и возвращает TRUE, если что-то произошло. После этого мы можем изучить поле event.type, чтобы понять, что именно случилось, и отреагировать надлежащим образом:
while (SDL_PollEvent(&event))
{
if (event.type==SDL_QUIT)
{
GAME=0;
SDL_Quit();
}
if (event.type==SDL_KEYDOWN)
{
if(event.key.keysym.sym==SDLK_ESCAPE)
{
GAME=0;
SDL_Quit();
}
}
}
В первую очередь обрабатываются события, сигнализирующие о необходимости выхода из игры: системное SDL_QUIT (оно генерируется, если, например, пользователь щелкнул кнопку закрытия окна) и нажатие на клавишу Escape (событие типа SDL_KEYDOWN, код клавиши при этом сохраняется SDL в поле event.key.keysym.sym). Доступные коды клавиш перечислены в файле SDL/SDL_keysym.h, там же определены и константы наподобие SDLK_ESCAPE. Кстати, можно реагировать не на нажатие, а на отпускание клавиши – такое событие будет иметь тип SDL_KEYUP.
Затем наступает черед обработки нажатия на управляющие клавиши: «вправо», «влево», «огонь» и т.п. Здесь можно было бы воспользоваться тем же приемом, что и для Escape, но есть одно «но»: пользователь должен иметь возможность нажать на несколько клавиш одновременно (скажем, «влево» и «вверх» или «вверх» и «огонь»). Для таких целей SDL предоставляет в наше распоряжение функцию SDL_GetKeyState(), которая возвращает указатель на массив значений типа Uint8, представляющий собой текущее состояние клавиатуры в целом. Исходя из того, что именно было нажато, мы устанавливаем переменные-флаги (LEFT, RIGHT и т.д.), а также изменяем значение «велосити».
keys=SDL_GetKeyState(NULL);
if(keys[SDLK_LEFT])
{
LEFT=1; vel=velocity;
}
else
LEFT=0;
Аналогично поступаем и с другими управляющими клавишами.
Далее в игру вступает механизм «велосити», который был достаточно подробно рассмотрен в LXF100/101, и, наконец, отрабатывает система ограничения FPS:
++frames;
now = SDL_GetTicks();
if ( now > then ) {
fps= (int)((double)frames*1000)/(now-then);
}
if ( fps > FPS_LIMIT ) {SDL_Delay(1000/FPS_LIMIT);}
Мы увеличиваем счетчик кадров, получаем текущее значение таймера и вычисляем FPS. Если полученный результат превышает заданный предел, функция SDL_Delay() приостанавливает работу программы на необходимый промежуток времени.
В качестве завершающего аккорда функция fx() меняет местами теневой и экранный буферы:
SDL_Flip(display);
делая видимым все, что было нарисовано на данной итерации цикла, и очищает вспомогательный экран, заливая его черным цветом при помощи непрямого вызова функции SDL_FillRect().
Музыка и звук
Работа со звуком в Ingame по своей сути похожа на работу со спрайтами. Здесь также определяются структуры SiHi и MiHi, являющиеся обертками для указателей на Mix_Chunk (звуковой эффект) и Mix_Music (музыкальная композиция), соответственно. Структуры объединяются в массивы, насчитывающие 500 и 100 элементов. Для загрузки эффекта из файла формата WAV функция loadsound() вызывает Mix_LoadWAV(), которая, в свою очередь, принимает имя WAV-файла в качестве единственного параметра. Для воспроизведения эффекта используется функция sound(), эквивалентная вызову:
Mix_PlayChannel (-1, sn[num].tmp, 0);
-1 является указанием использовать первый доступный канал, 0 – число повторений, означающее, что звук будет проигран один раз.
С музыкой все обстоит ненамного сложнее. Соответствующие функции являются обертками над Mix_LoadMUS() и Mix_PlayMusiс(). Интерес представляет лишь функция stopmusic(), вызывающая
Mix_FadeOutMusic(1000);
для плавного затухания звука в течение 1 секунды.
Что дальше?
Наш краткий экскурс в SDL подошел к концу. Конечно, мы не рассмотрели и десятой части возможностей этой замечательной библиотеки, но сделали главное – разобрались в том, как работает движок Ingame. Теперь, когда на нашей карте не осталось белых пятен, вы можете сами решать, куда двигаться дальше. Хотите – развивайте ingame.h: исходный код распространяется по лицензии GPL; хотите – напишите с его помощью собственную игру. Возможен так-же третий вариант – разберитесь в деталях с SDL и создайте что-нибудь с нуля. Главное, если у вас получится что-то стоящее – не забудьте сообщить нам об этом!