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

         

Отображение двухмерной сетки


Простейший способ отображения блоков — использование метода двухмерной сетки. В этом методе вы отображаете на экране горизонтальные и вертикальные столбцы из блоков. Чтобы увидеть данный метод в действии, взгляните на Рисунок 5.8.



Отображение двухмерных блоков


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



Отображение двухмерных изометрических блоков


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



Отображение двухмерных изометрических блоков со спрайтами


В этой программе используется предусмотренный в DirectX 9.0 SDK интерфейс ID3DXSprite. Он упрощает процесс рисования на экране двухмерных изображений. Я заменил большую часть используемых в предыдущей программе вызовов функкций, относящихся к трехмерной графике, вызовами функций работы со спрайтами. Первое, что бросается в глаза — код стал проще и яснее. Хорошо это или плохо — решать вам. На Рисунок 5.42 показан результат работы программы, отображающей двухмерные изометрические блоки с использованием спрайтов.



Отображение изометрических блоков


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

Множество игр используют изометрические блоки, например, Age of Empires, Civilization и Command & Conquer. Основное преимущество изометрических блоков в том, что они позволяют реализовать качественный трехмерный вид без использования настоящей трехмерной графики. Однако, это уже не является причиной для беспокойства, поскольку компьютеры большинства игроков замечательно справляются с трехмерной графикой. Поэтому большинство стратегий реального времени сегодня используют трехмерную графику. Использование изометрических блоков весьма ограничено.

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



Отображение текстуры в трехмерном пространстве на экране





На Рисунок 6.20, точка с координатами (0,0) соответствует центру экрана. Это коренным образом отличается от традиционной двухмерной визуализации. В традиционной двухмерной среде изображенной на рис 6.20 точке в центре экрана соответствуют координаты (400,300). Так как вы имеете дело с трехмерным миром, вам необходимо компенсировать это различие. Для этого и пригодился код о котором я только что говорил. Он преобразует трехмерную геометрию для отображения в двухмерном пространстве экрана.

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

Теперь матрица содержит данные о масштабировании, вращении и перемещении, необходимые для визуализации. Матрицу следует активировать, для чего необходимо вызвать функцию IDirect3DDevice9::SetTransform(). Я вызываю эту функцию, которая меняет матрицу D3DTS_WORLD на матрицу, созданную ранее для отображения растра. В результате активируется новая матрица.

Затем я активирую требуемую текстуру. Для этой цели применяется вызов функции IDirect3DDevice9::SetTexture(), которой в качестве параметра передается указатель на ту текстуру, которую мы хотим отобразить.

Чтобы при визуализации использовался созданный ранее в программе буфер вершин, я должен активировать его вызвав функцию IDirect3DDevice9::SetStreamSource(). Эта функция активирует указанный буфер вершин, после чего он будет использован в качестве потокового источника для последующих вызовов функций визуализации.

Следующей вызывается функция с именем IDirect3DDevice9::SetFVF(), которая сообщает системе визуализации формат буфера вершин. В нем содержится важная информация, такая как нормаль, цвет и координаты текстуры. В параметре этой функции я передаю описание настраиваемого формата вершин D3DFVF_CUSTOMVERTEX определенное в заголовочном файле программы.

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

И, наконец, я разыменовываю текстуру, присваивая активной текстуре значение NULL.

Закончив разбирать функцию рисования элементов интерфейса, вернемся назад к функции vRender() и посмотрим, каким образом в ней реализовано рисование интерфейса. Ниже я еще раз привел отвечающий за это фрагмент кода:

vDrawInterfaceObject(0, 0, 256.0f, 256.0f, 0); vDrawInterfaceObject(256, 0, 256.0f, 256.0f, 1); vDrawInterfaceObject(512, 0, 256.0f, 256.0f, 2); vDrawInterfaceObject(0, 256, 256.0f, 256.0f, 3); vDrawInterfaceObject(256, 256, 256.0f, 256.0f, 4); vDrawInterfaceObject(512, 256, 256.0f, 256.0f, 5); vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 6);

