Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #119, НОЯБРЬ 2008 г.

Игра в одни ворота: разрабатываем с помощью темной силы полноценную однопользовательскую игру

Юрий «Yurembo» Язев / yazevsoft@gmail.com /




Мы продолжаем начатое в прошлом номере погружение в мир игростроя. Сегодня ты освоишь новые увлекательные возможности DarkSDK, сотворив вполне законченную однопользовательскую игру. Этот 3D-action с видом от третьего лица вполне можно будет с гордостью показывать всем друзьям и подругам.

Hапомню: игровой движок, разработанный нами в прошлый раз, загружает объекты, отображает их на экране, обрабатывает ввод пользователя и перемещает робота. Он также осуществляет работу со светом, «включает», «выключает» анимацию Х-файла и обрабатывает столкновения робота с поверхностью, благодаря чему, наш терминатор способен подниматься и спускаться с возвышенностей. Кроме этого, движок создает камеру, которая следит за роботом и перемещается вслед за ним. Согласись, нехило для того небольшого количества строк кода, которые нам понадобилось написать! Другими словами, у нас получился графический движок с возможностью проводить манипуляции с персонажем (перемещать его и поворачивать). А сегодня мы добавим к нему то, что отличает голый графический движок от собственно игры — так называемый «геймплей»! Этот термин, как ты знаешь, означает игровой процесс или, фактически, — все игровые возможности (включая сюжет и предысторию… хотя, по большому счету, отношения к рассматриваемой теме они не имеют).

Дизайн-документ, или начало всех начал

Прежде чем соваться в воду, не зная броду, предлагаю хорошенько подумать над будущей игрой. Определимся с идеями и напишем коротенький диздок. Если исходить из наших движка и объектов (небесная сфера, ландшафт, робот…), становится понятно, что игра будет про огромных боевых роботов. Ради чего они сражаются — неважно, предысторию пускай геймеры сами придумывают (окружение создает соответствующую атмосферу: например, видно, что война проходит на другой планете). Подобный ход геймдизайнера во многом упрощает проектирование и процесс разработки, одновременно делая геймплей более увлекательным. Как говорится, просто и со вкусом!

Структура исходного кода

Естественно, размер сорца в сегодняшнем примере обещает значительно увеличиться (по сравнению с программой из прошлой статьи). Конечно, автор не собирается приводить код в журнале целиком, но наш чудо-диск еще никто не отменял. К тому же, комментарии, которые yurembo заботливо добавил в исходник, помогут тебе лучше разобраться в происходящем. Кстати, на этот раз мы решили отказаться от привычного именования классов и объектов (как то — добавление к именам классов лидирующей буквы c) в пользу наиболее значимых имен для объектов игры, по которым сразу становится понятно, к чему относится тот или иной класс (объект).

Итак, исходный код будет размещен в восьми файлах, из которых четыре — заголовочных, три — с реализациями классов и последний — основной; в нем происходит реализация и связь всего, что описано в остальных файлах. Материала получилось много, в рамках одной статьи все рассмотреть не удастся (даже вкратце). Поэтому пока мы разберем класс роботов, а через месяц — остальные. Вообще, программировать игры с использованием ООП — крайне удобно, поскольку эта модель программирования позволяет разрабатывать игру так, как мы ее себе и представляем: можно использовать одинаковые объекты, как во время проектирования, так и при разработке.

Заголовочный Файл «robot.h»

Держи перед глазами сорец с нашего диска — сейчас мы рассмотрим заголовочный файл, содержащий описание класса роботов. Вначале подключается заголовочный файл со всеми объявлениями, включая описание базового класса. Следующим идет имя рассматриваемого класса, который открыто наследуется от класса-предка. В классе роботов (по сравнению с базовым) объявляются три дополнительные переменные: хранящие количество жизней, информацию о том, было ли столкновение, и режим робота — в процессе реализации мы рассмотрим возможные режимы. Затем объявляются два конструктора, переопределяются три виртуальные функции, деструктор, и определяются пять новых функций. Что они делают, мы выясним при разборе реализации. Отмечу, что пользовательский и вражеский роботы представлены одним классом. Все, — больше здесь ничего интересного, поэтому плавно перейдем дальше.

Я, робот

