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

Рисование подразделений

Все эти классы великолепны, но как насчет визуализации? Если вы откроете файл main.cpp из проекта D3DFrame_UnitTemplate, я покажу вам! Спускайтесь вниз до функции vInitTileVB().

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


Рис. 8.28. Два квадрата с различными базовыми точками

Рис. 8.28. Два квадрата с различными базовыми точками


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

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


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

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


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

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

// Создание вершин
pVertices[0].position  = D3DXVECTOR3(-0.5f, -0.5f, 0.0f);
pVertices[0].tu        = 0.0f;
pVertices[0].tv        = 1.0f;
pVertices[0].vecNorm   = D3DXVECTOR3(0.0f,0.0f,1.0f);
pVertices[1].position  = D3DXVECTOR3(-0.5f, 0.5f, 0.0f);
pVertices[1].tu        = 0.0f;
pVertices[1].tv        = 0.0f;
pVertices[1].vecNorm   = D3DXVECTOR3(0.0f,0.0f,1.0f);
pVertices[2].position  = D3DXVECTOR3(0.5f, -0.5f, 0.0f);
pVertices[2].tu        = 1.0f;
pVertices[2].tv        = 1.0f;
pVertices[2].vecNorm   = D3DXVECTOR3(0.0f,0.0f,1.0f);
pVertices[3].position  = D3DXVECTOR3(0.5f, 0.5f, 0.0f);
pVertices[3].tu        = 1.0f;
pVertices[3].tv        = 0.0f;
pVertices[3].vecNorm   = D3DXVECTOR3(0.0f,0.0f,1.0f);

Код создает четыре вершины — по одной для каждого из углов квадрата. Их расположение показано на рис. 8.30.

Рис. 8.30. Координаты вершин квадрата с базовой точкой в центре

Рис. 8.30. Координаты вершин квадрата с базовой точкой в центре


На рис. 8.30 вы видите квадрат с базовой точкой, находящейся в центре. Так же там показано расположение осей X, Y и Z относительно вершин квадрата. Точки снизу и слева находятся в отрицательном пространстве, а точки сверху и справа — в положительном.

Функция vDrawUnit()

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

void CD3DFramework::vDrawUnit(
   float fXPos,
   float fYPos,
   float fXSize,
   float fYSize,
   float fRot,
   CUnitAnimation *animObj,
   int iTexture,
   int iOwner)
{
   D3DXMATRIX   matWorld;
   D3DXMATRIX   matRotation;
   D3DXMATRIX   matTranslation;
   D3DXMATRIX   matScale;

   // Установка значений по умолчанию для 
   // местоположения, масштабирования и вращения
   D3DXMatrixIdentity(&matTranslation);
   // Масштабирование блока
   D3DXMatrixScaling(&matScale, fXSize, fYSize, 1.0f);
   D3DXMatrixMultiply(&matTranslation,&matTranslation,&matScale);
   // Вращение блока
   D3DXMatrixRotationZ(&matRotation, (float)DegToRad(-fRot));
   D3DXMatrixMultiply(&matWorld, &matTranslation, &matRotation);
   // Перемещение блока
   matWorld._41 = fXPos - 0.5f; // X-Pos
   matWorld._42 = fYPos + 0.5f; // Y-Pos
   // Установка матрицы
   m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld);
   // Используем буфер вершин блока
   m_pd3dDevice->SetStreamSource(
          0, m_pVBUnit,
          0, sizeof(TILEVERTEX));
   // Используем фрмат вершин блока
   m_pd3dDevice->SetFVF(D3DFVF_TILEVERTEX);
   // Задаем используемую текстуру
   m_pd3dDevice->SetTexture(
          0, animObj->m_Textures[iTexture].m_pTexture);
   // Отображаем квадрат
   m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
   // Задаем используемую текстуру
   m_pd3dDevice->SetTexture(
          0, animObj->m_Textures[iTexture + iOwner + 1].m_pTexture);
   // Отображаем квадрат
   m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
   // Разыменовываем текстуру
   m_pd3dDevice->SetTexture(0, NULL);
}