Чтобы лучше представить себе результат этих вызовов функций, взгляните на Рисунок 6.21.



Отображение трехмерных блоков


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

Динамическое отображение. Вращение. Глубина.

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

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

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

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




Отображение трехмерных блоков! Оооо, звучит угрожающе, не так ли? Хотя название темы звучит пугающе, в действительности она почти ничем не отличается от рассмотренного в этой главе ранее отображения двухмерных блоков. Действительно, в каждом, использующем двухмерную графику примере из этой главы, применялась трехмерная визуализация. Главное видимое отличие заключается в том, что «настоящие» трехмерные программы не используют ортогональную проекцию. На Рисунок 5.43 показан результат работы программы, демонстрирующей отображение трехмерных блоков.





Отслеживание

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

Отслеживание также полезно, чтобы не забыть устранить ошибку. Во время разработки вы можете сколкнуться с проблемами, которые сочтете несущественными. Конечно, они не приведут к краху игры, но они также должны быть устранены! Лучший способ помнить о необходимости их устранения заключается в том, чтобы ввести информацию в систему отслеживания ошибок. Большинство систем отслеживания ошибок позволяет вам указывать серьезность ошибки. Это позволяет располагать ошибки по их приоритетам, чтобы сначала работать над наиболее серьезными, а потом перейти к менее серьезным.

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

СОВЕТ Один из пакетов, которые я использую для отслеживания ошибок называется TestTrack и выпущен Seapine Software. Посетите веб-сайт этой компании, расположенный по адресу www.seapine.com.

Отслеживание ошибок


Предположим, во время игры вы обнаружили ошибку в вашем коде. Что делать дальше? Вместо того, чтобы сразу исправлять ее, вам необходимо ввести данные о ней в систему отслеживания ошибок. Зачем? Использование программ отслеживания ошибок приносит много выгод. Основные из них — отслеживание, привязка к исходному коду и метрики качества.



Пальмовая роща со зданием в центре





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



Передвижение по воде


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

В игре Warcraft III нет морских подразделений, так что данный тип перемещения не будет проиллюстрирован примером.



Передвижение по воздуху


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

Одним из моих любимых воздушных подразделений в игре Warcraft III является химера. Это двухголовые летучие бестии, которых разводят ночные эльфы. Больше всего мне нравится способность химеры быстро разрушать вражеские постройки. Ее изображение приведено на Рисунок 8.3.



Передвижение по земле


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

Если вернуться к теме Warcraft, хорошим примером наземного подразделения является катапульта. Катапульта изготавливается орками и хорошо подходит для разрушения вражеских построек. Изображение катапульты приведено на Рисунок 8.2.



Переходные блоки


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



Переходные блоки для углов





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



Перехват фоновых событий


Теперь, когда все необходимые части проинициализированы и запущены, код присваивает флагу g_bBackgroundMusicActive значение 1. Благодаря этому цикл сообщений функции WinMain() узнает, что надо проверять состояние музыки. Вернитесь назад к функции WinMain() и взгляните на следующий фрагмент кода:

if(g_bBackgroundMusicActive) { vCheckMusicStatus(); }

Эй, я не говорил, что будет много кода! Так или иначе, главный цикл проверяет состояние музыки вызывая мою функцию vCheckMusicStatus().



Перемещение окна на рабочем столе





