Темное искусство игродела, часть 3. Однопользовательская игра: достижение абсолютаЮрий «yurembo» Язев (yazevsoft@gmail.com) Сегодня мы закончим разрабатывать однопользовательскую игру, доведя ее до совершенства. Добавим и реализуем пару классов, подключим несколько звуков (для поддержки геймплея), рассмотрим работу с дополнительными функциями и типами объектов библиотеки Dark GDK – и сделаем еще много интересного. Объекты игрового мираС заголовочным файлом Game_Obj.h мы встретились еще в прошлой статье, но из-за жестких ограничений (в плане размеров журнала) нам пришлось отложить его рассмотрение до лучших времен. И они наступили! Этот заголовочный файл содержит объявления всех констант, используемых в игре, как то: объекты, текстуры, звуки и параметры экрана. Все эти константы числовые (если помнишь, ранее я говорил о том, что все типы объектов в Dark GDK представляются в виде чисел). Кроме объявления констант, файл содержит описание абстрактного супер-класса (в терминах Java и в C++ это будет просто базовый класс). Как и полагается, этот класс содержит минимум необходимых данных-членов и функций-членов, используемых всеми производными классами. Это такие переменные, как: номер текущего объекта, положение объекта в трехмерном пространстве по осям x, y, z, угол поворота (только по оси Y), индикаторы жизни и горения (переменные булевого типа) и переменная для хранения времени. Многие функции не только объявлены, но имеют реализацию прямо в заголовочном файле, что автоматически делает их встраиваемыми. Действительно, если функция выполняет одну единственную инструкцию, то почему бы не реализовать ее прямо здесь? Использование встраиваемых функций уменьшает необходимость в препроцессоре – экономятся ресурсы, связанные с вызовом функции. Эти функции выполняют такие операции, как: возвращение номера текущего объекта, возвращение состояния (жив или мертв, горит или нет), возвращение позиции, возвращение угла поворота. Все перечисленные функции-члены не изменяют (и не должны изменять) своего поведения во всех классах-потомках, поэтому работают они одинаковым образом, прямо как программист-проектировщик прописал. Еще в описании интерфейса класса имеются объявления трех виртуальных функций, плюс деструктор (тоже виртуальный). В нашей программе деструктор можно было и не делать виртуальным, поскольку в ней не используются указатели на объекты классов-предков. Но yurembo (если кто не догадался, автор любит обращаться к себе в третьем лице, - Прим. ред.) решил не отходить от привычного стиля программирования и не отказываться от советов мастеров ООП. Да, кстати, деструктор сделан чисто виртуальным (или абстрактным), что не позволит программисту, наследующему свои классы от нашего, не реализовать в них деструктор. Ну да хватит о деструкторах, перейдем к трем сакраментальным виртуальным функциям:
Замечу, что компилятор не выдает никаких сообщений, если в классе-предке объявить не виртуальную функцию, а затем переопределить ее в классе-потомке. Таким образом, мы столкнемся с неоднозначностью, которая заключается в вызове нужной функции: может быть вызвана совсем не та функция, вызов которой ожидается! Поэтому необходимость ключевого слова virtual очень существенна. Вообще, компилятор, в принципе, не может (и не должен) указывать на неявные ошибки при использовании модели ООП. Кстати, он этого и не делает, потому что он не пророк и не может знать, о чем думает и к какой цели стремится программист. В определенных случаях «огрызается» линковщик, сообщая об ошибках на только ему понятном наречии и выдавая довольно странную абракадабру, состоящую из имен заголовочных файлов и функций :). Обрати внимание, каким образом yurembo объявил данные-члены и функции-члены. Все данные-члены объявлены со спецификатором доступа protected, который делает их видимыми для функций-членов этого класса (что не так важно), а также видимыми для функций-членов производных классов. С другой стороны, все функции-члены объявлены со спецификатором public, который делает их доступными из любого места программы. Это общепринятый подход создания интерфейса с классом: данные-члены недоступны извне функций данного класса, а вся работа с объектом происходит через его функции-члены. Подчеркну: только в этом файле (Game_Obj.h) подключается заголовочный файл библиотеки DarkGDK.h, а все остальные файлы проекта подключают уже наш файл - Game_Obj.h. РакетыВзглянем на следующий заголовочный файл – Rocket.h. В этом файле мало кода, и нет ничего сложного. Как и следует из названия, в нем содержится описание класса игровых ракет, используемых как пользовательским роботом, так и врагами. Класс ракет наследует от рассмотренного выше основного класса игровых объектов все данные-члены и функции-члены, тем самым, приобретая всю его функциональность. Этот подход дарует нам множество плюсов. К примеру, код, написанный однажды (для класса-предка), используется всегда одинаково (во всех классах-потомках). Кстати, почитай любую хорошую книгу по ООП! Здесь и в остальных наследующих классах наследование открытое (public; если при наследовании не указать спецификатор доступа, то по умолчанию будет закрытое наследование, private) – мы ведь не хотим, чтобы открытые функции-члены в наследующем классе стали недоступными (закрытыми). Заметь, в этом классе не добавляется ни одна переменная, переопределяются все виртуальные функции (включая деструктор), объявляются два конструктора: по умолчанию и с параметрами, а также объявляются две отсутствующие в классе-предке функции. ВзрывыВ рамках этого раздела мы перейдем к третьему заголовочному файлу – Explode.h, содержащему описание класса взрывов. Этот класс тоже наследуется от GameObj, приобретая его функциональность; как и класс ракет, этот определяет два конструктора (конструкторы вообще по ряду причин никогда не наследуются :)). Переопределяются две виртуальные функции и деструктор. Третья виртуальная функция в этом классе не используется, такое возможно, так как в базовом классе она объявлена с ключевым словом virtual (попросту говоря, виртуальна). Две объявляемых функции имеют то же имя, что и в классе ракет, – они различаются только количеством (и типом) параметров. В таком случае можно было бы их объявить в базовом классе виртуальными, но yurembo этого не сделал. Почему? Ведь они не используются в классе роботов. Это, впрочем, можно сказать и о функции-члене PlaySound (третья виртуальная функция, неиспользуемая в классе взрывов, но используемая во всех других классах). Но здесь только одна функция, и автор решил сделать виртуальной лучше ее, чем тащить в и без того большой класс роботов неиспользуемый функционал (две рассмотренные выше функции, которые объявляются в двух производных классах). Реализация: ракетыПервый, по установленному нами порядку, класс – это Rocket (соответственно, его реализация находится в файле Rocket.cpp). После подключения заголовочного файла в нем идут два конструктора: первый (тот, который по умолчанию) не используется в нашей программе, но должен формально присутствовать – во избежание ошибок компиляции. Формален он потому, что имеет пустое тело во избежание ошибок линковщика. В конструктор с параметрами в качестве значений параметров передаются инициализирующие объект данные, которые и присваиваются данным-членам вновь создаваемой ракеты. Почти все используемые здесь функции библиотеки Dark GDK нами уже рассмотрены, поэтому не будем повторяться. Хотя стоп, одна не рассмотрена – dbCloneObject! Она ведет себя в полном соответствии с названием, клонируя объект. Взгляни на условный оператор в конструкторе: если ракета № 1, то загружаем ее из файла, а все последующие копируем с первой, так как они одинаковы. Таким образом, сокращается время загрузки: вместо того, чтобы загружать с диска все ракеты, грузится одна, а остальные быстро копируются с нее в оперативной памяти. Именно в оперативной, а не в видео – в последней хранятся только визуализированные сцены, готовые к выводу на экран. После создания объекта он скрывается (с помощью функции dbHideObject) и деактивируется, чтобы быть готовым появиться во время выстрела (функция Fire, – смотри ниже). Далее идет деструктор, который делает то, что ему и полагается: обнуляет все данные-члены, плюс функцией dbDeleteObject удаляет загруженный объект. Следующая функция Draw вовсе не отображает объект. Она вызывается при перерисовке (на каждом кадре) и производит различные манипуляции над объектом, в том числе, проверку столкновения ракеты с поверхностью ландшафта. Функция Fire представляет собой место «рождения» ракеты, в ней вызываются функции проигрывания звука и позиционирования (к месту выстрела) ракеты. Функция Pos делает то, что и должна, – то есть перемещает ракету в пространстве к месту старта (выстрела). Предпоследняя функция Die вовсе не уничтожает ракету, а просто делает ее невидимой и неактивной, чтобы не пришлось снова загружать ее при следующем выстреле (в целях ускорения игрового движка и процесса). Однако, в таком раскладе есть и минусы: расходуется память, но если посмотреть на комплектацию современного (или даже морально устаревшего) компьютера, то мы в подавляющем большинстве обнаружим не менее 512 Мб оперативки, а на машинах геймеров – не ниже 1 Гб. Поэтому автор решил пожертвовать оперативкой в пользу скорости работы, ведь игры не могут быть тормознутыми, а если таковые найдутся – пиши пропало. Последняя в этом файле функция PlaySound выполняет два действия: позиционирует (в координаты, переданные в качестве параметров) источник звука и, собственно, проигрывает звук (также переданный в качестве параметра). ВзрывыКласс Explode, содержащийся в файле Explode.cpp, представляет взрывы и вообще крайне интересен для нас, поскольку в нем реализуется новый (ранее не используемый нами) тип объектов – частицы. Но обо всем по порядку. Сначала, когда у автора возникла идея добавить в игру взрывы, он полез в интернет, чтобы нагуглить анимированные х-файлы с огнем (загружаемые нашим движком). Обнаружилось, что таких файлов приемлемого качества в Сети нема. Попытка смоделить анимированный огонь в любимой trueSpace также не увенчалась успехом: частицы наотрез отказались экспортироваться в х-файл. Тогда, у автора возникла идея: если частицы не экспортируются в х-файл, то их надо создать в самой программе. В DirectX есть такая возможность. Yurembo уже собрался кодить под голый DirectX (со всеми вытекающими отсюда трудностями), как внезапно наткнулся на функцию с очень заманчивым названием – dbMakeParticles. Отсюда он и начал «плясать» :). Второй параметр – номер изображения, которое будет использоваться, как искра. В результате, нетрудно догадаться, получится множество таких искр. В нашей программе этим параметром передается числовая константа, за которой загружено маленькое квадратное изображение (20 х 20). Оно представляет собой красный квадрат, посреди которого нарисован желтый круг. Издалека огонь-фонтан из таких искорок выглядит очень эффектно! Третий параметр – частота частиц (искр). Не стоит задавать слишком большое число, иначе время загрузки игры чересчур возрастет. Разумной кажется цифра в 3000. Главный файл реализацииВ функции инициализации по сравнению с прошлым примером ничего не изменилось. Но в главный файл реализации добавилась новая функция, название которой – BSOD – расшифровывается, как Black Screen Of Death. Она вызывается по окончанию игры, в двух равновероятных случаях: проигрыш или выигрыш (смотри картинки). Передаваемое в качестве параметра значение – по сути, индикатор различия победы и поражения. Благодаря этой функции, в финале игры пользователя будут представлены разные звуки и изображения. Кроме того, она выводит текст, осведомляющий геймера, что нужно сделать для продолжения. Она же выполняет обработку пользовательского ввода, соответствующего ее рекомендациям. В функции DarkGDK() кроме создания роботов происходит создание всех остальных динамических объектов: ракеты, взрывы. Из-за того, что теперь в игре участвуют новые объекты, код главного цикла заметно вырос и требует обратить на себя внимание. Сперва мы запускаем цикл (которых будет здесь множество) по вражеским роботам, в котором проверяем их состояние: если механический упырь уже «отбросил кони», а его ракета запущена, то уничтожаем ее, заодно увеличив значение переменной, заботливо учитывающей число погибших врагов. После этого цикла выполняется проверка, выясняющая победил ли юзер. В ней, кроме прочего, участвует упоминавшаяся выше скорбная переменная. Если количество мертвых механизмов из этой самой переменной оказывается равным количеству врагов в общей сложности, – победа засчитывается игроку и нам становится необходимо подчистить ресурсы и перевести пользователя на экран победы (вызвать BSOD :)). Ресурсы подчищаются только за динамическими объектами, для этого явно вызываются деструкторы наших классов. Остальные же объекты оставляются в покое, чтобы не возникло потом необходимости пересоздавать их перед началом новой игры. После этого идут проверки и манипуляции с главным персонажем, которые были рассмотрены в прошлой статье. Однако и здесь есть свои новинки – это код, в результате выполнения которого геймерский робот осуществляет залп огня. Затем, как обсуждалось в прошлой статье, идут циклы проверки столкновений между роботами. После этих циклов следует новая проверка, которая на этот раз осуществляется для выяснения: не погиб ли под обстрелом юзверский робот? И если да, то выполняется код, аналогичный коду конца игры. Затем снова начинаются циклы, в которых на этот раз осуществляются манипуляции над ракетами: выполняется проверка на столкновения между ними и, собственно, мишенями – роботами. В результате вызываются их (ракет) функции-члены. Под конец осуществляет свою работу еще один цикл, в теле которого происходит обновление взрывов. Взрывы, кстати, создаются тогда и в том месте, когда и где объект (ракета, робот) принимает состояние горения (burn). Закат солнца вручнуюВ конце программы ресурсы, занятые объектами наших классов, освобождаются автоматически - мы создали объекты в стеке, и когда объект выходит из области видимости, то автоматически вызывается деструктор определенного класса, который выполняет предписанные ему действия – очищает память, удаляя объект. Тем не менее, в качестве эксперимента автор решил написать функции удаления объектов и вот, что он получил. Как и следовало ожидать, процесс компиляции завершился успешно, – программа запустилась под дебагером. Кроме того, по команде автора она спокойно завершила свое выполнение. Однако, в следующий раз, когда yurembo снова запустил программу, угробил под огнем врагов своего робота, начал игру заново и попытался прикрыть ее… – дебагер запаниковал. И, даже выполняя проверку на существование объекта (функции dbImageExist, dbObjectExist, etc) перед их удалением, дебагер не успокоился. Тогда, углубившись в дебри листингов (как сишных, так и дизассемблерных), автор пришел к вышеописанному выводу. ЗаключениеВ аутсайдерах остался еще один модуль DirectX – это DirectPlay. О мультиплеере мы, будем надеяться, подробно поговорим в следующей статье, где нашей целью будет разработать мультиплеерную баталию. DVDНа диске лежит полный исходный код финальной версии однопользовательской игры DarkRobot, для компиляции которого нужны: Visual C++ 2008 Express Edition, DirectX 9.0 SDK, Dark GDK. WWWЕсли влом вставлять диск, можешь скачать исходник с сайта ][акера – www.xakep.ru. INFOЕсли тема тебя заинтересовала, сообщи об этом автору, продолжим развитие хакерского игропрома. |