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

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

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

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

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

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

Файл программы Main.cpp
Загрузите файл 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. Инициализация генератора случайных чисел позволяет получать различные результаты при каждом запуске программы.

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

Обратите внимание, что устанавливая блоки основного слоя я обращаюсь к первому слою массива карты, используя ссылки вида m_iTileMap[xxTileToChangexx] [0]. Располагая блоки с деталями, я обращаюсь ко второму слою, и ссылки выглядят так: m_iTileMap[xxTileToChangexx] [1]. Если вы желаете, то можете добавить еще измерения; только не забудьте проверить, что вы заполняете эти слои осмысленными значениями.

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

Прииигооотооовииитьсяяя к вииизуууааалииизааацииииии!!!!! (Мои извинения Майклу Бафферу). Сейчас действительно настало время перейти к отображению блоков, так что не будем задерживаться...

// Вертикаль
for(iY = 0; iY < m_shTileMapHeight; iY++) {
// Горизонталь
for(iX = 0; iX < m_shTileMapWidth; iX++) {
//---------------------------------------------
// ВИЗУАЛИЗАЦИЯ БАЗОВОГО СЛОЯ
//---------------------------------------------
// Вычисление номера отображаемого блока
iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][0];

// Вычисление экранных координат
fTileX = -32.0f + (iX*32.0f) - (iY*32.0f);
fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f));
// Отображение блока
vDrawTile(fTileX, fTileY, 64.0f, 32.0f, iCurTile);

//---------------------------------------------
// ВИЗУАЛИЗАЦИЯ СЛОЯ С ДЕТАЛЯМИ
//---------------------------------------------
// Вычисление номера отображаемого блока
iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][1];
if(iCurTile != 0) {
// Вычисление экранных координат
fTileX = -32.0f + (iX*32.0f) - (iY*32.0f);
fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f));
if(iCurTile == 5)
vDrawTile(fTileX, fTileY, 64.0f, 125.0f, iCurTile);
else if(iCurTile == 6)
vDrawTile(fTileX, fTileY, 67.0f, 109.0f, iCurTile);
else if(iCurTile == 8)
vDrawTile(fTileX, fTileY, 64.0f, 64.0f, iCurTile);
}
}
}

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

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

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

X-Pos = ЭкранноеСмещение + (X * ШиринаБлока) - (Y * (ВысотаБлока / 2))

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

Y-Pos = ЭкранноеСмещение - ((Y * (ШиринаБлока / 2)) + (X * (ШиринаБлока / 2)))

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

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

Вот мы и обсудили отображение изометрических блоков. А вы думали, что на это потребуется целая книга! Ох, я совсем забыл еще об одном примере программы. Уберите шампанское — нам надо посмотреть еше один вариант кода для отображения изометрических блоков.