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

         

Анимированные блоки


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

Так как же выполнить анимацию блоков? Один из простейших методов — выделить для выполнения анимации какой-нибудь диапазон блоков. Предположим, для выполнения анимаций мы выделили блоки с 1 по 100, и каждая анимационная последовательность будет состоять из 10 кадров. Это дает вам 10 анимированных блоков с 10 кадрами в каждой анимации. Если в цикле визуализации встречается блок с номером 0, 10, 20, 30, 40, 50, 60, 70, 80 или 90, программа прибавляет к номеру блока номер текущего кадра анимации, чтобы получить номер того блока, который должен быть выведен. Когда счетчик кадров анимации достигает значения 10, он сбрасывается в 0 и цикл анимации снова повторяется с самого начала. Взгляните, например, на следующий псевдокод:

Anim_Frame = 0; Loop Start = 0; Loop < #TilesToDisplay; Loop++ // Визуализация анимированных блоков If(Current_Tile.type == TYPE_ANIMATION) RenderTile(Current_Tile.value + Anim_Frame); // Визуализация обычных блоков Else RenderTile(Current_Tile.value);

// Увеличение счетчика кадров Anim_Frame++; If(Anim_Frame == 10) Anim_Frame = 0; Loop Repeat

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

netlib.narod.ru< Назад | Оглавление | Далее >



Архитектура проекта D3DFrame_Isometric2DSpriteTiles


Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK. Снова используется каркас приложения DirectX.



Архитектура проекта D3DFrame_2DTiles


Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK.

Уникальные файлы я создал специально для этого примера. Вы можете обратить внимание на префикс D3DFrame у названия проекта. Он означает, что при создании программы я пользовался предоставляемым Microsoft каркасом приложения Direct3D. Каждый раз, когда вы увидите этот префикс, знайте, что я использовал каркас приложения. Если вам не нравятся подобные каркасы, не волнуйтесь, — я покажу как выполнить ту же самую работу не применяя их.

Структура проекта показана на рис. 5.39.


Рис. 5.39. Структура файлов программы, демонстрирующей двухмерную блочную графику

Как видно на рисунке, проект включает собственные уникальные файлы, файлы каркаса приложения DirectX и библиотеки d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.





Архитектура проекта D3DFrame_3DTiles


Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfile.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK.

Обратите внимание, что название одного из файлов выделено полужирным шрифтом. Файл d3dfile.cpp отсутствовал ранее и был добавлен только к этому проекту. Его назначение — помогать загружать файлы формата .x, содержащие информацию о трехмерных объектах. Он необходим для загрузки трехмерных моделей, созданных в таких пакетах визуального моделирования, как Maya, 3D Studio MAX и MilkShape. Если хотите, вы можете написать собственный загрузчик моделей, но встроенные функции, предоставляемые файлом d3dfile.cpp сделают вашу жизнь намного проще.



Архитектура проекта D3DFrame_Isometric2DTiles


Проект содержит два уникальных файла: main.cpp и main.h. Остальные включенные в проект файлы, — d3dapp.cpp, d3denumeration.cpp, d3dfont.cpp, d3dsettings.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK. Звучит знакомо, а? Вы должны заметить повторяющуюся тему в программах, написанных с использованием каркаса приложения Direct3D. Я пытаюсь оставить вещи настолько простыми, насколько это возможно, и буду так же поступать в дальнейшем.



Блоки с изображениями дорог


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

Предположим, игрок занят расширением своей империи в вашей игре, и он хочет построить новую дорогу. Что произойдет, если он разместит блок с изображением дороги рядом с еще одним (или не одним) таким же? Будете ли вы проверять все присутствующие на карте блоки с изображением дорог, чтобы посмотреть как они изменены? Будете ли вы проверять блоки, размещенные вокруг нового добавляемого блока, чтобы посмотреть не требуют ли они изменения? Решения, решения! Чтобы лучше проиллюстрировать рассматриваемую концепцию, я привожу рис.5.25.


Рис. 5.25. Новый блок с изображением дороги помешается рядом с существующими дорогами

Итак, взглянем на рис. 5.25. На только что созданный блок с изображением дороги указывает большая стрелка. Представленная здесь проблема заключается в необходимости вычислить, каким образом должно быть повернуто изображение дороги на вновь добавляемом блоке. Мозг сообщает вам: «Элементарно — это должен быть угловой блок». (С робкой надеждой мозг спрашивает, не ошиблись ли вы в выборе профессии.) Независимо от способа, по ряду причин вы твердо уверены, что здесь необходим угловой блок.

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