На Рисунок 6.25 окно было перемещено. Поэтому его клиентская область сместилась на 10 точек вправо и на 10 точек вниз. Щелчки мышью по активной зоне теперь регистрируются со смещением на 10 точек по обеим осям. Это вызвано тем фактом, что система передает программе данные о положении указателя мыши в пространстве рабочего стола. Проверка активной зоны использует координаты (340, 10) и не беспокоится о смещении клиентской области окна. Это вызывает проблему, поскольку активаня зона на Рисунок 6.25 в действительности расположена по координатам (350, 20). Для решения этой проблемы мы вычисляем в каком месте рабочего стола расположено окно и вычитаем его координаты из координат указателя мыши в момент щелчка. В результате мы получаем координаты в клиентской области не зависящие от местоположения окна. На Рисунок 6.25 корректировка значений снова вернет нас в точку (340, 10), поскольку вычисления будут выглядеть следующим образом: (350–10, 20–10).



Перемотка музыки


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

HRESULT SetPositions( LONGLONG *pCurrent, DWORD dwCurrentFlags, LONGLONG *pStop, DWORD dwStopFlags );

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

Следующий параметр, dwCurrentFlags, является комбинацией битовых флагов, относящихся к устанавливаемой позиции. Существует два типа флагов: флаги позиционирования и модификаторы. Здесь я использую флаг AM_SEEKING_AbsolutePositioning, чтобы сообщить системе, что позиция 0 является абсолютной, а не относительной. Названия трех других флагов описывают их назначение: AM_SEEKING_NoPositioning, AM_SEEKING_RelativePositioning и AM_SEEKING_IncrementalPositioning.

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

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



Перевернутые угловые блоки в действии





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



Первая цель в игре Empire Earth





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

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

На Рисунок 3.2 я добавил две новых цели. Первая — увеличение населения, чтобы обеспечить более быструю добычу ресурсов. Вторая — добывание пищи, необходимой чтобы кормить людей, собирающих ресурсы. О, каким порочным кругом это становится!



Первые популярные стратегии реального времени


Развитие большинства жанров видеоигр определялось появлением какой-либо популярной игры. Castle Wolfenstein и Doom сделали популярными трехмерные «стрелялки» с видом от первого лица. Игра SimCity принесла популярность экономическим симуляторам. Civilization определила развитие пошаговых стратегических игр. Путь стратегий реального времени не столь ясен, поскольку развитие жанра определили несколько игроков.



Первые стратегические игры


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



Первый экран программы D3D_MouseZones





Сейчас изображенный на Рисунок 6.23 экран должен выглядеть для вас очень знакомо. Это титульный экран игры Battle Armor. Поскольку делать на титульном экране почти нечего, я устанавливаю лишь пару активных зон. Давайте перейдем к коду функции vSetupMouseZones(), чтобы увидеть какие зоны используются на титульном экране.



Пишем первую программу для Windows

На этом я заканчиваю обсуждение теории, лежащей в основе программирования для Windows. Пришло время написать вашу первую программу для Windows. Во всех, рассматриваемых в этой книге примерах, я использую компилятор Microsoft Visual C++ 6.0. Если у вас еще нет этой программы, я настоятельно рекомендую пойти и приобрести ее.

СОВЕТ Когда я проверял последний раз, версия Visual C++ 6.0 Standard стоила около 100 долларов США. Вероятно, вы сможете приобрести ее еще дешевле, присоединившись к программе скидок для студентов. Это хорошее вложение денег.

Подсветка пунктов меню


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

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



Погружаемся и сталкиваемся с кодом


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

