Программирование стратегических игр с DirectX 9.0

       

Включение файлов и прототипы функций


В нескольких первых строках кода указываются включаемые заголовочные файлы и приводятся прототипы функций. В программировании для Windows необходим только один заголовочный файл с именем windows.h.

Для нашего примера требуется единственный прототип функции для обработчика сообщений. Любая программа для Windows должна содержать функцию, которая будет вызываться для обработки сообщений и должна соответствовать следующему прототипу:

LRESULT CALLBACK fnMessageProcessor (HWND, UINT, WPARAM, LPARAM);

Имя функции может отличаться, но параметры должны оставаться неприкосновенными. Это вызвано тем, что Windows автоматически вызываеет данную функцию и не сможет работать правильно, если вы измените параметры.



Внутреннее устройство функции WinMain()


Первый элемент, упоминаемый в функции WinMain() - это объект, используемый для создания окна. Объект является структурой типа WNDCLASSEX и очень важен для создания окна вашей программы.



Воспроизведение музыки


После всей выполненной работы воспроизведение MP3 осуществляется исключительно просто. Достаточно лишь вызвать функцию IMediaControl::Run(). Если функция возвращает S_OK, вы знаете, что воспроизведение успешно началось.



Воспроизведение звука


Функция PlaySegmentEx() начинает воспроизведение загруженного сегмента. У нее есть несколько параметров, но как видите, в коде моего примера значения многих из них равны NULL. Вот как выглядит прототип функции:

HRESULT PlaySegmentEx( IUnknown* pSource, WCHAR *pwzSegmentName, IUnknown* pTransition, DWORD dwFlags, __int64 i64StartTime, IDirectMusicSegmentState** ppSegmentState, IUnknown* pFrom, IUnknown* pAudioPath );

В первом параметре, pSource, передается указатель на интерфейс воспроизводимого объекта. В рассматриваемой программе я использую глобальный указатель на сегмент, загрузка которого была выполнена раньше.

Второй параметр, pwzSegmentName, в DirectX 9.0 не используется.

Третий параметр, pTransition, позволяет задать модуляцию для сегмента. Я передаю в этом параметре NULL.

Четвертый параметр, dwFlags, позволяет вам указать набор флагов, определяющих различные параметры воспроизведения. В рассматриваемом примере для этого параметра я использую флаги DMUS_SEGF_DEFAULT и DMUS_SEGF_SECONDARY. Эти флаги указывают, что сегмент воспроизводится в его границах по умолчанию и что сегмент воспроизводится как вторичный звук. Доступно еще много других флагов, и я рекомендую вам посмотреть их описание в документации DirectX SDK.

Пятый параметр, i64StartTime, задает начальное время для сегмента. Я передаю в этом параметре NULL чтобы воспроизведение звука началось немедленно.

В шестом параметре, ppSegmentState, передается адрес указателя в котором функция возвращает указатель на интерфейс, позволяющий получить состояние сегмента. Я обычно не пользуюсь этой возможностью и передаю в данном параметре NULL.

Седьмой параметр, pFrom, позволяет указать интерфейс для остановки воспроизведения когда стартует новый сегмент. Здесь я также передаю NULL.

Восьмой параметр, pAudioPath, сообщает системе какой аудио-путь используется для воспроизведения сегмента. Я присваиваю этому параметру значение NULL чтобы для воспроизведения использовался аудио-путь по умолчанию.

Теперь запустите программу и щелкните несколько раз по ее окну левой кнопкой мыши. Важная особенность программы заключается в том, что она может одновременно воспроизводить несколько звуков. Это может не произвести на вас большого впечатления, если вы никогда раньше не занимались воспроизведением звука, но поверьте мне — это очень круто. Замечательно то, что DirectMusic за вас выполняет всю необходимую буферизацию.



Воспроизведение звуковых фрагментов

Вернемся к циклу обработки сообщений Windows. В нем находится код, ожидающий пока не будет нажата левая или правая кнопки мыши. Как только произойдет одно из указанных событий, будет воспроизведен соответствующий звук. Вот как выглядит код:

switch(msg) { case WM_LBUTTONDOWN: // Воспроизведение звука g_SoundSys.hrPlaySound(g_sndButtonOver); break; case WM_RBUTTONDOWN: // Воспроизведение другого звука g_SoundSys.hrPlaySound(g_sndButton); break; case WM_DESTROY: PostQuitMessage(0); return 0; default: break; } return DefWindowProc(hWnd, msg, wParam, lParam);

В блоке логики оператора switch видно, как система реагирует на события кнопок мыши вызывая функцию класса звуковой системы hrPlaySound(). Если нажата левая кнопка мыши, воспроизводится звук из объекта g_sndButtonOver. Если нажата правая кнопка мыши, воспроизводится звук из объекта g_sndButton.

ПРИМЕЧАНИЕ Сообщения WM_LBUTTONDOWN и WM_RBUTTONDOWN являются частью системы событий Microsoft Windows. Существуют сотни различных событий и для получения более подробной информации о них я рекомендую обратиться к документации Visual C++.

На Рисунок 7.12 показан ход выполнения программы с момента запуска до завершения.



Возвышенность


Возвышенность — полезное свойство для тех стратегических игр, на картах которых есть настоящие возвышения. Например, в игре Total Annihilation есть возвышенности, влияющие на стратегию. Пулемет, расположенный на вершине холма может стрелять поверх стен вражеской базы. Хороший способ хранить карту возвышенностей — задавать значение высоты каждого блока. Программа может читать значение высоты блока, чтобы определить линию прицеливания.



Вторая и третья цели игры Empire Earth







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

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



Выберите что вы будете пить


Первое необходимое действие — выбор ресурсов, которые должен собирать игрок. В зависимости от сложности вашей игры, потребуется наличие от трех до шести видов ресурсов, которые будет добывать игрок. Я всегда люблю обучать на примерах, поэтому давайте начнем с игры, над которой я работал некоторое время. В игре Battle Armor есть три основных вида ресурсов: топливо, руда и пища. Вы можете спростить — как я придумал их? Проще простого — я всего лишь напечатал их названия! Это была шутка. В действительности все следует начинать с записи некоторых ключевых целей для игрока. Для Battle Armor я придумал следующий список целей:

Кормить население. Строить инфраструктуру. Увеличивать армию.

Выбор размера блоков


Вы должны начать с выбора графического пакета, такого как Adobe Photoshop 7. Затем следует выбрать размер основного блока. Для наборов блоков, которые не являются изометрическими, размер блока обычно выбирается из степеней двойки, например 32 x 32, 64 x 64 или 128 x 128. Размер блока, который вы реально выберете, зависит от конкретной ситуации. Нет никаких стандартов выбора размера блоков, но вы должны учесть следующее:

Сколько различных блоков требуется для игры? Сколько памяти будет выделено для графики? Сколько блоков будут отображаться одновременно?

Первые два вопроса взаимосвязаны, поскольку количество блоков непосредственно влияет на объем, необходимой для их хранения памяти. Если ваша игра требует тысячи блоков, вы, вероятно, будете избегать использовать блоки большого размера. Если же в вашей игре используется всего несколько сотен блоков, скорее всего, использование блоков большого размера не вызовет никаких трудностей.

Последний вопрос относится к тому, сколько блоков будут видны на экране в любой момент времени. Это необходимо учесть потому, что существует много возможных разрешений экрана и различных решений интерфейса. Взгляните, например, на интерфейс, изображенный на Рисунок 5.6.



Вычисление местоположения указателя мыши

Следующим этапом работы кода является вычисление текущего местоположения указателя мыши. Это делается с помощью вызова предоставляемой Windows функции GetCursorPos(). Эта функция проверяет текущее местоположение указателя мыши и сохраняет полученный результат в структуре POINT. Структура POINT содержит координаты указателя мыши по осям X и Y.

ПРИМЕЧАНИЕ Функция GetCursorPos() не является частью DirectX SDK. Это внутренняя функция Windows. Для работы с мышью я предпочитаю применять стандартные функцииI Windows, поскольку они достаточно быстрые и значительно проще в использовании, чем вызовы DirectX.

Следующие строки кода корректируют полученные координаты указателя мыши. Это достаточно простое вычисление — берутся координаты указателя мыши и из них вычитаются координаты окна и смещение клиентской области окна. В результате будут получены скорректированные координаты, которые можно использовать для проверки попадания по активной зоне.



Вычисление обороноспособности


Создание формулы вычисления защиты боевых единиц для вашей игры — боле тонкая задача, чем вычисление скорости. Главная проблема состоит в том, что вы можете захотеть иметь различные типы брони, которые по разному реагируют на различные типы вооружений. Предположим, в вашей игре есть боевая единица с огнеметом, которая наносит серьезные повреждения пехоте. Вы же не хотите, чтобы этот огнемет был так же эффективен против танков? Возможно, вы решите использовать метод, предстаавленный в таблице 3.1.

Таблица 3.1. Броня боевых единиц

Тип брони Огонь Снаряд Химическое
оружие
Личная 0,1 0,2 0,5
Тяжелый танк 0,9 0,7 0,8
Легкий танк 0,7 0,6 0,7

В таблице 3.1 перечислены типы брони и три формы атаки: огонь, снаряды и химическое оружие. Личная броня — это персональные средства защиты, которые носит пехота. Рассмотрим простой алгоритм:вы берете количество наносимых оружием повреждений, умножаете его на оценку брони и вычитаете полученное число из количества наносимых оружием повреждений, чтобы определить реально причиненный ущерб.

Предположим, что упомянутый выше огнемет наносит пехотинцу 100 единиц повреждений. Вы используете следующую формулу, чтобы определить количество повреждений, поглощенное броней:


100 (наносимый ущерб) x 0.1 (оценка брони) = 10 единиц поглощено броней


Эта формула означает, что пехотинец получит следующее количество повреждений:


100 (наносимый ущерб) – 10 (поглощено броней) = 90 единиц повреждений получено


Как видно из этого примера, огнемет очень эффективен против брони пехотинцев. Теперь посмотрим, что получается если тот же самый огнемет используется против тяжелого танка:


100 (наносимый ущерб) x 0.9 (оценка брони) = 90 единиц поглощено броней


100 (наносимый ущерб) – 90 (поглощено броней) = 10 единиц повреждений получено


Броня танка гораздо эффективнее и в результате полученный ущерб гораздо меньше. Здесь, конечно совсем не учитывается разница в максимально допустимом количестве повреждений между танком и пехотинцем.



Вычисление скорости боевых единиц


Что касается математической стороны расчета скорости боевых единиц, я рекомендую вам создать базовую шкалу скоростей со значениями от 1 до 100, где 1 соответствует самой медленной скорости, а 100 — самой быстрой. Используя подобную шкалу вы можете просто умножить скорость боевой единицы на метод ее передвижения, чтобы получить перемещение.



Вычисление скорости снаряда


Скорость оружия очень похожа на скорость боевой единицы. Вам необходима стандартная система скоростей для назначения всем, используемым в игре снарядам. Я еще раз предлагаю использовать шкалу от 1 до 100 для задания скорости снарядов. Тогда вы сможете умножать данное значение на используемую в игре стандартную меру длины, чтобы вычислить перемещение снаряда.


Вот и все, что я хотел рассказать сейчас о боевых единицах. Пора перейти к управлению ресурсами.



Вычисление скорострельности


Хорошо зарекомендовавший себя на практике метод вычисления скорострельности — определить сколько раз в минуту может выстрелить конкретное оружие. Это дает хорошую систему для установки параметров скорострельности (rate of fire, ROF) или количества снарядов в минуту (round per minute, RPM) ваших систем вооружения. Несколько примеров приведено в таблице 3.2.

Таблица 3.2. Скорострельность

Оружие Снарядов в минуту
9-мм пистолет 120
M1917A1 — влагозащищенный автомат калибра .30 400 – 600
Автоматический пистолет PPS43 700
81-мм гранатомет M1 18

Как видно из таблицы 3.2, автоматическая винтовка имеет гораздо большую скорострельность чем миномет. Это имеет смысл, если принять во внимание перечисленные виды оружия.



Вычисление смещения клиентской области


Вы должны помнить, что у функции CreateWindowEx() есть параметры, задающие размер создаваемого окна. Эти параметры позволяют создать окно необходимого размера. Проблема заключается в том, что при этом не учитывается наличие у окна заголовка. В результате, если у вашего окна есть заголовок, вы получите меньше пространства для визуализации, чем рассчитывали. Взгляните на Рисунок 6.13.



Вычисление смещения клиентской области окна на рабочем столе


Поскольку окна в операционной системе могут перемещаться, программа сначала должна проверить где именно расположено окно в данный момент. Эту задачу выполняет вызов функции GetWindowRect(). Вооружившись координатами окна, код может вычислить где находился указатель мыши в момент щелчка относительно клиентской области окна. Это очень важный момент, поскольку активные зоны определяются в клиентском пространстве, а не в пространстве рабочего стола. Взгляните, например на экран, изображенный на Рисунок 6.24.



Выход из программы


Давайте на минуту сосредоточим наше внимание на функции WinMain(). Я знаю, что заставляю вас прыгать с места на место, но пожалуйста потерпите. Вернемся к функции WinMain() в которой содержится следующий код:

if(g_iCurrentScreen == 3) { break; }

Этот код я использую для выхода пользователя из программы. Нельзя выкинуть пользователя из программы, как только он щелкнул по завершающему экрану. Сперва надо завершить главный цикл обработки сообщений. Лучший способ обнаружения завершения программы состоит в проверке значения специальной переменной. Я проверяю не равно ли значение переменной g_iCurrentScreen числу 3. Это сообщает мне, что пользователь закончил играть и необходимо завершить работу программы. В реальной игре может использоваться более трудоемкий метод, но для простого примера подходит и рассмотренный простой способ.



Взаимодействие класса звуковой системы и класса звукового фрагмента





Обратите внимание, что интерфейс загрузчика в классе звуковой системы используется для загрузки данных в интерфейс сегмента в классе звукового фрагмента. Кроме того, вы можете заметить, что интерфейс исполнителя совместно используют оба класса.



Взаимодействие объектов классов звуковой системы и звукового фрагмента





На Рисунок 7.10 показано взаимодействие объектов и взаимоотношения между классом звуковой системы и классом звукового фрагмента. В верхней части рисунка показано как интерфейсы зхагрузчика и исполнителя используются в функции инициализации. В центре рисунка видно как интерфейс загрузчика применяется в функции загрузки звука. В функцию загрузки передается объект звукового фрагмента и там вызывается его метод загрузки данных. В нижней части рисунка показано как объект звукового фрагмента передается в функцию возпроизведения, чтобы началось воспроизведение звука. Чтобы воспроизвести реальный звук объект исполнителя из класса звуковой системы использует данные сегмента из объекта звукового фрагмента. У-ух. Понятно?



Взаимоотношения между классом окна и обработчиком сообщений





Четвертый член структуры относится к типу int и называется cbClsExtra. Это целое число задает количество байт, которые будут выделены сразу за структурой данных класса окна. Я не имею ни малейшего представления, для чего это может быть нужно, поскольку во всех примерах, которые я видел, это значение устанавливалось равным 0. Я рекомендую вам поступать точно так же. Если желаете, можете просто игнорировать этот член данных, поскольку система сама по умолчанию присваивает ему нулевое значение.

Пятый элемент также относится к типу int и называется cbWndExtra. Это целое число задает количество байтов, которые будут выделены сразу за экземпляром окна. Работа с ним аналогична обращению с предыдущим членом структуры, и система также по умолчанию присваивает ему нулевое значение.

Шестой член структуры имеет тип HANDLE и называется hInstance. Дескриптор, который вы задаете здесь, является дескриптором экземпляра к которому относится окнонная процедура класса. В большинстве случаев можно задать значение дескриптора hInstance, получаемого функцией WinMain() в одном из параметров.

Седьмой параметр называется hIcon и имеет тип HICON. Тип HICON это ни что иное, как замаскированный тип HANDLE, поэтому он не должен смущать вас. Данный дескриптор указывает на класс значка, используемого окном. Класс значка в действительности является ресурсом значка. Не присваивайте этой переменной значение NULL, поскольку если сделать так, то программа будет перерисовывать изображение значка, при каждом свертывании окна.

Если вы еще раз взглянете на код, то увидите, что для инициализации седьмого параметра я использую вызов функции LoadIcon().

Функция LoadIcon() загружает ресурс значка из исполняемой программы. Хотя ресурсы компилируются внутрь вашей программы для Windows, вам все равно необходимо загружать их, поэтому и требуется вызов данной функции. Вот как выглядит ее прототип:

HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName );

К счастью, у этой функции всего два прарметра — HINSTANCE и LPCTSTR. Первый параметр с именем hInstance, содержит дескриптор экземпляра модуля чей исполняемый файл содержит значок, который вы желаете использовать. В моих примерах программ я присваиваю этому параметру значение NULL. Делая так вы разрешите использование встроенных значов Windows, которые часто также называют стандартными значками.

Второй параметр является указателем на строку, содержащую имя загружаемого значка. Взглянув на разбираемый пример, вы увидите, что для инициализации данного параметра я использую константу IDI_APPLICATION. Ее значение соответствует значку, используемому по умолчанию для приложений, который вы могли видеть во многих программах для Windows. Остальные стандартные значения перечислены в таблице 2.3.

Таблица 2.3. Константы для стандартных значков

Значение Описание
IDI_APPLICATION Значок, используемый по умолчанию для приложений. Он применяется и в рассматриваемом примере. В большинстве случаев вы можете использовать это значение, если только не хотите, чтобы у вашего приложения был нестандартный значок.
IDI_ASTERISK Данное значение создает для вашей программы значок в виде небольшого овала с буквой «i» внутри.
IDI_ERROR Красный круг с крестом внутри.
IDI_EXCLAMATION Желтый треугольник с восклицательным знаком внутри.
IDI_HAND Я понятия не имею, для чего нужны эти дубли, но этот значок выглядит точно так же, как IDI_ERROR.
IDI_INFORMATION Еще один дубль. Это значение задает небольшой овальный значок, аналогичный задаваемому значением IDI_ASTERISK.
IDI_QUESTION Использование этого значения даст вашему приложению значок с вопросительным знаком.
IDI_WARNING О, опять дублирование. На этот раз значок выглядит так же как для значения IDI_EXCLAMATION.
IDI_WINLOGO Если использовать эту константу, значок вашего приложения будет выглядеть как небольшой аккуратный логотип Windows.

Вот и все, что можно сказать о функции LoadIcon(). Теперь вернемся к разбираемой программе. Ох, подождите, прежде чем продолжить, взгляните на Рисунок 2.10, где изображены некоторые стандартные значки.



Взаимоотношения между проектной документацией





На Рисунок 4.1 вы видите как высокоуровневая идея игры обретает плоть в наброске. Элементы наброска служат основой для документа с формулировкой требований. Каждый элемент наброска разбивается на несколько требований, которые, в свою очередь служат основой для технического задания. В техническом задании выделены и подробно описаны основные задачи программирования. Возьмем для примера требования к многопользовательской игре которые я сформулировал в предыдущем разделе.

Требования к многопользовательской игре

Необязательный выделенный сервер

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

Как видите, в техническом задании я перечисляю детали которые необходимы с точки зрения разработчика. В документе ничего не говорится о маркетинге или о самом процесссе игры. В нем должны присутствовать только основные элементы, относящиеся к разработке.

Теперь возьмите ваш документ с требованиями и составьте техническое задание. Вы должны перечислить все, о чем можно подумать, рассматривая игру с точки зрения разработчика. В отличие от документа с требованиями, этот не столь гибок и может вызвать значительные разветвления проекта, если вы забыли что-либо включить в него. К счастью, поскольку вы сами являетесь разработчиком, — завершить эту фазу вам будет проще всего. Я завершил написание технического задания для моего последнего проекта приблизительно за три недели (в результате получился документ, объемом 137 страниц!).



Взаимосвязь между структурой данных и активной зоной





Из Рисунок 6.27 видно, что переменные m_shZoneXPos и m_shZoneYPos задают координаты верхнего левого угла зоны. Член данных m_shZoneWidth определяет ширину зоны, а m_shZoneHeight — ее высоту.

Член данных m_bActive предназначен для внутреннего использования классом активных зон и определяет используется ли зона в данный момент. Он используется, когда при создании новой зоны происходит поиск свободного места для ее данных.

Член данных m_shClickType хранит информацию о том, какой именно тип щелчка активирует данную зону. Возможные значения перечнслены в таблице 6.10.

Переменная m_szZoneName используется для хранения имени активной зоны.



Взаимосвязь шаблонов интерфейса


Хотя «Крестики-нолики» — это интригующая и сложная игра, она не отвечает требованиям, предъявляемым к настоящему примеру; поэтому я начну создавать полностью новую схему интерфейса.



Warcraft Orcs & Humans


Почти в то же время, когда вышла игра C&C, другая компания выпустила не менее захватывающую стратегию реального времени. Этой игрой, конечно, была Warcraft от Blizzard Entertainment. Находясь на другом конце спектра, Warcraft является игрой в стиле фэнтези, проходящей в средневековом окружении, в то время как C&C — это футуристическая военная игра.

В Warcraft вы можете играть либо за орков, либо за людей. Игра разворачивается вокруг битвы за землю Азерот. Доступно несколько сценариев, я полагаю по 12 для каждой стороны. Игра в однопользовательском режиме очень интересна, но гораздо больше удовольствия я получил от выбивания соплей из моих друзей в многопользовательском режиме. Warcraft является еще одной игрой, которая отняла множество часов моего времени. Мы могли играть до рассвета и дальше.

Как и Command & Conquer, Warcraft — хорошо сбалансированная игра. Играете ли вы за орков или за людей, у вас есть равные шансы на победу в игре с грамотным противником. Позднее была выпущена игра Warcraft II: Tides of Darkness, но я не думаю, что она была также хорошо сбалансирована. Может быть это объясняется количеством доступных подразделений. Следует помнить, что больше не всегда значит лучше. Игра с сотнями несбалансированных подразделений гораздо хуже, чем игра с десятком идеально сбалансированных произведений.