Рис. 5.26. Исследуем блоки, расположенные вокруг нового

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


Соседние блоки помечены, что дальше? Теперь воспользуемся справочной таблицей, чтобы определить, какой блок должен использоваться в данной ситуации. Как создается справочная таблица? В этом нет ничего сложного: двигаясь по часовой стрелке вы назначаете каждому блоку номер, являющийся последовательно увеличивающейся степенью двойки. В этом методе первому блоку назначается номер 1, второму — 2, третьему — 4, четвертому — 8. Звучит знакомо? Надеюсь, что да, учитывая что практически все, что вы делаете на компьютере основано на этом методе! Рис. 5.27 показывает, какие значения присвоены соседним блокам.



Рис. 5.27. Соседние блоки с присвоенными номерами

Помечены два блока, поэтому вы берете назначенные им числа и складываете их вместе. В результате получается 1 + 2 = 3. Проконсультировавшись со справочной таблицей, вы увидите что блок с номером 3 — это угловой блок с изображением дороги. Не волнуйтесь о разгадывании справочной таблицы — она представлена на рис. 5.28. Счастливого Рождества!



Рис. 5.28. Справочная таблица для блоков с изображением дорог


Что такое блок?


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

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


Рис. 5.1. Мозаика из блоков на экране

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

Теперь, когда вы знаете о том, что такое блоки, как насчет нескольких примеров? Взгляните на рис. 5.2, где показан пример нескольких блоков для ландшафтной библиотеки.


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

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



Добавление деталей к блокам


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


Рис. 5.14. Три блочных карты с различным уровнем детализации

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

На второй карте добавлены камни. Это уже двухслойная карта. Первый слой содержит фоновые блоки с изображением травы, а во второй слой включены блоки с дополнительными деталями (камнями).

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

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


Рис. 5.15. Блок с изображением травы с камнями на ней

Блок на рис. 5.15 содержит и текстуру травы и текстуру камней. Это прекрасно и хорошо работает, но что случится, когда для разнообразия вы решите добавить другие фоновые блоки, например с изображением песка? В результате вам потребуется создавать новый блок и изображением камней на песке. Теперь у вас есть четыре блока: песок, трава, камни на песке и камни на траве.

Рис. 5.16 показывает, как использование слоев решает проблему и приводит к менее интенсивному расходованию ресурсов.


Рис. 5.16. Три блока: трава, песок и камни

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



Файл программы Main.cpp


Файл main.cpp не слишком сложен, поскольку он по большей части следует каркасу приложения DirectX. Первая представляющая для нас интерес функция — это конструктор класса. Вот его код:

CD3DFramework::CD3DFramework() { m_strWindowTitle = _T("2D Tile Example"); m_pStatsFont = NULL; m_shWindowWidth = 480; m_shWindowHeight = 480; m_shTileMapWidth = 10; m_shTileMapHeight = 10; }

Как видно из приведенного фрагмента кода, в конструкторе класса задается размер блочной карты. Я установил высоту и ширину карты равной 10 блокам, чтобы при выводе карта занимала все окно целиком. Ширина и высота блока равны 48 пикселам, поэтому размер окна устанавливается равным 480 на 480 точек. Поскольку 10 * 48 = 480, данный размер как раз обеспечивает точное совпадение окна и выводимой карты.

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

HRESULT CD3DFramework::OneTimeSceneInit() { m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL); if(m_pStatsFont == NULL) return E_FAIL; // Заполнение карты блоками с изображением травы memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int)); // заполнение второй половины блоками с изображением песка for(int i = 0; i < 50; i++) { m_iTileMap[i+50] = 3; } // Случайное размещение камней на траве // Инициализация генератора случайных чисел srand(timeGetTime()); for(i = 0; i < 50; i++) { // Размещение камней на траве, если случайное число = 5 if(rand()%10 == 5) m_iTileMap[i] = 1; } // Размещение переходных блоков между травой и песком for(i = 50; i < 60; i++) { m_iTileMap[i] = 2; }

return S_OK; }

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

Следующая часть кода представляет собой цикл, в котором блокам в нижней половине карты присваивается значение 3. Блок с номером 3 содержит текстуру с изображением песка; таким образом этот код формирует песчанный пляж в нижней части карты.