// Стандартный включаемый файл Windows #include <windows.h> // Прототип функции обратного вызова для обработки сообщений LRESULT CALLBACK fnMessageProcessor (HWND, UINT, WPARAM, LPARAM); // Функция вызывается автоматически, когда программа запускается int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hWnd; MSG msg; WNDCLASSEX wndclass; // Настройка класса окна wndclass.cbSize = sizeof(WNDCLASSEX); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = fnMessageProcessor; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "Window Class"; // Имя класса wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); // Регистрация класса окна if(RegisterClassEx(&wndclass) == 0) { // Сбой программы, выход exit(1); } // Создание окна hWnd = CreateWindowEx( WS_EX_OVERLAPPEDWINDOW, "Window Class", // Имя класса "Create Window Example", // Текст заголовка WS_OVERLAPPEDWINDOW, 0, 0, 320, 200, NULL, NULL, hInstance, NULL); // Отображение окна ShowWindow(hWnd, iCmdShow); // Обработка сообщений, пока программа не будет прервана while(GetMessage (&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (msg.wParam); } // Функция обратного вызова для обработки сообщений // (НЕОБХОДИМА ВСЕМ ПРОГРАММАМ ДЛЯ WINDOWS) LRESULT CALLBACK fnMessageProcessor (HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch(iMsg) { // Вызывается, когда впервые создается окно case WM_CREATE: return(0); // Вызывается, когда окно обновляется case WM_PAINT: return(0); // Вызывается, когда пользователь закрывает окно case WM_DESTROY: PostQuitMessage(0); return(0); default: return DefWindowProc(hWnd, iMsg, wParam, lParam); } }

Полеты в космосе


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

Warcraft III это игра в жанре фэнтези, так что в ней нет никаких космических подразделений.


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

enum UNIT_ATTR_MOVETYPE { MOVETYPE_LAND, MOVETYPE_SEA, MOVETYPE_AIR, MOVETYPE_SPACE };

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

Если вы хотите использовать более детализированный набор способов передвижения, используйте код, похожий на приведенный ниже:

enum UNIT_ATTR_MOVETYPE_ADV { MOVETYPE_LAND_WHEELED, MOVETYPE_LAND_TRACKED, MOVETYPE_LAND_HOVER, MOVETYPE_LAND_FOOT, MOVETYPE_SEA_SURFACE, MOVETYPE_SEA_SUBMERGED, MOVETYPE_AIR_LOW, MOVETYPE_AIR_HIGH, MOVETYPE_SPACE_INNER, MOVETYPE_SPACE_OUTER };

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





На Рисунок 6.18 вы видите четырехугольник, созданный из двух полигонов. У фигуры четыре вершины, соединенные линиями, образующими четырехугольник. Вы можете заметить, что две грани изображены пунктирной линией. Эти грани выделены по той причине, что мы используем для описания объекта полосу треугольников. Полоса треугольников требует меньшего объема памяти, чем список треугольников и зачастую быстрее отображается, поскольку центральному процессору и графическому процессору приходится выполнять меньше работы. За дополнительной информацией я рекомендую обратиться к справочной системе DirectX SDK.



Получение сообщений функцией GetMessage()


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

Чтобы проверить наличие ожидающих обработки сообщений вызывается функция GetMessage(). Вот ее прототип:

BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax );

В первом параметре функция ожидает указатель на объект MSG. Структура данных MSG содержит всю информацию о любом обнаруженном сообщении.

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

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

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



Помещение сообщений в очередь функцией DispatchMessage()


Подобно функции TranslateMessage(), функция DispatchMessage() требует единственный параметр. Цель этой функции — отправить прошедшее трансляцию сообщение в очередь сообщений программы. После вызова этой функции сообщение попадает в функцию обработки сообщений.

Последняя строка кода функции WinMain() возвращает значение wParam последнего сообщения Windows извлеченного функцией получения сообщений. Как говорил поросенок Порки, «Вот и все, ребята!».



Populous от Bullfrog


Через несколько лет после дебюта игры Utopia компания Bullfrog выпустила игру Populous. Это не была типичная стратегия реального времени, поскольку вы не могли непосредственно создавать военные соединения. Вместо этого ваши здания «выводили» большее количество жителей. Большее население увеличивало вашу власть.



Порядок двухмерных блоков





Обратите внимание, что нумерация блоков начинается с 0 в верхнем левом углу и заканчивается 99 в нижнем правом углу. Разве это не просто?

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