Warlords


Давным давно я играл в игру с названием Warlords на моем компьютере Amiga. Несколько друзей собирались у меня дома и приступали к игре. Эти встречи обычно продолжались по 12 часов и более, поскольку ходы делали все по очереди. В конечном итоге один из нас побеждал, и все получали массу удовольствия.

Warlords — это походовая стратегическая игра выпущенная Strategic Studies Group, в которой вы командуете фэнтезийными отрядами и ведете их в бой с противником. Усыпающие местность замки позволяют вам строить армии, необходимые для завоеваний. Уловка игры состоит в том, что замки создают различные армии. Некоторые замки производят драконов, другие — эльфов. Кроме того, вы можете получить героев, что дает значительное преимущество. Герои могут путешествовать по стране и находить специальные боевые артефакты в заброшенных храмах и руинах. Герои очень важны для игры, поскольку их преимущества влияют и на армии, путешествующие с ними.

За прошедшие годы SSG выпустила несколько новых версий Warlords, включая стратегическую игру в реальном времени с названием Warlords: Battlecry, у которой есть даже вторая версия. Привлекательность игры Warlords вызвана мощной системой искусственного интеллекта. Искусственный интеллект в серии игр всегда был превосходен иявляется достойным соперником в однопользовательском режиме игры.



XCOM UFO Defense


Ни один список классических стратегических игр не может считаться полным бехз упоминания об X-COM. X-COM — это очень интересная игра в которой вы защищаете Землю от вторжения инопланетян. Главная часть игры представляет собой изометрический вид на действия боевой группы. Вы командуете группой бойцов, сражающихся против инопланетян в нескольких миссиях. Интерес игре придает развитие технологии. Вы начинаете игру с простейшим оборудованием, производимым земной промышленностью, а затем модернизируете его, изучая технологии инопланетян. С каждым сражением ваши солдаты становятся лучше и лучше, и в конце концов сравниваются с инопланетянами. Нет ничего более приятного, чем запустить управляемую ракету за угол, вниз по лестнице, а затем направо через дверной проем, чтобы поразить врага.

X-COM объединяет управление ресурсами и тактическую стратегию, предоставляя вам контроль над обороной всего мира. Вы должны перехватывать вражеские истребители, строить оборонные базы и уничтожать врагов на земле. Все это дает полезный опыт.

Единственным недостатком X-COM являются последовавшие продолжения. Первоначальная игра дает массу удовольствия, но продолжения не столь хороши.На этом язавершаю обзор первых стратегических игр. я мог бы продолжать еще и еще, но думаю, что у вас теперь есть достаточно примеров. Зачем я привел этот список игр? Чтобы дать вам идей для ваших собственных стратегических игр. Едва ли можно найти лучший способ начать разрабатывать игру, чем оглянуться назад и посмотреть, что люди создали до вас. История — великий учитель.



Зачем использовать блоки?


Теперь, когда вы узнали что такое блоки, давайте узнаем для чего их используют разработчики игр. Есть три основные причины, по которым разработчики применяют в своих играх блочную графику: экономия памяти, повторное использование графики и динамическое содержание.



Зачем использовать спрайты?


Теперь, посмотрев на интерфейс спрайтов в действии, вы можете задаться вопросом — зачем вам когда-либо может понадобиться применять спрайты. Одна из причин заключается в том, что использовать спрайты проще, чем выполнять визуализацию буфера вершин. Интерфейс спрайтов скрывает всю работу, необходимую для правильного отображения ортогональной проекции. За исключением вышесказанного, я не могу найти дополнительных причин для использования спрайтов. Поэтому я предлагаю, чтобы вы сами поэкспериментировали с визуализацией вершин и с работой со спрайтами, после чего выбрали бы то, что вам больше нравится.



Заголовочный файл Main h

Есть только один заголовочный файл, который следует обсудить подробно - файл main.h. Он содержит всю заголовочную информацию, необходимую для программы. Вот как выглядит содержащийся в этом файле код:

#define STRICT #include <windows.h> #include <commctrl.h> #include <commdlg.h> #include <math.h> #include <tchar.h> #include <stdio.h> #include <D3DX9.h> #include "DXUtil.h" #include "D3DEnumeration.h" #include "D3DSettings.h" #include "D3DApp.h" #include "D3DFont.h" #include "D3DUtil.h" // Структура для данных вершин блока struct TILEVERTEX { D3DXVECTOR3 position; // Позиция D3DXVECTOR3 vecNorm; // Нормаль FLOAT tu, tv; // Координаты текстуры (U,V) }; // Наш собственный FVF, описывающий созданную структуру данных вершин // D3DFVF_XYZ= Информация о координатах // D3DFVF_NORMAL = Информация о нормалях // D3DFVF_TEX1 = Информация о текстуре #define D3DFVF_TILEVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1) class CD3DFramework : public CD3DApplication { // Шрифт для отображения FPS и данных видеорежима CD3DFont* m_pStatsFont; // Массив целых чисел для хранения блочной карты int m_iTileMap[100]; short m_shTileMapWidth; short m_shTileMapHeight; // Буфер для хранения текстур LPDIRECT3DTEXTURE9 m_pTexture[32]; // Размеры окна short m_shWindowWidth; short m_shWindowHeight; // Буфер для хранения вершин LPDIRECT3DVERTEXBUFFER9 m_pVBTile; protected: HRESULT OneTimeSceneInit(); HRESULT InitDeviceObjects(); HRESULT RestoreDeviceObjects(); HRESULT InvalidateDeviceObjects(); HRESULT DeleteDeviceObjects(); HRESULT Render(); HRESULT FinalCleanup(); HRESULT CreateD3DXTextMesh(LPD3DXMESH* ppMesh, TCHAR* pstrFont, DWORD dwSize); // Создание буфера вершин блока void vInitTileVB(void); // Рисование блока на экране void vDrawTile(float fXPos, float fYPos, float fXSize, float fYSize, int iTexture); public: LRESULT MsgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); CD3DFramework(); };

Я не собираюсь докучать вам обсуждением кода, который не является специфичным для приложения, поэтому мы сразу перескочим к следующему фрагменту:

struct TILEVERTEX { D3DXVECTOR3 position; // Позиция D3DXVECTOR3 vecNorm; // Нормаль FLOAT tu, tv; // Координаты текстуры (U,V) };

ПРИМЕЧАНИЕ Действительно, в названии программы присутствует слово 2D, но не позволяйте ему одурачить вас. DirectX больше не работает с чисто двухмерной графикой, так что теперь программы используют трехмерную графику, которая выглядит как двухмерная. По этой причине я создал структуру для описания вершин, содержащую информацию, необходимую для создания трехмерного блока, который будет выглядеть как имеющий только два измерения. Если вы хотите получить больше информации об этом аспекте программирования, я рекомендую вам обратиться к главе 6, которая сосредотачивается на программировании двухмерной графики в трехмерном окружении.

Структура данных вершины содержит информацию о местоположении, нормали и координатах текстуры. Это все, что необходимо для отображения на экране графики, которая будет выглядеть двухмерной.

К заслуживающим упоминания переменным класса относятся m_iTileMap, m_shTileMapWidth, m_shTileMapHeight, m_pTexture и m_pVBTile. Массив m_iTileMap хранит информацию блочной карты. Я объявляю массив из 100 целых чисел, поскольку ширина и высота карты будут равны 10 блокам.

Переменные m_shTileMapWidth и m_shTileMapHeight хранят размеры блочной карты. В конструкторе класса я присваиваю этим переменным значение 10.

Массив m_pTexture содержит указатели на текстуры, используемые библиотекой визуализации. Каждое значение в массиве, хранящем блочную карту, является индексом для этого массива.

Указатель m_pVBTile указывает на буфер вершин, необходимый программе для визуализации блоков.

Следующий блок кода, который мы рассмотрим более подробно, выглядит так:

// Создание буфера вершин блока void vInitTileVB(void); // Рисование блока на экране void vDrawTile(float fXPos, float fYPos, float fXSize, float fYSize, int iTexture);

Эти два прототипа функций являются сердцем рассмартиваемого примера, поскольку именно они относятся к отображению блоков. Первая функция, vInitTileVB(), инициализирует буфер вершин блока, используемый для визуализации. Следующая функция, vDrawTile(), применяется для отображения блока на экране.

ПРИМЕЧАНИЕ Код, который вы видите здесь, не является точной копией кода из файла с сопроводительного компакт-диска. Чтобы сэкономить место в книге, я удалил из кода некоторые комментарии.

Заголовочный файл Main h


И снова наше внимание будет сосредоточено только на одном заголовочном файле — main.h. Раньше вы уже видели большую часть содержимого этого файла, поэтому я сконцентрируюсь на наиболее важных фрагментах. Первый из таких фрагментов — объявление переменных класса:

// Массив целых чисел для хранения карты int m_iTileMap[100][2]; short m_shTileMapWidth; short m_shTileMapHeight; // Буфер для хранения текстур LPDIRECT3DTEXTURE9 m_pTexture[32];

Ну что, посмотрели? Все переменные остались теми же самыми, за исключением еще одного измерения, добавленного к массиву m_iTileMap. Массив был изменен, чтобы хранить несколько слоев, для чего к нему было добавлено еще одно измерение. Новый слой, добавленный в этом примере, предназначен для того, чтобы хранить детали блочной графики. Вместо того, чтобы хранить все в одном слое, мы теперь воспользуемся двумя.

Вот и все, что я хотел сказать о заголовочном файле; остальные его части ничем не отличаются от тех, которые мы разобрали, обсуждая отображение двухмерных блоков.



Заголовочный файл Main h

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

#define STRICT #include <windows.h> #include <commctrl.h> #include <commdlg.h> #include <math.h> #include <tchar.h> #include <stdio.h> #include <D3DX9.h> #include "DXUtil.h" #include "D3DEnumeration.h" #include "D3DSettings.h" #include "D3DApp.h" #include "D3DFont.h" #include "D3DFile.h" #include "D3DUtil.h" int g_iNumTiles = 2; // Формат вершин для трехмерных блоков struct D3DVERTEX { D3DXVECTOR3 p; D3DXVECTOR3 n; FLOAT tu, tv; static const DWORD FVF; }; const DWORD D3DVERTEX::FVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1; class CD3DFramework : public CD3DApplication { CD3DFont* m_pStatsFont; TCHAR m_strFont[LF_FACESIZE]; DWORD m_dwFontSize; // Данные трехмерных объектов CD3DMesh* m_pObject[32]; // Массив целых чисел для хранения блочной карты int m_iTileMap[100]; short m_shTileMapWidth; short m_shTileMapHeight; // Размеры окна short m_shWindowWidth; short m_shWindowHeight; protected: HRESULT OneTimeSceneInit(); HRESULT InitDeviceObjects(); HRESULT RestoreDeviceObjects(); HRESULT InvalidateDeviceObjects(); HRESULT DeleteDeviceObjects(); HRESULT Render(); HRESULT FrameMove(); HRESULT FinalCleanup(); HRESULT CreateD3DXTextMesh(LPD3DXMESH* ppMesh, TCHAR* pstrFont, DWORD dwSize); public: LRESULT MsgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); CD3DFramework(); };

Первое изменение находится в разделе включаемых файлов. Файлу d3dfile.cpp требуется заголовочный файл d3dfile.h. Поскольку наша программа использует функции из файла d3dfile.cpp, заголовочный файл должен быть также включен в исходный код.

ПРИМЕЧАНИЕ Вспомогательные файлы, такие как d3dfile.cpp, d3dapp.cpp, d3dsettings.cpp, и т.д., находятся в той папке, куда вы установили DirectX SDK. Если вы не меняли предлагаемый путь по умолчанию, файлы находятся в папке C:\DXSDK\Samples\C++\Common.

Следующий фрагмент, который представляет интерес — объявление переменной g_iNumTiles расположенное сразу после перечисления включаемых файлов. Данная переменная содержит количество загружаемых программой трехмерных моделей. Очень важно отслеживать это число, поскольку программа должна знать, сколькими моделями ей управлять. Если вы решите добавить к рассматриваемому примеру еще модели, убедитесь, что значение переменной также соответствующим образом увеличено.

Следующее изменение находится в структуре данных D3DVERTEX. Ее формат слегка отличается от использованного ранее. Это сделано для поддержки формата вершин трехмерных моделей. Для такой поддержки необходима переменная FVF типа DWORD. Это единственное отличие от использованного ранее формата вершин.

Спустимся ниже к определению класса. Здесь добавлена новая переменная с именем m_pObject. Это массив значений типа CD3DMesh. Объект CD3DMesh предоставляется библиотекой d3dfile.cpp. Это ваше окно в мир загрузки и отображения трехмерных моделей. Я произвольным образом установил размер массива равным 32. Это указывает, что максимальное количество загруженных блоков будет равно 32. Не стоит волноваться, — если захотите, вы в дальнейшем всегда сможете изменить это число.



Заголовочный файл Main h


Есть только один заголовочный файл, который представляет для нас интерес — main.h. В этом заголовочном файле я выполняю следующие действия:

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

Заголовочный файл Main h


Главный заголовочный файл проекта называется main.h. В нем я выполняю следующие действия:

Устанавливаю глобальные переменные Direct3D. Устанавливаю структуру настраиваемого формата вершин. Устанавливаю различные глобальные переменные. Определяю прототипы функций. Устанавливаю глобальные данные активных зон. Устанавливаю глобальные данные кнопок мыши.

Заголовочный файл Main h


Главный заголовочный файл содержит несколько новых строк кода, необходимых для реализации подсветки пунктов меню. Перед тем, как перейти к коду, взгляните на Рисунок 6.28, чтобы увидеть подсветку пунктов меню в действии.



Заголовочный файл Main h

Откройте заголовочный файл main.h и следуйте за мной. Заголовочный файл в этом примере очень простой и короткий. Вот он целиком:

#ifndef MAIN_H #define MAIN_H #define STRICT #include <windows.h> #include <stdio.h> #include <D3DX9.h> #include <dmusici.h> #include <dsound.h> #include <dshow.h> #include <dxutil.h> // Прототипы функций LRESULT WINAPI fnMessageProcessor(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void vCleanup(void); bool bInitializeSoundSystem(HWND hWnd); void vPlaySound(void); // Глобальные звуковые данные IDirectMusicLoader8 *g_pLoader; IDirectMusicPerformance8 *g_pPerformance; IDirectMusicSegment8 *g_pSound; #endif

Список включаемых файлов не должен преподнести вам много сюрпризов. В основном это обычные включаемые файлы для Windows-программы. Единственный файл, добавляемый специально для DirectMusic — это dmusici.h.

В разделе прототипов функций появились две новые функции: bInitializeSoundSystem() и vPlaySound(). Назначение обоих очевидно из их названий. (Разве вы не любите самодокументируемый код?)

Следующий блок кода заголовочного файла содержит глобальные переменные, которые я использую в программе. Если вы прочитали предыдущий раздел этой главы, они должны выглядеть для вас очень знакомо. Интерфейс g_pLoader используется для загрузки звукового файла, интерфейс g_pPerformance предназначен для воспроизведения звука, а интерфейс g_pSound содержит загружаемые из звукового файла данные.

ПРИМЕЧАНИЕ Я вовсе не поощряю вас использовать глобальные переменные в коде. Я применяю их в примерах только для того, чтобы сделать код как можно проще.

Заголовочный файл Main h


Заголовочный файл main.h содержит обычный набор объявлений глобальных переменных и директив включения файлов, необходимых для примера. Вот как выглядит код секции с директивами включения файлов:

#include <windows.h> #include <stdio.h> #include <D3DX9.h> #include <dxutil.h> #include <dshow.h>

Новым в этом блоке является включение файла dshow.h. Он необходим для вызова интерфейсов и функций DirectShow. Если вы планируете использовать функциональность DirectShow, убедитесь, что этот файл есть в списке включаемых.

В следующем блоке кода находятся прототипы функций. Вот как он выглядит:

LRESULT WINAPI fnMessageProcessor(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void vCleanup(void); bool bPlayTitleMusic(void); void vStopTitleMusic(void); void vCheckMusicStatus(void);

Функция fnMessageProcessor() — это обычный обработчик сообщений Windows. Здесь нет ничего нового — все та же старая чепуха.

Функция vCleanup() вызывается перед выходом из программы. Она выполняет освобождение интерфейсов.

Функция bPlayTitleMusic() вызывается один раз в начале программы. Она инициализирует DirectShow, загружает файл MP3 и начинает его воспроизведение.

Функция vStopTitleMusic() останавливает воспроизведение музыки перед завершением работы программы.

Функция vCheckMusicStatus() проверяет не завершилось ли воспроизведенеи музыкального сегмента. Если да, воспроизведение музыки повторяется с начала.

Далее в заголовочном файле расположены глобальные переменные. Я создаю несколько указателей на интерфейсы и одну логическую переменную, как показано ниже:

bool g_bBackgroundMusicActive = 0; IGraphBuilder *g_pGraph; IMediaControl *g_pMediaControl; IMediaEvent *g_pEvent; IMediaSeeking *g_pSeeking;

Переменная g_bBackgroundMusicActive используется для отслеживания состояния музыки. Если музыка воспроизводится, ее значение равно 1. Если нет, значение равно 0.

Переменная g_pGraph является указателем на интерфейс IGraphBuilder. Эй, эй, что это за граф?



Заголовочный файл MouseZoneClass h


Итак, вы увидели класс активных зон в действии, и теперь настало время разобраться как он работает. Класс активных зон состоит из двух файлов: MouseZoneClass.h и MouseZoneClass.cpp. Откройте заголовочный файл MouseZoneClass.h и следуйте дальше.



Заголовок класса


Существует несколько моментов, которые следует рассмотреть перед созданием классов для блочной графики. Во-первых, вам необходим класс для блоков и класс для блочных карт. Класс для блоков используется для описания отдельных блоков, в то время как класс для блочных карт используется для определения групп блоков. Приведенный ниже фрагмент кода показывает пример заголовка класса для описания блоков:

class TileClass { private: int*m_iValue; intm_iNumLayers; float *m_fRotX; float *m_fSize; public: TileClass(); ~TileClass(); int iGetValue(int layer); void vSetValue(int value, int layer); float fGetRot(int layer); void vSetRotation(float fRot, int layer); float fGetSize(int layer); void vSetSize(float fSize, int layer); void vSetNumLayers(int layers); };

В классе есть четыре закрытых члена. Первый из них, m_iValue, хранит значение блока. Предположим, у вас есть 1000 загруженных в память растровых изображений, предназначенных для рисования блоков. Значение равное 1, указывает, что для блока используется первое загруженное в память растровое изображение. Таким образом, значение блока — это ни что иное, как индекс в массиве растровых изображений.

Второе значение, m_iNumLayers, хранит количество присутствующих в блоке слоев. Позволив блокам содержать различное число слоев, вы придадите системе блочной графики значительную гибкость. Позднее вы можете решить, что подобная гибкость является излишней; в этом случае достаточно просто удалить данный член класса.

Следующим в поле нашего внимания попадает член класса с именем m_fRotX. Эта переменная определяет угол поворота рассматриваемого блока. Она действительно полезна, чтобы добавить вашим картам разнообразия не добавляя нового содержимого. Чтобы создать полностью новую графику вам достаточно просто повернуть блок на 90 или больше градусов. Я использую здесь значение с плавающей точкой только потому, что в последнее время работаю исключительно с трехмерной графикой. Если вы создаете библиотеку для двухмерной блочной графики, то можете использовать здесь целое число.

Теперь мы переходим к члену данных m_fSize. Эта переменная содержит размер блока. Для трехмерного мира размер измеряется в единицах трехмерной системы координат. Для двухмерного мира размер задается в пикселах. Если вы используете блоки 64 x 64 точки, размер будет равен 64. Обратите внимание — я предполагаю, что блоки будут квадратными. Если вы решите использовать прямоугольные блоки, вам придется использовать для хранения их размера две переменные, например m_fSizeX и m_fSizeY.

Переменные класса уже продефилировали перед вами, так что теперь настало время для функций класса. Первые две функции, которые вы видите — это конструктор и деструктор класса. В них нет ничего специального — обычная чепуха С++.

Первая заслуживающая отдельного упоминания функция класса называется iGetValue(). Она применяется для получения значения указанного слоя блока. Это наиболее часто вызываемая функция класса, поскольку обращение к ней происходит каждый раз, когда рисуется данный блок карты. Приведенная в примере функция возвращает при вызове значение целочисленной переменной m_iValue. Я предпочитаю этот метод, поскольку использую возвращаемое число в качестве индекса в моем массиве данных блоков. Вы можете захотеть, чтобы функция возвращала дескриптор растрового изображения, структуру данных или какой-либо другой тип данных, используемых в вашей библиотеке.

Следующая функция класса, vSetValue(), получает два параметра. Первый параметр является целым числом, определяющим значение блока, которое вы хотите установить. Второй параметр задает слой в котором будет установлено это значение. Значения хранятся в переменной класса m_iValue.

Функция fGetRot() возвращает значение переменной класса m_fRotX. Если ваша библиотека блочной графики поддерживает вращение относительно нескольких осей, вам потребуется добавить новые переменные класса для каждой оси вращения и параметр функции fGetRot(), определяющий ваш угол зрения.

Следующая функция, vSetRotation(), получает два параметра. Первый параметр задает угол поворота в градусах. Второй параметр определяет слой, к которому относится эта информация о повороте изображения. Сделанные изменения сохраняются в переменной класса m_fRotX.

Метод fGetSize() возвращает значение переменной класса m_fSize. Если ваша библиотека блочной графики поддерживает работу с прямоугольными блоками, вам следует добавить еще одну переменную класса для хранения второго измерения, а также добавить параметр функции fGetSize(), указывающий какой именно размер необходимо получить.

Следующая функция, vSetSize(), получает два параметра. Первый параметр устанавливает размер блока в единицах трехмерной системы координат или в пикселах (для двухмерной графики). Второй параметр указывает слой, к которому эта информация относится. Полученные значения сохраняются в переменной класса m_fSize.

Последняя, но не менее важная функция класса — это vSetNumLayers(). Ей передается единственный параметр с именем layers. Главное назначение этой функции — установка количества слоев блока для хранения номеров растровых изображений, углов поворота и размеров.

Вот и все, что можно сказать о заголовке класса. Структура класса показана на Рисунок 5.34.



Загрузчик DirectMusic


Загрузчик в DirectMusic одвечает за загрузку аудио-содержимого. С его помощью вы можете загрузить файлы MIDI, файлы WAV, коллекции DLS и файлы сегментов DirectMusic. Как только вы сообщите, какой именно аудио-ресурс вам требуется загрузить, загрузчик выполнит всю необходимую работу и начнет потоковое чтение ресурса. Вам остается только выполнить воспроизведение аудиоданных!

Загрузчик использует единственный интерфейс с именем IDirectMusicLoader8. Возможно, вы удивляетесь почему в имени интерфейса DirectX 9.0 не стоит цифра 9? Дело в том, что по сравнению с 8 версией в DirectMusic не было сделано никаких существенных изменений. Большинство изменений были сделаны для увеличения быстродействия кода.

Интерфейс загрузчика содержит несколько методов, перечисленных в таблице 7.1.

Таблица 7.1. Методы интерфейса IDirectMusicLoader8

Метод Описание
CacheObject Увеличивает счетчик ссылок объекта. Полезно использовать для предотвращения многократной загрузки объекта.
ClearCache Очищает счетчик ссылок для объекта указанного типа.
CollectGarbage Очищает неиспользуемые ссылки.
EnableCache Включает автоматическое кэширование. Может также применяться для выключения автоматического кэширования.
EnumObject Перечисляет объекты заданного типа.
GetObject Возвращает объект.
LoadObjectFromFile Загружает объект из файла. Это наиболее часто используемый метод, поскольку он отвечает за загрузку файлов WAV.
ReleaseObjectByUnknown Освобождает ссылку на объект.
ReleaseObject Освобождает ссылку на объект.
ScanDirectory Выполняет поиск в каталоге файлов указанного типа. Кэширует результаты для перечисления.
SetObject Позволяет установить атрибуты некорректного объекта.
SetSearchDirectory Устанавливает путь к каталогу, в котором будет выполняться поиск аудиофайлов.

Вот что можно сказать об интерфейсе загрузчика. Позже в этой главе я покажу как используются некоторые из перечисленных в таблице 7.1 методов.



Загрузка музыкального файла


Функция IGraphBuilder::RenderFile() выполняет всю работу, необходимую для загрузки файла. Она получает два параметра: первый содержит строку с именем файла, а второй не используется. Как видно из кода в рассматриваемой программе я загружаю файл c:\dxsdk\samples\media\track3.mp3. Если у вас DirectX SDK установлен в другом каталоге, убедитесь что путь соответствующим образом скорректирован. Вы можете скорректировать имя файла, чтобы воспроизводить любую из имеющихся на вашем компьютере песен в формате MP3. Лично я указал песню Amish Paradise, которую исполняет Weird Al Yankovic. Поскольку у меня нет прав на распространение этой песни, пришлось указать файл, который, скорее всего, будет на диске вашего компьютера!

Если загрузка файла прошла успешно, функция возвращает значение S_OK. Если же при загрузке произошел сбой, вам может понадобиться консультация с документацией DirectX SDK для получения информации о возвращаемом коде ошибки.



Загрузка текстур


Буфер вершин полностью наряжен, но пока нет места куда он мог бы пойти. Как насчет нескольких текстур? Оставшаяся часть кода инициализации осуществляет загрузку текстур, необходимых программе TitleScreen. Для загрузки текстур я использую функцию D3DXCreateTextureFromFile() предоставляемую вспомогательной библиотекой DirectX. Вот как выглядит ее прототип:

HRESULT D3DXCreateTextureFromFile( LPDIRECT3DDEVICE9 pDevice, LPCSTR pSrcFile, LPDIRECT3DTEXTURE9 *ppTexture );

Первый параметр, pDevice, является указателем на устройство Direct3D, которое вы используете для визуализации. Я передаю в этом параметре созданный ранее глобальный указатель на устройство.

Второй параметр, pSrcFile, содержит имя загружаемого файла. Функция может загружать файлы различных типов, включая JPEG, TGA, BMP и PCX. В коде программы я передаю в этом параметре имена различных файлов с текстурами. Если у вас есть желание поэкспериментировать с различными типами графических файлов, вы можете изменить приведенные имена.

Третий параметр, ppTexture, является адресом указателя на объект IDirect3DTexture9, который будет хранить загруженную текстуру. Как видите в рассматриваемом примере я передаю в этом параметре глобальный массив текстур с именем g_pTexture.

Вот и все, что я хотел сказать о загрузке текстур. Как видите, стоит только привыкнуть и эта работа оказывается очень легкой.



Загрузка трехмерных моделей


Мы выделили память для объектов и инициализировали блочную карту. Пришло время загрузить трехмерные модели, содержащие данные для трехмерных блоков. Вот код, делающий это:

HRESULT CD3DFramework::InitDeviceObjects() { HRESULT hr; char szFileName[512]; // Инициализация шрифта if(FAILED(hr = m_pStatsFont->InitDeviceObjects(m_pd3dDevice))) return hr; // Загрузка информации трехмерных блоков for(int i = 0; i < g_iNumTiles; i++) { // Создаем имя файла sprintf(szFileName, "ground_tile%d.x", i+1); // Загружаем сетку if(FAILED(m_pObject[i]->Create(m_pd3dDevice, _T(szFileName)))) return D3DAPPERR_MEDIANOTFOUND; // Устанавливаем тип вершин m_pObject[i]->SetFVF(m_pd3dDevice, D3DVERTEX::FVF); } return S_OK; }

Цикл в этой функции начинается с создания имени загружаемого файла с данными трехмерного объекта. Как только имя готово к использованию, код вызывает функцию Create(), принадлежащую объекту класса CD3DMesh. Вот прототип функции CD3DMesh::Create():

HRESULT Create( LPDIRECT3DDEVICE9 pd3dDevice, TCHAR* strFilename)

Первый параметр, pd3dDevice, является указателем на используемое в приложении трехмерное устройство. Я передаю в этом параметре указатель m_pd3dDevice, который инициализируется в коде каркаса приложения.

Следующий параметр, strFilename, является именем файла, содержащего загружаемый в память объект. Здесь я передаю созданное ранее имя файла.

Теперь, когда объект загружен в память, укажем формат вершин для объекта. Выполняйте эти действия когда хотите управлять форматом вершин, используемым при визуализации объекта. Если вам это не нужно, нет никаких причин использовать SetFVF(). Данный вызов добавлен только чтобы обеспечить дополнительный контроль.



Загрузка звукового файла


Вы помните интерфейс загрузчика, который создали минуту назад? Пора снова воспользоваться им для загрузки тестового файла WAV, включенного в сопроводительные файлы. Чтобы сделать это, обратимся к функции LoadObjectFromFile(). Перед тем, как я покажу вам ее прототип, взгляните на код из программы:

// Загрузка звукового файла if (FAILED(g_pLoader->LoadObjectFromFile( CLSID_DirectMusicSegment, IID_IDirectMusicSegment8, L"testsound.wav", (LPVOID*) &g_pSound ))) { return(0); } // Загрузка данных if (FAILED (g_pSound->Download(g_pPerformance))) { return(0); }

В коде я использую функцию загрузки файла, чтобы загрузить WAV-файл с жесткого диска. Затем я загружаю сегмент в объект исполнителя для воспроизведения. Вот что видно с высоты птичьего полета.

Прототип функции LoadObjectFromFile() выглядит следующим образом:

HRESULT LoadObjectFromFile( REFGUID rguidClassID, REFIID iidInterfaceID, WCHAR *pwzFilePath, void ** ppObject );

В первом параметре, rguidClassID, должен быть передан уникальный идентификатор класса. Для загрузки в сегмент используйте идентификатор класса CLSID_DirectMusicSegment.

Второй параметр, iidInterfaceID, должен быть уникальным идентификатором интерфейса. Для загрузки в сегмент используйте IID_IDirectMusicSegment8.

Третий параметр, pwzFilePath, является именем загружаемого файла. В рассматриваемом примере я использую файл testsound.wav. Вы можете заметить букву L перед строкой с именем файла. Я поместил ее потому, что в данном параметре должно передаваться имя в кодировке Unicode (где для представления одного символа используются два байта).

В четвертом параметре, ppObject, передается адрес указателя на возвращаемый функцией интерфейс. Я использую глобальный указатель g_pSound.



Заключительная цель


Как только в Empire Earth вы достигли третьей эпохи, перед вами открывается новый набор целей. Во-первых, вы должны начать концентрировать усилия на создании армии, чтобы получить возможность нападать на противников. Другая цель состоит в исследовании жизненно важных технологий, чтобы улучшить существующую инфраструктуру.

По мере продолжения игры на горизонте появляются все новые и новые цели. Здесь содержится важный урок, который следует запомнить: вы не должны показывать игроку каждую цель заранее. Лучше постепенно подводить его к новым целям в процессе игры. Это не только не даст игроку запутаться, но и поддержит его интерес, поскольку по мере продвижения игры он сможет делать новые захватывающие вещи. Взгляните на Рисунок 3.4, где приведена полная диаграмма целей игры Empire Earth.



Закрытые члены данных класса MouseZoneClass


Далее в заголовочном файле расположено объявление класса MouseZoneClass. Ниже приведен его код:

class MouseZoneClass { private: int m_iMaxZones; stHotSpot *m_HotSpots; public: MouseZoneClass(void); ~MouseZoneClass(void); void vInitialize(int iMaxZones); void vFreeZones(void); int iAddZone(char *szZoneName, short shX, short shY, short shWidth, short shHeight, short shClickType); int iRemoveZone(char *szZoneName); bool bCheckZones(short shX, short shY, char *szZoneHit, bool bLeftDown, bool bRightDown); };

Есть только две закрытые переменные класса — m_iMaxZones и m_HotSpots. Переменная m_iMaxZones хранит количество активных зон, для которых выделена память. Это очень важные сведения, поскольку количество используемых зон может изменяться. Переменная m_HotSpots является указателем на массив структур данных stHotSpot, представляющих реально существующие активные зоны.



Заполнение буфера вершин данными


Итак, у вас в руках есть свежесозданный буфер вершин, но в нем нет никаких осмысленных данных. Что же делать программисту? Я скажу что вам требуется сделать: создайте необходимые данные вершин и поместите их в буфер!

Взгляните на следующий фрагмент кода в котором содержатся используемые программой данные вершин:

pVertices[0].position = D3DXVECTOR3(0.0f, 0.0f, 0.0f); pVertices[0].tu = 0.0f; pVertices[0].tv = 1.0f; pVertices[0].tu2 = 0.0f; pVertices[0].tv2 = 1.0f; pVertices[0].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f); pVertices[1].tu = 0.0f; pVertices[1].tv = 0.0f; pVertices[1].tu2 = 0.0f; pVertices[1].tv2 = 0.0f; pVertices[1].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[2].position = D3DXVECTOR3(1.0f, 0.0f, 0.0f); pVertices[2].tu = 1.0f; pVertices[2].tv = 1.0f; pVertices[2].tu2 = 1.0f; pVertices[2].tv2 = 1.0f; pVertices[2].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[3].position = D3DXVECTOR3(1.0f, 1.0f, 0.0f); pVertices[3].tu = 1.0f; pVertices[3].tv = 0.0f; pVertices[3].tu2 = 1.0f; pVertices[3].tv2 = 0.0f; pVertices[3].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f);

Я знаю, что необученному взгляду эти данные почти ничего не говорят. Черт побери, даже для обученного взгляда они похожи на мигрень. Вы еще не забыли Рисунок 6.18? Его обновленная версия приведена на Рисунок 6.19.



Запуск музыки


Теперь, после того, как песня остановлена и перемотана к началу, я снова запускаю ее воспроизведение с помощью испытанной функции запуска, которой я уже пользовался ранее при инициализации.

Взгляните на Рисунок 7.7, чтобы увидеть все, что выполняет наша программа.



Здания


Есть всего несколько типов зданий, которые можно выбрать, но каждый из них имеет важное значение для благосостояния цивилизации. Есть форты, фабрики, поля, школы, больницы, жилые дома и мятежники.

Вы можете спросить — как мятежники могут быть зданием? Для вас это может казаться странным, но в игре мятежные солдаты могут появляться и занимать квадраты земли. Земля, которую они занимают, не может быть использована для других целей, пока мятежники не будут уничтожены. Чтобы предотвратить захват вашей собственности мятежниками используются форты. Мятежные солдаты не могут находиться на смежных с фортом квадратах. Таким образом, если каждое из ваших зданий примыкает к форту, мятежники не смогут атаковать. Кроме того, форты защищают от пиратов. Представление форта показано на Рисунок 1.2.




Земля


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



Звуковое оформление интерфейса


Помните активную зону стартового экрана, щелчок по которой переводит игрока к основному меню? Схема кадра с этим меню показана на Рисунок 6.3.



Звуковые API


Существует несколько способов воспроизведения звука в компьютерной игре, включая использование различных свободно распространяемых API, таких как OpenAL и DirectX. Кроме того, доступны различные коммерческие библиотеки, но их стоимость может вас неприятно удивить.

На данный момент я предпочитаю пользоваться DirectX API. Вы можете спросить, почему? Девятая версия DirectX предоставляет сотни различных возможностей. Она также поддерживается практически каждым производителем оборудования. А больше всего мне нравится, что за ее использование не надо платить.

OpenAL тоже распространяется свободно, но на данный момент не может похвалиться такой широкой поддержкой производителей аппаратуры. Это вызывает проблемы с драйверами. Если ваша игра поддерживает только OpenAL, пользователи не имеющие драйверов OpenAL для своих звуковых карт скорее всего возвратят вашу игру даже не попытавшись сыграть в нее. Пока OpenAL не стал таким же распространенным, как OpenGL, я рекомендую воздержаться от его использования. Не поймите меня неправильно. Если вы действительно хотите использовать OpenAL, просто сделайте так, чтобы ваша игра могла работать как с DirectX так и с OpenAL. Тогда пользователь сможет выбирать то, что он хочет, и все останутся в выигрыше.