Загрузите файл main.cpp. Первая вещь, которая представляет для нас интерес — глобальная переменная с именем g_iNumTextures. Она содержит количество загружаемых в память текстур блоков. Я создал ее, чтобы упростить добавление блоков в программе. В настоящее время значение этой переменной равно 9. Если вы будете экспериментировать с программой и добавите свои собственные блоки, убедитесь, что соответствующим образом изменено и значение переменной.

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

m_shWindowWidth = 640; m_shWindowHeight = 320; m_shTileMapWidth = 10; m_shTileMapHeight = 10;

На этот раз я создаю окно, ширина которого равна 640 точкам, а высота — 320 точкам. Я поступаю так по той причине, что визуализация изометрических блоков слегка отличается от визуализации двухмерных квадратных блоков, и для нее требуется большая экранная область.

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

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

// Инициализация генератора случайных чисел srand(timeGetTime()); for(int i = 0; i < 100; i++) { // Заполнение базового слоя блоками с изображением травы песка и брусчатки if(rand()%10 == 3) m_iTileMap[i][0] = 2; else if(rand()%10 == 4) m_iTileMap[i][0] = 3; else m_iTileMap[i][0] = 4; // Заполнение слоя деталей деревьями и колоннами if(rand()%10 == 5) m_iTileMap[i][1] = 6; else if(rand()%10 == 4) m_iTileMap[i][1] = 5; else if(rand()%10 == 3) m_iTileMap[i][1] = 8; }

Сперва вызов функции srand() инициализирует генератор случайных чисел, используя в качестве начального значения текущее системное время. Чтобы получить текущее значение времени, я вызываю функцию timeGetTime(). Она находится в библиотеке winmm.lib и требует, чтобы в программу был включен заголовочный файл mmsystem.h. Инициализация генератора случайных чисел позволяет получать различные результаты при каждом запуске программы.




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

D3DXCreateSprite(m_pd3dDevice, &pd3dxSprite);

Просто, да? Функция D3DXCreateSprite() выполнит за вас всю работу, необходимую чтобы создать спрайтовое устройство. Ей передаются два параметра. Первый параметр — это указатель на устройство трехмерной визуализации. Второй параметр является адресом указателя на спрайтовое устройство, которое будет создано функцией.

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

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

Перед тем, как начать визуализацию, вы должны вызвать функцию спрайтового устройства Begin(). Завершать визуализацию следует вызовом функции спрайтового устройства End(). Эти вызовы полностью отличаются от того, что мы делали при работе с трехмерным устройством. Они нужны только в том случае, если вы используете спрайты, и не применяются, когда используется трехмерная визуализация.




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

HRESULT CD3DFramework::OneTimeSceneInit() { int i; m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL); if(m_pStatsFont == NULL) return E_FAIL; // Выделение памяти для блоков for(i = 0; i < g_iNumTiles; i++) { m_pObject[i] = new CD3DMesh(); } // Заполнение карты блоками с кодом 0 memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int)); // Случайное размещение скал на траве // Инициализация генератора случайных чисел srand(timeGetTime()); for(i = 0; i < 100; i++) { if(rand() % 5 == 3) m_iTileMap[i] = 1; else m_iTileMap[i] = 0; } return S_OK; }

Новые действия начинаются в первом цикле for. В нем выделяется память для трехмерных объектов (блоков). Для этой цели используется оператор new. Смотрите, разве это не просто?

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



Хранение блоков в двухмерном массиве


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

// Установка размеров карты #define TilesWide 10 #define TilesHigh 10 // Объявление массива с картой int iTileMap[TilesWide][TilesHigh]; // Заполнение всей карты блоком с номером 0 memset(&iTileMap, 0, (TilesWide * TilesHigh) * sizeof(int));

Как видите, в приведенном фрагменте кода объявлен двухмерный массив целых чисел. Поскольку и ширина и высота карты составляют 10 блоков, вся карта содержит 100 блоков. Координаты верхнего левого блока равны 0, 0, а координаты нижнего правого блока — 99, 99. Такая карта изображена на рис. 5.31.


Рис. 5.31. Блочная карта из 100 элементов

В приведенном выше фрагменте кода я очищаю карту, заполняя ее блоками с номером 0. Это общепринятая практика, поскольку блок с номером 0 является базовым блоком для визуализации. Обычно этот блок содержит изображение земли или даже является специальным блоком-пустышкой. Блок-пустышка — это обычный блок, на котором расположена предупредительная надпись, например «НЕ ИСПОЛЬЗУЕТСЯ». Наличие такого блока позволяет сразу увидеть те области карты, блоки которых не были инициализированы.

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