Случалось тебе когда-нибудь заниматься робототехникой? Ну, если ты не такой крутой фрикер, как dlinnyj, то возможно, что и нет. В реале я тоже этим не занимался. Но виртуально — пожалуйста, сколько угодно! Впрочем, нам будет достаточно пяти свежесконструированных кибермонстров (четыре вражеских и один пользовательский). Откроем пачку печенья «Хакер» (поскольку пить пиво еще рано, а подкрепиться надо) и перейдем к рассмотрению реализации функциональности класса роботов. В начале файла Robot.cpp кроме подключения заголовочного файла, содержащего описание класса, подключается еще один заголовочный файл — String.h для работы с типичными C-строками. Сразу после подключения идут два конструктора: конструктор по умолчанию и конструктор с параметрами. В последний, в качестве параметров, передаются значения, которые присваиваются данным-членам — происходит инициализация создаваемого робота (объекта). Несмотря на то, что конструктор по умолчанию в нашей игре не используется, он должен быть объявлен и иметь реализацию (хотя бы пустую). Заметь, переменная-режим (evil) сбрасывается в конструкторе в false. То есть, робот — изначально пассивный (не злой; эта характеристика относится только к врагам, поскольку карму юзерского робота задает ум самого геймера :)).

Затем в коде идет деструктор, выполняющий стандартные функции. Далее идет большая функция Draw, на которой остановимся подробнее. После объявления констант условный оператор начинает большой блок кода, в котором робот делается видимым (если он жив); следующий предназначен вражескому роботу — для него вызывается самописная функция AI_Moving, суть которой заключается в перемещении компьютерного робота с помощью AI. Позже рассмотрим ее тщательнее. Теперь идет большой условный оператор, состоящий из нескольких условий. Их выполнение играет роль обработчика столкновений. После условного оператора находятся обновленные координаты робота, а в функции Check_Position проверяется позиция робота. Это поможет нам не пустить его за край ландшафта (планеты), где происходит баталия. Следующие две функции управляют позицией и углом поворота ушей. Да-да, ушей — того самого места, на котором расположена слуховая зона. Следующая функция перемещает железное тельце робота.

Вот наконец-то и заканчивается блок истинности условного оператора. В отрицательной ветви объект (робот) просто скрывается (если мертв). Затем идет еще один условный оператор. В нем проверяется, какой робот является текущим: если геймерский, то выполняется работа со стандартными C-строками по преобразованию числа (количество жизней пользовательского робота) и вывода результирующей строки на экран совместно со строкой «Life:».

Искусственный интеллект

В многопользовательском режиме врагами управляют живые (и разумные) люди, а в однопользовательской игре — искусственный интеллект (ИИ, а чаще — ИД, или искусственный дебилизм). Конечно, до уровня искусственного интеллекта ботов в Q3, разработка которого в свое время потребовала кучи баксов и человекочасов самой известной 3D-экшенной компании, он не дорастет, но кое-как играть с ним будет можно. В нашей игре весь ИИ находится в функции AI_Moving, исходный код которой ты можешь увидеть на врезке. В ней выполняется следующий сценарий: если вражеский робот находится в пассивном режиме, то он просто патрулирует местность. Двигается вперед, немного поворачиваясь в сторону: вправо или влево — зависит от того, какой у него номер (четный или нет). В активном режиме (если его возбудить ракетой в бочок) он стремительно атакует главробота, следуя за ним по пятам.

Чтобы остудить его пыл, придется запустить в него еще одну ракету; тогда вражина струсит, развернется на 180 градусов и направится в противоположную сторону. Для нахождения угла поворота врага в сторону главробота используются математические вычисления с помощью оптимизированных функций библиотеки DarkGDK. В первой версии игры класс ракет отсутствует, поэтому стрелять нечем и изменить состояние врага (во время игры) невозможно; дождись следующей версии, где этот недочет будет исправлен :). Функция Die просто деактивирует и скрывает робота. Функция Control_Position вызывается только для пользовательского робота: в ней осуществляется проверка нажатых клавиш и перемещение в пространстве главробота. Эта функция хотя и длинная, но разобраться в ней (с помощью комментариев) не составит для тебя труда. Следующая функция (Life_Decrease) вызывается для уменьшения количества жизней любого робота. Если оно станет равным нулю, то для этого робота наступит смерть.