// Глобальный массив для карты блоков int g_iTileMap[100]; // 10*10 = 100 ячеек необходимо // Прототип функции отображения блока void vDisplayTile(int x, int y); void main() { int x, y; // Сверху вниз for(y = 0; y < 10; y++) { // Слева направо for (x = 0; x < 10; x++) { // Отображение блока vDisplayTile(x, y); } } } void vDisplayTile(int x, int y) { int iTile; int tileWidth = 64; int tileHeight = 64; int mapWidth = 10; // // Вычисляем номер блока, расположенного // по полученным координатам x и y. // iTile = g_iTileMap[(x + (y * mapWidth))]; // Отображение на экране растрового изображения // Следующая функция является фиктивной // и представляет лишь псевдокод. Чтобы код // работал вам необходимо заменить ее на // настоящую функцию рисования блока. // DrawBitmap(iTile, (x * tileWidth), (y * tileHeight)); }

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



Порядок отображения изометрических блоков





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

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



На рисунке видно как система





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


Поток выполнения программы DMusic_PlaySound





На Рисунок 7.1 видно, что WinMain() вызывает функцию bInitializeSoundSystem(). Эта функция инициализации выполняет несколько вызовов функций DirectX для инициализации звуковой системы. Затем программа ожидает события мыши и воспроизводит файл WAV с помощью функции vPlaySound(). Если вы еще этого не сделали, запустите программу и щелкните по ее окну левой кнопкой мыши чтобы воспроизвести файл WAV. Разве вам не понравился этот замечательный тестовый файл WAV? Эй, я знаю, что он не производит особого впечатления. Если вы хотите поэкспериментировать, замените файл testsound.wav одним из ваших собственных звуковых файлов. Программа должна работать с любым WAV-файлом.



Поток выполнения программы отображающей двухмерные блоки





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



Позиционирование аудиовизуального потока


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

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

Метод Описание
CheckCapabilities Проверяет, обладает ли поток указанными возможностями.
ConvertTimeFormat Преобразует из одного формата в другой.
GetAvailable Возвращает доступный диапазон значений времени для позиционирования.
GetCapabilities Возвращает возможности аудиовизуального потока.
GetCurrentPosition Возвращает текущую позицию в потоке.
GetDuration Возвращает длину потока.
GetPositions Возвращает текущую и конечную позиции.
GetPreroll Возвращает размер аудиовизуального потока, расположенного перед начальной позицией.
GetRate Возвращает темп воспроизведения.
GetStopPosition Возвращает конечную позицию. Она сообщает вам, когда воспроизведение потока будет завершено.
GetTimeFormat Возвращает используемый в данный момент формат времени.
IsFormatSupported Проверяет поддерживается ли указанный формат времени.
IsUsingTimeFormat Проверяет используется ли в данный момент указанный формат времени.
QueryPreferredFormat Возвращает предпочтительный формат времени.
SetPositions Устанавливает текущую и завершающую позиции.
SetRate Устанавливает темп воспроизведения.
SetTimeFormat Устанавливает формат времени.

В описываемой программе я использую функции SetRate() и SetPositions().



Приманка


Первая игра Populous (сейчас игр с таким названием существует по меньшей мере три) была первой популярной игрой, позволяющей игроку побыть в роли «бога». Такая власть позволяет игроку испытать гораздо больше, чем простое строительство империи. В результате строительства большего числа городов вы получаете большую цивилизацию, но, кроме того, вы получаете и магические силы. По мере возрастания этих сил вы можете использовать все более мощные заклинания. Некоторые заклинания представляют собой простые атаки, такие как удар молнии. Другие заклинания гораздо мощнее, например, вулканы. Нет ничего приятнее, чем создать вулкан посреди города ващего противника.



Применение блоков для повторного использования графики


Повторное использование графики очень важно в разработке игр, время художника также важно, как и время разработичика (если не более важно!). Взгляните на Рисунок 5.4.



Пример для изучения — Empire Earth