iTileMap[2][3] = 15;

Все, что надо сделать — присвоить желаемое значение расположенному в требуемой позиции элементу массива.

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

// Сверху вниз iTileMap[0] [0] = 15; iTileMap[0] [1] = 15; iTileMap[0] [2] = 15; iTileMap[0] [3] = 15; iTileMap[0] [4] = 15; // Слева направо iTileMap[1] [4] = 15; iTileMap[2] [4] = 15; iTileMap[3] [4] = 15; // Снизу вверх iTileMap[3] [3] = 15; iTileMap[3] [2] = 15; iTileMap[3] [1] = 15; iTileMap[3] [0] = 15; // Справа налево iTileMap[2] [0] = 15; iTileMap[1] [0] = 15;

Ответ показан на рис. 5.32.


Рис. 5.32. Карта с составленной из блоков буквой О

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

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



Хранение многослойных блоков


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

// Установка размеров карты #define TilesWide 10 #define TilesHigh 10 #define TileLayers 3

// Объявление массива для хранения карты int iTileMap[TilesWide][TilesHigh][TileLayers]; // Заполнение всей карты блоком с номером 0 memset(&iTileMap, 0, (TilesWide * TilesHigh * TileLayers) * sizeof(int));

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

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

Последнее изменение кода относится к инициализации массива. Раз у вас несколько слоев, вам требуется очистить больше элементов массива.

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


Рис. 5.33. Блочная карта с двумя слоями

Первый слой карты содержит два типа блоков с изображанеием травы. Второй слой карты содержит блоки с изображением камней. Взгляните на код, предназначенный для генерации такой карты:

// // Установка базовых блоков //

// Вертикаль for(int i = 0; i < 10; i++) { // Горизонталь for(int j = 0; j < 10; j++) { // Случайный выбор базового блока iTileMap[i][j][0] = rand() % 2; } }

// // Добавляем блоки с деталями //

iTileMap[5][5][1] = 3; iTileMap[3][9][1] = 3; iTileMap[1][7][1] = 3; iTileMap[8][8][1] = 3; iTileMap[6][3][1] = 3; iTileMap[4][1][1] = 3;

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

В следующем слое вручную размещаются блоки с изображением камней. Положение блока в массиве определяется заданными координатами [x][y][z], где координата z отвечает за слой, в котором будет находиться блок. Задавая значение z равным 1, мы указываем игре, что изображения камней помещаются во втором слое.



Использование блоков для динамического содержимого


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

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



Использование блоков для экономии памяти


Давайте для примера возьмем карту игры WarcraftIII, ширина которой равна 100 блокам, и высота также равна 100 блокам. Подобная сетка карты изображена на рис. 5.3.


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

Пока ничего особенного, вы просто получили карту размером 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.36, чтобы понять, о чем я говорю.


Рис. 5.36. Использование яркости блока для эффекта тумана войны

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



Как добавить трехмерные деревья


Что может вызвать трудности при добавлении деревьев к игре? Множество самых разных вещей. Во-первых, у деревьев есть ветви и листва, сквозь которую вы можете видеть. Во-вторых, деревья обычно колеблются под дуновениями ветра. В-третьих они должны по крайней мере казаться объемными. Графика Doom и Doom II больше не считается превосходной. Дни использования двухмерных спрайтов вместо трехмерных моделей остались далеко в прошлом. Или нет?

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

Взгляните на небольшую пальмовую рощу, изображенную на рис. 5.29.


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

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


Рис. 5.30. Разоблачение поддельных трехмерных деревьев

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



Как отображать блоки?


Теперь начинается самая интересная часть! Отображение блоков в теории выглядит очень просто, но выполнить его на практике достаточно сложно. Насколько трудным может быть отображение блоков в сетке? Вы заметили ключевое слово в предыдущем предложении? Если вы решили, что это слово «сетка»,— наградите себя аплодисментами.

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



Как создавать блоки?


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



Как вычислить местоположение в массиве


Получение местоположение блока в массиве начинается с координаты X. Рассмотрим пример нахождения в массиве блока с координатами 5,5. На рис.5.10 от блока в верхнем левом углу мы перемещаемся на пять блоков вправо. В результате мы окажемся в позиции, отмеченной на рисунке буквой A.