Первое отличие данной функции от ранее рассмотренной vDrawTile() — добавление параметра вращения. Он позволяет вам развернуть двухмерное изображение на любой необходимый угол. Само вращение реализуется перемножением матриц поворота и преобразования. Матрица поворота создается вспомогательной функцией DirectX D3DXMatrixRotationZ().

СОВЕТ
В DirectX угол поворота всегда вычисляется в радианах. Для преобразования угловых величин в радианы я использую макрос DegToRad(). Вам в ваших программах также следует использовать аналогичную функцию или вращение графики и трехмерных объектов будет выполняться неправильно.

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

Самое большое отличие данной функции — добавление параметра CUnitAnimation. Он сообщает функции откуда она должна брать текстуры. Указатель на класс анимации необходим потому, что именно в нем хранятся используемые для визуализации текстуры.

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

Функция vRender()

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

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

Использование альфа-канала

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

// Включение прозрачности
m_pd3dDevice->SetRenderState(
       D3DRS_ALPHABLENDENABLE,
       TRUE);
m_pd3dDevice->SetRenderState(
       D3DRS_SRCBLEND,
       D3DBLEND_SRCALPHA);
m_pd3dDevice->SetRenderState(
       D3DRS_DESTBLEND,
       D3DBLEND_INVSRCALPHA);

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

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

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

// Цикл перебирающий подразделения
for(int i = 0; i < m_UnitManager.m_iTotalUnitObjs; i++) {
   // Устанавливаем указатель на подразделение
   ptrUnit = &m_UnitManager.m_UnitObjs[i];
   // Проверяем, активно ли подразделение
   if(ptrUnit->m_bActive) {
      // Рисуем подразделение
      vDrawUnit(
         ptrUnit->m_fXPos,
         ptrUnit->m_fYPos,
         ptrUnit->m_fScale * 128.0f,
         ptrUnit->m_fScale * 128.0f,
         ptrUnit->m_fRot,
         ptrUnit->m_Animation,
         ptrUnit->m_iCurAnimFrame,
         ptrUnit->m_iOwner
      );
   }
}

В приведенном выше коде я в цикле перебираю все подразделения, созданные в диспетчере подразделений. Если подразделение активно я вызываю функцию рисования и передаю ей параметры подразделения. Местоположение подразделения определяет в каком месте экрана оно будет находиться. Параметр, задающий поворот определяет ориентацию подразделения. Указатель анимации сообщает функции визуализации подразделения откуда ей брать данные текстуры. Номер текущего кадра анимации сообщает функции визуализации подразделения, какую именно текстуру ей следует выводить. Код владельца определяет, какая именно текстура с цветами владельца будет наложена поверх изображения подразделения. Хм-м-м... Мне кажется, я что-то забыл. Ах, да! Как вычисляется текущий кадр анимации? С помощью функции vUpdateUnits(). Взгляните на рис. 8.31, чтобы увидеть ход выполнения функции визуализации до данного момента.


Рис. 8.31. Ход выполнения функции визуализации

Рис. 8.31. Ход выполнения функции визуализации


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

Обновление кадра анимации

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

// Обновление подразделений
if(timeGetTime() > dwLastUpdateTime) {
   vUpdateUnits();
   dwLastUpdateTime = timeGetTime() + 33;
}

