- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF100-101:Стрелялка
Материал из Linuxformat.
Стрелялка за выходные |
---|
|
Содержание |
Стрелялка за выходные
- ЧАСТЬ 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 (см. LXF87/88-LXF91) – здесь он окажет вам неоценимую помощь.
Ниже представлен пример, демонстрирующий основы анимации. В данном случае ролик формируется из четырех кадров:
#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. Все входящие в него функции (LXF98) абсолютно прозрачны и доступны для редактирования и изменения.
В ingame.h не предусмотрена обработка ошибок. Это сделано по следующим причинам: во-первых, дополнительные проверки сделали бы код более громоздким и менее понятным, что для обучающего материала неприемлемо. На мой взгляд, проще потом внести пару собственных строчек. Во-вторых, игровая программа не сможет быть запущена, если какие-либо файлы, входящие в комплект, повреждены или отсутствуют. Но, положив руку на сердце, скажите, действительно ли вы хотите, чтобы кто-то запустил вашу игру с поврежденной или недостающей графикой?
Я также могу предположить, что у особенно заинтересовавшегося читателя возникнет желание разобрать файл ingame.h на кусочки, дабы понять, как же все устроено. Тогда вам придется углубиться в таинства библиотеки SDL – а это настолько же большая тема, насколько коротка наша серия. И сейчас, думаю, пришло время подумать о создании игр и других вещах: о башмаках и сургуче, капусте, королях, и почему, как суп в котле, кипит вода в морях. Да, не забывайте присылать ссылки на сделанные вами игры на letters@linuxformat.ru – возможно, мы даже разместим наиболее удачные экземпляры на одном из LXFDVD!