Теперь к текущей позиции вы должны прибавить координату Y блока, умноженную на ширину карты. Ширина карты равна десяти блокам, поэтому мы прибавляем 10 * 5 (координата Y) к текущей позиции. Следуйте по стрелке, размещенной справа от позиции, помеченной буквой A, и вы увидите, что она заканчивается в искомой позиции массива, отмеченной буквой B.

Итак, следуя рис. 5.10, мы получаем следующую формулу:

X (A) + (Y * Ширина карты) = Позиция в массиве (B)

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



Как вычислить видимую позицию


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

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

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

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

Вернемся к примеру на рис. 5.10. Координаты X и Y в пикселах для блока с координатами на карте 5, 5 будут равны (5 * 64), (5 * 64), или 320, 320. Большинство графических функций позволяют указать координаты X и Y растрового изображения, так что пример не должен вызывать никаких затруднений.

Вот как вычисляются координаты X и Y для блока в пикселях:

Координата X в пикселях = Координата X блока на карте * Ширина блока

Координата Y в пикселях = Координата Y блока на карте * Высота блока



Многослойные блоки


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

Многослойные блоки используются для добавления деталей и глубины составленной из блоков карте.



Определение необходимых блоков


Теперь, когда вы знаете размер базового блока для вашей игры, можно перейти к фактическому созданию блоков. С чего же начать? Я начинаю строительство с создания фрагментов земной поверхности для игры. Например, в играх Age of Empires и Age of Wonders от Ensemble Studios, базовыми строительными блоками библиотеки работы с ландшафтом являются трава, почва или даже снег.

Вы знаете, что следует создать базовые блоки для изображения травы, почвы, снега и т.д. Теперь, когда вы уже вступили на этот путь, следует внести разнообразие в пейзаж. Кто захочет играть на огромных полях травы, верно? Если вы выбираете в качестве базового блока изображение травы, можно подумать о добавлении к списку блоков камней, деревьев, воды, кустарников или даже небольших участков почвы. Чтобы подхлестнуть творческие способности, посмотрите на рис. 5.7.


Рис. 5.7. Несколько блоков из набора

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



Основы блочной графики


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

Что такое блок?

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

Как создавать блоки?

Как отображать блоки?



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


Мы уже там? Мы уже там? Мы уже там? ДА!!!! Мы уже там. Извините, мне просто вспомнилась последняя поездка на автомобиле, в которую я взял своих детей.

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

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

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

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

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



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


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


Рис. 5.8. Двухмерная сетка для размещения блоков

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

int x,y; // отображение сверху вниз for(y = 0; y < 10; y++) { // Отображение слева направо for(x = 0; x < 10; x++) { // Ваша функция для отображения блока vDisplayTile(x, y); } }

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


Рис. 5.9. Порядок двухмерных блоков

Обратите внимание, что нумерация блоков начинается с 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.10. Вычисление местоположения в массиве


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


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


Рис. 5.38. Окно программы, демонстрирующей визуализацию двухмерных блоков

Выглядит не слишком захватывающе? Хорошо, я понимаю, что вы уже видели десятки блочных карт, но для этой в сопроводительных файлах к книге (www.wordware.com/files/games) доступен полный исходный код, создающий ее! Загрузите проект D3DFrame_2DTiles, чтобы двигаться дальше.



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


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


Рис. 5.41. Окно программы, демонстрирующей визуализацию двухмерных изометрических блоков

Проект, содержащий код программы называется D3DFrame_Isometric2DTiles. Вы найдете его в сопроводительных файлах (www.wordware.com/files/games). Загрузите его и следуйте за мной дальше.



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


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


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

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



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


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

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

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


Рис. 5.11. Базовая карта изометрических блоков

Фактически, карта для изометрических блоков является картой для двухмерных блоков, повернутой на 45 градусов по двум осям. Это создает интересную сетку, поскольку с точки зрения зрителя блоки теперь кажутся повернутыми. Поскольку карта развернута, координаты X и Y теперь не соответствуют экранным координатам X и Y. На рис. 5.11 новые оси соответственно помечены. Обратите внимание, что ось X направлена от середины верхней части экрана к к его нижнему правому углу. Ось Y начинается также в середине верхней части экрана, но направлена к нижнему левому углу. В результате блок с координатами 0,0 расположен в центре верхней части экрана. Не слишком простой метод отображения!

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


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

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

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



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


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

Динамическое отображение.

Вращение.

Глубина.

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

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

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

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


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

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