Main.cpp

Не спеша мы подобрались к главному файлу исходного кода игры, в котором реализуется логика, вызываются функции-членов всех объектов и, вообще, происходит основное театрализованное действо.

Звук

После подключения заголовочных файлов всех классов (у нас — пока только одного) объявляются указатели на строки типа char*, содержащие пути к загружаемым файлам. Следом идет функция LoadAllSounds, которая вызывается однажды во время стартовой загрузки игры и выполняет инициализацию аудио-подсистемы. В ней присутствуют новые для нас функции, которые имеют совсем иную природу, так как работают с другим модулем DirectX. Работая с графикой, мы использовали модуль Direct3D, а ввод обрабатывался модулем DirectInput.

Сейчас мы будем колбасить звук, работая с модулем DirectSound. Конечно, вместо кодирования компонента мы воспользуемся средствами библиотеки Dark GDK — благо, она предоставляет широчайший набор функций для работы со звуком (DirectSound и DirectMusic). Эти функции отличаются по внешнему виду, поэтому в них важно не запутаться. Но не будем рассматривать модули DirectX, лучше сконцентрируем внимание на функциях Dark GDK, работающих поверх них. Функции db*Sound и db*Music различаются тем, что первые работают с wav-форматом, тогда как Music — с mp3, midi и музыкальными CD. Кроме того, и те, и другие могут работать с wav, но функций у входящих в первую группу гораздо больше. Однако, чтобы заюзать эти возможности, нужны музыкальные файлы с монозвучанием. Автор откопал для игры шесть таких звуковых файлов (пока реализовано проигрывание одного — но все еще впереди).

Мелодии загружаются с помощью функций dbLoadSound и dbLoad3DSound. У этих двух функций имеют место одинаковые параметры: путь к файлу-звуку и число (в нашем случае — константа), за которым закрепляется загружаемый звук. Те мелодии, которые были загружены с помощью dbLoad3DSound, можно впоследствии позиционировать в пространстве, что мы и сделаем в будущем, когда добавим дополнительные объекты. Кроме загрузки мелодий, еще производится настройка громкости каждой в отдельности. Это делается посредством функции dbSetVolumeSound. Ей передаются два параметра: число-звук, громкость которого надо изменить и, собственно, сама громкость в процентном эквиваленте. Изначально все звуки имеют громкость 100% (прямо как шоколадный заяц со стопроцентной сладостью).

Инициализация

После функции LoadAllSounds идет функция Init, которая вызывается один раз при старте и производит инициализацию графической подсистемы, загружает текстуры, устанавливает параметры текста, а также — создает объекты, которые не нужно пересоздавать при начале новой игры (небесная сфера и ландшафт).

Darkgdk()

Настало время заглянуть в главную функцию программы — DarkGDK(). По сравнению с прошлым разом она претерпела много изменений: что-то было вынесено (в функции инициализации и функции-члены классов), а что-то добавлено. Давай обо всем по порядку. Сперва производится проверка, в какой раз вызывается функция, и если это впервые (программа была только что запущена), то надо провести инициализацию видео- и аудио-подсистем. Для этого происходит вызов уже рассмотренной функции Init, которая кроме инициализации видеоподсистемы вызывает функцию LoadAllSounds. Если функция DarkGDK() вызывается не в первый раз, то она просто выводит картинку, пока создаются объекты.

Создание объектов как раз и происходит далее. В процессе работы создаются все динамические объекты. Их состояние во время игры изменяется, поэтому вызовы конструкторов и размещены тут, ведь функция DarkGDK() вызывается в начале каждой игры. Здесь, к примеру, создаются все роботы. Заметь, они только объявляются; специально конструкторы не вызываются (по причине того, что в C++ конструкторы вызываются при объявлении объектов). Значит, для создания объектов их достаточно объявить. В результате, они создаются в стеке. Затем зацикливаем игровой звук, перемещаем камеру, объявляем глобальную переменную — секундомер (в ней будет сохраняться текущее время) и попадаем в мартеновскую печь (основной цикл), где и будем «вариться» во время всего игрового процесса.

Главный цикл