Чтобы прояснить разбираемый вопрос, я взял на себя смелость разбить процесс игры Empire Earth на несколько целей. Цели, которые я вношу в список, используются в однопользовательской игре против компьютера. Помните об этом, если вы когда-либо играли в эту игру в другом режиме и удивляетесь, какого черта я придумал свой список!

ПРИМЕЧАНИЕ Если вы никогда не играли в Empire Earth, я советую вам посетить сайт этой очень интересной игры, расположенный по адресу http://empireearth.sierra.com.

Пример использования класса


Ниже приводится пример использования разработанного класса блочной графики для создания карты игрового мира:

void main() { int iMapWidth = 10; int iMapHeight = 10; TileClass *Tiles; int iBMPToRender; // Выделяем память для блоков Tiles = new TileClass[(iMapWidth * iMapHeight)]; // // Цикл выполняет перебор всех блоков // и нинциализирует каждый из них // for(int i = 0; i < (iMapWidth * iMapHeight); i++) { // Выделяем для каждого блока один слой Tiles[i].vSetNumLayers(1); // Присваиваем каждому блоку значение 0 Tiles[i].vSetValue(0, 0); // Устанавливаем размер блока равным 64 пикселам Tiles[i].vSetSize(64, 0); } // // Отображение блоков с использованием // фиктивной функции визуализации // // Отображение горизонтальных рядов for(int y = 0; y < iMapHeight; y++) { // Отображение блоков в каждом ряду for(int x = 0; x < iMapWidth; x++) { // Отображение конкретного блока iBMPToRender = Tiles[x + (y * iMapWidth)].iGetValue(0); vRenderTile(x, y, iBMPToRender); } } }

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

В следующем фрагменте кода в цикле осуществляется перебор и инициализация всех только что созданных объектов TileClass. Сперва в коде количество слоев устанавливается равным 1. Это позволяет задавать для каждого блока одно значение. Затем, значение каждого блока устанавливается равным 0. Последняя часть кода устанавливает размер каждого блока, равным 64 единицам.

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

Если хотите, перед продолжением чтения поиграйтесь немного с приведенным кодом. Лично я, перед тем как перейти к следующей теме собираюсь поиграть в America's Army. Если хотите, зарегистрируйтесь и попробуйте найти меня; имя моего игрока — LostLogic.



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


Как насчет примера программы, которая использует рассмотренный только что класс? Загрузите с компакт-диска проект с именем DSound_SoundSystem и следуйте за мной дальше.

Рассматриваемый пример состоит из пяти основных файлов: main.cpp, main.h, SoundSystem.cpp, SoundSystem.h и dxutil.cpp. Файлы main.cpp и main.h содержат основной код программы, а в файлах SoundSystem.cpp и SoundSystem.h находится код класса звуковой системы. Файл dxutil.cpp содержит код полезных вспомогательных функций DirectX.

Для компиляции приложения потребуются несколько библиотек: dxguid.lib, comctl32.lib, winmm.lib и dsound.lib. Список должен выглядеть знакомо, поскольку те же самые библиотеки использовались в первом примере программы из этой главы.

На Рисунок 7.11 показано окно, выводимое данной программой.



Пример карты для размещения блоков





Пока ничего особенного, вы просто получили карту размером 100 x 100 блоков. Всего получается 10 000 блоков. Теперь представим, что в качестве карты вы решили использовать не блоки, а одно большое растровое изображение. Чтобы вычислить объем требуемой для карты памяти, вы должны умножить общее количество блоков на размер одного блока. Эту концепцию демонстрируют следующие вычисления:

100 блоков в ширину * 100 блоков в высоту = 10 000 блоков

64 точки в ширину * 64 точки в высоту = 4 096 точек в блоке

10 000 блоков * 4 096 точек * 1 байт (8 бит) = 40 960 000 байтов (256 цветов)

10 000 блоков * 4 096 точек * 4 байта (32 бита) = 163 840 000 байтов

Ничего себе! Посмотрите на результат. Простая карта, размером 100 x 100 блоков требует для своего хранения колоссального объема памяти — 163 Мбайт. Даже если вы решите ограничиться 8-разрядным цветом (256-ю цветами), все равно придется выделить 41 Мбайт только для хранения карты. Если вы не читаете эту книгу в 2008 году, 163 Мбайт только для хранения игровой карты — это слишком много.

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

100 блоков в ширину * 100 блоков в высоту = 10 000 блоков

64 точки в ширину * 64 точки в высоту = 4 096 точек в блоке

100 блоков * 4 096 точек в блоке * 4 байта на точку = 1 638 400 байт

10 000 блоков * 1 байт на блок = 10 000 байт

10 000 байт + 1 638 400 байт = 1 648 400 байт всего

Взгляните на результат. Используя набор из 100 блоков вы можете создать карту размером 100 x 100, заняв всего два мегабайта памяти. Черт, вы можете использовать набор из 1000 блоков, и вам понадобится менее 20 Мбайт памяти.

Итак, вот что мы имеем. Первая причина для использования блоков в ваших стратегических играх заключается в экономии памяти.



Пример карты из трехмерных блоков





Хм-м-м, не так уж интересно? Рисунок 5.13 не слишком отличается от карты из двухмерных блоков. Это важный момент. Трехмерная графика не означает драматического изменения вида вашей стратегической игры; она только лишь добавляет гибкости и открывает для вас новые возможности. Чтобы применять в стратегической игре трехмерную графику не требуется переход к трехмерному виду от первого лица. Вы можете придерживаться традиционного для стратегий реального времени вида и при этом использоваить трехмерную графику. В этом и заключается вся красота.



Пример ландшафтных блоков





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



Пример несбалансированности


Возьмем для примера старую игру Total Annihilation выпущенную Cavedog. В Total Annihilation, или TA как часто называют ее, игрок добывает металл и энергию. Вот это простота — всего два ресурса!

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

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



Пример сбалансированности


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



Привязка к исходному коду


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



Процесс тестирования





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



Проект DMusic_PlaySound


Программа содержит несколько исходных файлов: main.cpp, main.h и DXUtil.cpp. Все они являются уникальными для данного проекта, за исключением файла DXUtil.cpp, который является частью набора вспомогательных файлов DirectX SDK.

Проект использует следующие библиотеки: dxguid.lib, winmm.lib и dsound.lib. Вы можете спросить, почему нет библиотеки с именем dmusic.lib. Я не знаю, что вам ответить. Microsoft решила поместить функции DirectMusic в библиотеку DirectSound. Я предполагаю, что это вызвано тем, что они большей частью совместно используют одну и ту же логику.

Что-то я давно не показывал вам новых иллюстраций. Взгляните на Рисунок 7.1, где показан основной поток выполнения примера программы.



Все исходные файлы, за исключением


Программа содержит несколько файлов с исходным кодом: main.cpp, main.h и DXUtil.cpp. Все исходные файлы, за исключением DXUtil.cpp, являются уникальными для данного проекта. Кроме того, в проекте используются следующие библиотеки: dxguid.lib, winmm.lib и Strmiids.lib. Файл Strmiids.lib необходим для работы с DirectShow.


Проектирование подразделений


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

Название. Способ передвижения. Скорость передвижения. Тип атаки. Тип защиты.

Проходимость


Свойство проходимости используется для того, чтобы определить, возможно ли продвижение через данный блок. Если блок помечен как непроходимый, подразделения не могут пересекать его. Главным образом это свойство используется в алгоритмах поиска пути, поскольку процедуры поиска пути должны знать, какие части карты являются проходимыми, а какие — нет. Взгляните на пример, изображенный на Рисунок 5.35.



Простая игровая карта





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

Рисунок 5.5 проливает новый свет на Рисунок 5.4.



Простая игровая карта состоящая из блоков





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