netlib.narod.ru< Назад | Оглавление | Далее >


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


Рис. 5.43. Окно программы, демонстрирующей визуализацию трехмерных блоков

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



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


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


Рис. 5.17. Блочная карта из травы и песка

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

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


Рис. 5.18. Блочная карта с плавными переходами от травы к песку

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

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


Рис. 5.19. Блоки для переходов в направлении с юга на север

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


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



Рис. 5.20. Блоки для переходов в направлении с востока на запад

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

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



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

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



Рис. 5.22. Совместное использование угловых блоков с другими переходными блоками

Видите как угловые блоки работают вместе с другими переходными блоками при создании приятно выглядящих больших квадратных и прямоугольных областей на карте? Угловые блоки обеспечивают хорошо выглядящие скругленные углы. Теперь подумайте, что случится если область которую вы сформируете не будет квадратной или прямоугольной? Если вы предположили, что карте не хватает некоторых ключевых блоков — получите 100 очков. (Я не знаю что это за 100 очков, но полагаю, что это счет игры!) Рис. 5.23 иллюстрирует недостатки созданных к данному моменту блоков.





Рис. 5.23. Ошибки, возникающие при использовании четырех угловых блоков

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



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

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


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


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


Рис. 5.4. Простая игровая карта

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

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


Рис. 5.5. Простая игровая карта, состоящая из блоков

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



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


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

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.

netlib.narod.ru< Назад | Оглавление | Далее >



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


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


Рис. 5.35. Использование свойства проходимости при поиске пути

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



Реализация класса


Приведенный ниже фрагмент кода содержит реализацию класса для блочной графики.

#include "TileClass.h"

// Конструктор TileClass::TileClass() { // Инициализация внутренних переменных m_iNumLayers = 0; m_iValue = NULL; m_fRotX = NULL; m_fSize = NULL; } // Деструктор TileClass::~TileClass() { // Освобождаем буфер слоев, если он был выделен if(m_iValue) delete [] m_iValue; if(m_fRotX) delete [] m_fRotX; if(m_fSize) delete [] m_fSize; } // Установка количества слоев void TileClass::vSetNumLayers(int layers) { // Освобождаем ранее выделенные буферы слоев if(m_iValue) delete [] m_iValue; if(m_fRotX) delete [] m_fRotX; if(m_fSize) delete [] m_fSize; // Выделяем память для буфера слоев m_iValue = new int[layers]; memset(m_iValue, 0, layers * sizeof(int));

m_fRotX = new float[layers]; memset(m_fRotX, 0,layers * sizeof(int));

m_fSize = new float[layers]; memset(m_fSize, 0,layers * sizeof(int));

// Устанавливаем количество слоев m_iNumLayers = layers; } // Получение значения блока int TileClass::iGetValue(int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return(-1); } // Возвращаем значение return(m_iValue[layer]); } // Установка значения блока void TileClass::vSetValue(int value, int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return; } // Устанавливаем значение m_iValue[layer] = value; } // Установка угла поворота void TileClass::vSetRotation(float fRot, int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return; } m_fRotX[layer] = fRot; } // Установка размера блока void TileClass::vSetSize(float fSize, int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return; } m_fSize[layer] = fSize; } // Получение угла поворота float TileClass::fGetRot(int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return(-1.0f); } return(m_fRotX[layer]); } // Получение размера блока float TileClass::fGetSize(int layer) { // Проверяем правильность указанного номера слоя if(layer >= m_iNumLayers) { return(-1.0f); } return(m_fSize[layer]); }


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

Следующая функция — это деструктор класса. Она просто проверяет была ли выделена какая-нибудь область памяти, и, если да, то освобождает ее.

Затем следует код функции TileClass::vSetNumLayers(), устанавливающей количество слоев блока. Поскольку вы можете указать в качестве количества слоев любое осмысленное число, главная задача этой функции заключается в выделении необходимой для каждого слоя памяти. Сначала функция освобождает выделенную ранее память. Затем она выделяет память для переменных класса m_iValue, m_fRotX и m_fSize. Как только эта задача выполнена, выделенная память заполняется нулями с помощью вызова функции memset(). Помните, что эта функция должна быть вызвана перед первым использованием объекта блока. Если вы попытаетесь получить данные несуществующего слоя, класс вернет ошибку.