Данный код перед тем как вызвать функцию обновления данных подразделений снова, проверяет прошло ли 33 миллисекунды с момента ее последнего вызова. Это позволяет ограничить частоту обновления графики. Если вы не поместите в код подобный ограничитель, анимация будет некорректно воспроизводиться на системах, которые работают быстрее чем ваша. Конечно, у вас может быть наилучшая на сегодняшний день система, но что будет через пару лет? Это напоминает мне о родственнике, который в давние времена написал игру для IBM PC. В программе была собственная встроенная операционная система. Он поступил так чтобы уменьшить занимаемый объем памяти, поскольку программа содержала около двух миллионов строк ассемблерного кода! В графические вызовы он не поместил никаких задержек, из за того, что подошел к самому пределу возможностей оборудования того времени. Недавно я посетил его, он стряхнул пыль со старой пятидюймовой дискеты, вставил ее в дисковод и загрузил ту самую программу. Верите или нет, но программа, которой исполнилось более десяти лет, без проблем загрузилась и запустилась в демонстрационном режиме. Мы попытались сыграть и игру, но графические и синхронизирующие функции выполнялись настолько быстро, что на экране мы увидели мешанину из различных изображений. Это выглядело забавно, но в то же время нам стало грустно из-за того, что мы не смогли насладиться игрой. Мораль этой длинной истории такова: всегда помещайте в ваши игры таймеры. (Если вам интересно, игра называлась Chain Reaction.)

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

Обработка ожидающих подразделений

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

ptrUnit->m_iCurStillFrame++;
if(ptrUnit->m_iCurStillFrame >=
   ptrUnit->m_Animation->m_iNumStillFrames)
{
   ptrUnit->m_iCurStillFrame = 0;
}
ptrUnit->m_iCurAnimFrame =
   ptrUnit->m_Animation->m_iStartStillFrames +
   (ptrUnit->m_iCurStillFrame * (UNITMANAGER_MAXOWNERS + 1));

Сперва в коде увеличивается номер текущего кадра ожидания. Это продвигает анимационную последовательность ожидания.

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

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

Обработка поворачивающих подразделений

Второе действие представляет собой разворот подразделения. Вот предназначенный для этого код:

// Поворот
ptrUnit->m_fRot += ptrUnit->m_Movement->m_fTurnSpeed;
// Сброс, если завершен полный разворот
if(ptrUnit->m_fRot > 360.0f)
   ptrUnit->m_fRot -= 360.0f;

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

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

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

Обработка атакующих подразделений

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

ptrUnit->m_iCurAttackFrame++;
if(ptrUnit->m_iCurAttackFrame >=
   ptrUnit->m_Animation->m_iNumAttackFrames)
{
   ptrUnit->m_iCurAttackFrame = 0;
}
ptrUnit->m_iCurAnimFrame =
   ptrUnit->m_Animation->m_iStartAttackFrames +
   (ptrUnit->m_iCurAttackFrame * (UNITMANAGER_MAXOWNERS + 1));
Обработка гибнущих подразделений

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

ptrUnit->m_iCurDieFrame++;
if(ptrUnit->m_iCurDieFrame >= ptrUnit->m_Animation->m_iNumDieFrames) {
   ptrUnit->m_iCurDieFrame = 0;
}
ptrUnit->m_iCurAnimFrame =
   ptrUnit->m_Animation->m_iStartDieFrames +
   (ptrUnit->m_iCurDieFrame * (UNITMANAGER_MAXOWNERS + 1));

Обычно эта анимационная последовательность воспроизводится когда подразделение взрывается в блеске славы.

Обработка перемещающихся подразделений

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

ptrUnit->m_iCurMoveFrame++;
if(ptrUnit->m_iCurMoveFrame >=
   ptrUnit->m_Animation->m_iNumMoveFrames) {
   ptrUnit->m_iCurMoveFrame = 0;
}
ptrUnit->m_iCurAnimFrame =
   ptrUnit->m_Animation->m_iStartMoveFrames +
   (ptrUnit->m_iCurMoveFrame * (UNITMANAGER_MAXOWNERS + 1));

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

// Передвижение подразделения
ptrUnit->m_fYPos += ptrUnit->m_Movement->m_fMovementSpeed;
// Если вышли за верхнюю границу экрана, начинаем заново снизу
if(ptrUnit->m_fYPos > 360.0f)
   ptrUnit->m_fYPos = -360.0f;

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


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

Сайт управляется системой uCoz