И вот, без малейшей передышки, мы попадаем в самую гущу программных событий: всяко-разных проверок и вызовов функций-членов объектов классов. Но, благодаря комментариям, которые оставил автор, разобраться в коде не будет стоить больших усилий. Тем более, мы его рассмотрим в общих чертах прямо сейчас. В самом начале геймерский робот проходит проверку на жизнеспособность, и если результат положителен, то происходит вызов функции Control_Position, которая обсуждалась ранее (смотри «класс роботов»). После нее, перед переходом к коду, который занимается манипуляциями с объектами (вызовами соответствующих функций-членов), в пространстве поворачивается камера. Это осуществляется двумя функциями, одна из которых представляет определенный интерес — это dbSetCameraToObjectOrientation; как и следует из ее названия, она ориентирует камеру по отношению к объекту, переданному ей в параметре. Вторая же функция просто поворачивает камеру по оси X для создания небольшого наклона последней.

Затем следуют циклы, в которых проверяются столкновения между роботами и осуществляются соответствующие телодвижения. Разрабатывая игру, автор пришел к выводу, что раз роботы металлические, то при столкновении друг с другом ущерб наноситься не должен. Поэтому в результате механического взаимодействия роботы просто расходятся (конечно, зависит от того, в каком режиме они находятся). Из всего этого великолепия особенно примечательна функция dbObjectCollision библиотеки Dark GDK. Она осуществляет проверку на наличие столкновений межу объектами, переданными ей в качестве параметров: если оно имеет место, то возвращается 1, иначе — 0. И уже в самом конце вызываются две знакомые нам GDK-функции, обновляющие ландшафт и, собственно, экран. На этом главный цикл программы завершается. Если из описания что-то непонятно — смотри диск или пиши свои гневные вопросы автору :).

Промежуточные итоги

Текущая версия игры на первый взгляд кажется незначительно отличающейся от демо-версии движка. Но если вдуматься, то отличий просто вагон: камера теперь «прикреплена» к роботу сзади, поэтому, в какую бы сторону он ни повернулся, камера будет смотреть на его «тыл» (как в боевиках с видом от третьего лица). Достаточно сделать несколько шагов и можно увидеть движущихся оппонентов. К сожалению, открыть по ним огонь до следующей статьи не получится. Зато их можно брать на таран, — при этом они будут сопротивляться :). Но это мелочи! Главное, теперь все разработано в соответствии с концепцией ООП, созданы предпосылки для расширения класса. У нас появился класс роботов и их можно создавать в любом количестве. Все это представляет собой задел для будущего развития игры. Жди продолжения!

Искусственный интеллект

void Robot::AI_Moving(const float speed)
{
const float pi = 3.14159265359; //число Пи
const int mult = 5; //множитель
if (!evil) { //если робот не «злой», то обычный
сценарий движения — патрулирование
if ((num % 2) == 0) //роботов с четными номерами
отправляем в одну сторону, с нечетными — в другую
Ydeg += 360 / (dbObjectSizeZ(terrain) * mult);
//немного поворачиваем робота при движении
else
Ydeg += 360 / (-dbObjectSizeZ(terrain) * mult);
dbYRotateObject(num, Ydeg);
//действительный поворот робота
dbMoveObject(num, speed);
//действительное смещение робота
} else { //иначе, если робот «злой», то активно
атакует главного персонажа
float x, z;
x = dbObjectPositionX(myrobot) dbObjectPositionX(num); //вычисляем расстоя ние от врага до
z = dbObjectPositionZ(myrobot) dbObjectPositionZ(num); //главного персонажа
по осям x и z
if (z < 0)
Ydeg = dbATANFULL(x, z) + pi / 2; //вычисляем
угол направления движения
else
Ydeg = dbATANFULL(x, z) - pi / 2;
dbYRotateObject(num, Ydeg);
//поворачиваем врага к пользовательскому роботу
if ((dbABS(x) > 0.02) && (dbABS(z) > 0.02))
{ //если имеется расстояние от врага до глав.
персон.,
Xpos = Xpos + x / 100;
//то перемещаем врага к главному персонажу
Zpos = Zpos + z / 100;// по осям x и z
} //заметь, вычисления проводятся с использовани ем функций библиотеки Dark GDK, поскольку они лучше
оптимизированы
dbPositionObject(num, Xpos, Ypos, Zpos);
//позиционирование вражеского робота по ранее вы численным координатам (см. выше)
}
}

Links

Содержание