Далее следует наиболее часто вызываемый метод класса, TileClass::iGetValue(). Первая часть кода функции проверяет, не пытается ли программист получить значение, которого не существует. Если из кода убрать эту проверку, программа может аварийно завершиться из-за попытки обращения к несуществующей памяти. Затем функция возвращает значение переменной класса m_iValue, относящееся к указанному слою.

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

Следующие две функции, TileClass::vSetRotation() и TileClass::vSetSize, работают точно так же, как функция установки значения слоя, за исключением того, что они изменяют переменные класса m_fRotX и m_fSize.

Последние две функции, TileClass::fGetRot() и TileClass::fGetSize(), работают аналогично функции получения значения блока, за исключением того, что они возвращают значения членов данных класса m_fRotX и m_fSize.

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


Редактирование и хранение блоков


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

Хранение блоков в двухмерном массиве.

Хранение многослойных блоков.

Реализация класса для блочной графики.



Смещение


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

Предположим, вы создали дерево высотой 64 пиксела и шириной 32 пиксела. Проблема с таким делевом заключается в том, что когда вы передадите этот блок вашей процедуре визуализации, на изображении будет казаться, что дерево погрузилось в землю, поскольку оно является слишком высоким. Это иллюстрирует рис. 5.37.


Рис. 5.37. Блоки разных размеров, визуализированные без смещения

Обратите внимание, что все блоки, кроме деревьев на рис. 5.37 выглядят замечательно. Деревья же букавльно протыкают ландшафт. Это объясняется тем, что визуализация верхней границы дерева начинается в обычной позиции, но само изображение дерева слишком высокое. Чтобы решить эту проблему вы должны при визуализации деревьев учитывать смещение. Чтобы вычислить смещение, возьмите высоту обычных блоков и вычтите из нее высоту блока с изображением дерева. В приведенном примере высота дерева равна 64 пикселам, а высота обычного блока — 32 пикселам. Таким образом, смещение будет равно 32 – 64 = –32 пиксела. Теперь, при отображении вашей карты, все деревья будут выводиться в позиции Y + –32. Это перемещает изображение деревьев при визуализации на 32 точки вверх. Если желаете, можете выполнять подобные изменения, добавляя смещения по осям X и Z. В основном данный метод используется для выравнивания блоков, которые не вписываются в стандартный размер блоков, принятый в вашей игре.

netlib.narod.ru< Назад | Оглавление | Далее >



Создание класса для представления блоков


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



Свойства блоков


Ох, парни— ну и классная игра America's Army. Я набрал 62 очка в миссии Pipeline и великолепно провел время. Ну, за работу!

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

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

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

Яркость

Смещение



Точечный источник света


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

ZeroMemory(&d3dLight, sizeof(D3DLIGHT9)); d3dLight.Type = D3DLIGHT_POINT; d3dLight.Diffuse.r = 1.0f; d3dLight.Diffuse.g = 1.0f; d3dLight.Diffuse.b = 1.0f; d3dLight.Position.x = 0.0f; d3dLight.Position.y = -20.0f; d3dLight.Position.z = 20.0f; d3dLight.Attenuation0 = 1.0f; d3dLight.Attenuation1 = 0.0f; d3dLight.Range = 100.0f;

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

Значение Type указывает системе визуализации тип источника света. В данном примере я задаю значение D3DLIGHT_POINT. Оно указывает, что система визуализации должна ожидать и использовать параметры для точечного источника света.

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

Структура Position содержит местоположение источника света в трехмерном пространстве. Я разместил его в точке с координатами (0.0, –20.0, 20.0).

Значение Attentuation0 определяет, как интенсивность света будет изменяться с увеличением расстояния. Это значение устанавливает константу, с которой начнется изменение интенсивности. Значение Attenuation1 устанавливает следующую константу, используемую для изменения интенсивности. Задав масштабирование в диапазоне от 1.0 до 0.0, я указываю, что с увеличением расстояния интенсивность света должна уменьшаться.

Значение Range указывает расстояние на котором источник света перестает оказывать влияние на объекты. В нашем примере источник света не освещает предметы, которые удалены от него более, чем на 100 единиц.



Визуализация трехмерных моделей


Готовы ли вы к визуализации трехмерных блоков? Я готов! Вот новый и улучшенный код визуализации трехмерных блоков:

HRESULT CD3DFramework::Render() { D3DXMATRIX matTranslation; int iX, iY; int iCurTile; float fXPos; float fYPos;

// Очистка порта просмотра m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0L);

// Начало создания сцены if(SUCCEEDED(m_pd3dDevice->BeginScene())) { for(iY = 0; iY < 10; iY++) { // Горизонтали for(iX = 0; iX < 10; iX++) { // Вычисляем, какой блок отображать iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)]; // Вычисляем местоположение блока fXPos = (-5.0f * iX) + 22.5f; fYPos = (-5.0f * iY) + 32.5f; // Устанавливаем позицию блока D3DXMatrixTranslation(&matTranslation, fXPos, fYPos, 0.0f); m_pd3dDevice->SetTransform(D3DTS_WORLD, &matTranslation); // Отображаем блок m_pObject[iCurTile]->Render(m_pd3dDevice); } }

// Показываем частоту кадров m_pStatsFont->DrawText(2, 0, D3DCOLOR_ARGB(255,255,255,0), m_strFrameStats); // Показываем сведения о видеокарте m_pStatsFont->DrawText(2, 20, D3DCOLOR_ARGB(255,255,255,0), m_strDeviceStats);

// Завершаем создание сцены m_pd3dDevice->EndScene(); }

return S_OK; }

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

Функция D3DXMatrixTranslation() создает матрицу перемещения, содержащую набор трехмерных координат. Перемещение — это всего лишь причудливое слово, обозначающее задание местоположения. Таким образом, матрица перемещения задает местоположение объекта в трехмерном пространстве. Как только местоположение объекта установлено, вызов функции SetTransform() вводит задающую местоположение матрицу в действие.

ВНИМАНИЕ

Убедитесь, что значение первого параметра функции SetTransform() равно D3DTS_WORLD. В ином случае вы измените что угодно, но только не местоположение объекта!

Теперь объект находится в требуемой позиции, и для его отображения следует вызвать функцию Render(). Функция Render() принадлежит объекту CD3DMesh и выполняет за вас всю необходимую работу. Вам надо только передать указатель на трехмерное устройство, и объект сделает все остальное. Разве не круто?

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

netlib.narod.ru< Назад | Оглавление | Далее >



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


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



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


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

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

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

Сколько блоков будут отображаться одновременно?

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

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


Рис. 5.6. Интерфейс с видимым фрагментом 16 x 16 блоков

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

int(Ширина экрана в точках % Количество блоков в ширину)

int(Высота экрана в точках % Количество блоков в высоту)

Предположив, что минимальное разрешение экрана равно 800 x 600, мы можем вычислить, что максимально возможный размер блоков на рис. 5.6 равен (800 / 16) = 50 точек в ширину и (600 / 16) = 37 точек в высоту. Если вы придерживаетесь метода, в котором размеры блоков равны степеням двойки, то максимальный размер блоков ограничен 32 точками в ширину и 32 точками в высоту.

Если вы чувствуете, что блоки размером 32 x 32 точки слишком малы, следует пересмотреть количество одновременно отображаемых блоков карты. В действительности все зависит от того, сколько подразделений вы планируете показывать одновременно. Если ваша игра требует, чтобы игрок управлял огромными армиями, предпочтительнее использовать блоки небольшого размера. Если в игре будет всего несколько подразделений, как, например в Warcraft III от Blizzard, использование блоков большого размера не вызовет никаких проблем.



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


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



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


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



Заголовочный файл 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(); };


В заголовочный файл было внесено несколько изменений. Так я удалил код, относящийся к буферу вершин блока, поскольку в данном примере этот буфер нам не потребуется. Вместо функции vInitTileVB() и переменных класса мы будем использовать единственный указатель LPD3DXSPRITE. Вместо функции vDrawTile() мы воспользуемся функцией BltSprite(). Ее прототип выглядит так:

HRESULT BltSprite( RECT *pDestRect, LPDIRECT3DTEXTURE9 pSrcTexture, RECT *pSrcRect)

Первый параметр, pDestRect, является указателем на переменную типа RECT. Этот прямоугольник указывает функции визуализации в каком месте экрана следует отображать текстуру.

Следующий параметр называется pSrcTexture; это указатель на текстуру, которая будет выведена на экран.

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



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

#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, и т.д., находятся в той папке, куда вы установили DirectXSDK. Если вы не меняли предлагаемый путь по умолчанию, файлы находятся в папке C:\DXSDK\Samples\C++\Common.

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


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

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.



Рис. 5.34. Структура класса для блочной графики


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


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

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(). Данный вызов добавлен только чтобы обеспечить дополнительный контроль.