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

       

Анимационные наборы


К настоящему моменту у вас есть сцена, состоящая из объектов с набором ключевых кадров для выполнения анимации. Кроме этого вооружения аниматора вам необходимы анимационные наборы. Зачем? Чтобы ваша жизнь стала проще. Вы знаете что говорится: программисты по своей природе ленивы. Именно поэтому они всегда пытаются писать программы, для того чтобы упростить работу. Или чтобы не покупать изделия других программистов. Кто еще работает 26 часов в сутки? Шутка. Если серьезно, анимационные наборы — хороший способ сэкономить время при разработке игры. Взгляните на рис. 11.6, где изображены анимационные наборы в действии.


Рис. 11.6. Анимационный набор для танка

На рис. 11.6 изображены три анимационных набора для танка. Верхний называется «поворот башни влево», средний — «поворот башни вправо», а нижний — «откат орудия». Идентифицировав таким образом анимационные наборы, вы можете произвольным образом комбинировать их, чтобы оживить объекты в игре. Возьмем, к примеру, стратегическую игру в которой требуется, чтобы башня танка повернулась влево, затем возвратилась в исходное положение, после чего был бы произведен выстрел из орудия. Если попробовать сделать для этого отдельную анимацию, то в результате у вас будут тысячи, если не миллионы отдельных анимаций для изображения каждого возможного сценария. Благодаря анимационным наборам вы можете создавать небольшие простые анимации, а затем динамически объединять их. Ниже приведен список анимационных наборов, необходимых для футуристической игры с боями механоидов.

Выстрел оружия.

Взрыв.

Падение.

Прыжок.

Включение щитов.

Ходьба.

Бег.

Шаг в сторону.

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

СОВЕТ

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

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



Автоматическая вставка промежуточных кадров


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




Рис. 11.5. Автоматическая вставка промежуточных кадров при анимации танка

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



Члены данных класса C3DAnimation


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


Рис. 11.20. Массивы в классе C3DAnimation

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

Еще один одномерный массив называется m_szObjectName. Его назначение — хранить имена участвующих в анимации трехмерных объектов. Это требуется для анимации, чтобы знать какие трехмерные объекты загружать когда используется функция vLoad().

Член данных m_iNumFrames применяется для того, чтобы отслеживать количество кадров в анимации. Вам следует помнить, что для каждого ключа должен существовать кадр каждого объекта. Например, у вас есть пять объектов и в вашей анимации 30 ключей — для хранения этой анимации потребуется 150 кадров: количество объектов * количество ключей = количество кадров.

Переменная m_iNumObjects отслеживает количество задействованных в анимации объектов. Для каждого объекта необходимо задать его имя.

В символьном массиве m_szAnimName хранится имя анимации.

Член данных m_iCurFrame отслеживает номер текущего кадра. Этот номер используется при воспроизведении анимации и не сохраняется при ее записи на диск.

Член данных m_lCurTime отслеживает сколько времени прошло с момента последней смены кадра. Благодаря ему можно реализовать различные временные задержки между сменой кадров.

Переменная m_pd3dDevice хранит указатель на активное устройство визуализации Direct3D. Она необходима для загрузки файлов .x, содержащих используемые в анимации объекты.



Что еще можно сделать


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

Добавьте элементы управления для редактирования временной задержки кадра.

Добавьте возможность создавать кадры отдельно для каждого объекта.

Создйте диспетчер ресурсов для хранения моделей.

Добавьте возможность управлять камерой с помощью мыши.

Добавьте диспетчер анимационных наборов.

Добавьте в окно редактирования несколько видов сцены.

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



Деструктор класса C3DAnimation


Деструктор вызывается когда объект класса анимации удаляется или выходит из области видимости. Вот как выглядит его код:

C3DAnimation::~C3DAnimation() { // Освобождение памяти vReset(); }

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



Добавление ключевых кадров


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

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

Теперь у вас есть пачка кадров и некуда идти. Выберите антену радара, если вы еще этого не сделали, и затем перебирайте кадры, пока текущим не станет кадр с номером 2. При щелчке по кнопке Next Frame выполняется следующий код:

case ID_BUTTON_NEXTFRAME: animTest.iNextFrame(); SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;

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

Теперь, когда у вас выбран второй кадр, измените поворот антены радара, чтобы ее центр был направлен немного влево. Сделав это, перейдите к третьему кадру и поверните антену еще на несколько градусов. Продолжайте процесс, пока не вернетесь к кадру номер 1. Затем щелкните по кнопке Start/Stop Anim, чтобы воспроизвести небольшую анимацию, которую вы только что создали. Будет выполнен следующий код:

case ID_BUTTON_STARTSTOP: // Если анимация активна, остановить ее if(g_iAnimActive) { g_iAnimActive = 0; } // Если анимация остановлена, запустить ее else { g_iAnimActive = 1; } SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;

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



Фиксированные объекты


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



Функция C3DAnimation::iNewObj()

Функция создания нового объекта получает имя файла .x и загружает его в ближайший доступный слот объекта. Вот как выглядит код функции:

int C3DAnimation::iNewObj(char *szObjName) { char szFileName[512]; // Создаем имя файла sprintf(szFileName, "Data\\3DObjects\\%s.x", szObjName); // Устанавливаем указатель на объект m_objObject[m_iNumObjects] = new Object3DClass; m_objObject[m_iNumObjects]->hLoad(szFileName, m_pd3dDevice); // Сохраняем имя для последующего использования strcpy(&m_szObjectName[m_iNumObjects][0], szObjName); // Увеличиваем внутренний счетчик m_iNumObjects++; // Возвращаем количество объектов return(m_iNumObjects); }

В первой части функции создается полностью квалифицированное имя файла. Оно включает путь и имя файла.

В следующем фрагменте кода создается новый объект Object3DClass для хранения данных модели из файла .x. Затем только что созданный объект загружает данные из файла .x с помощью собственной функции hLoad().

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

ПРИМЕЧАНИЕ

Класс Object3DClass создан мной для того, чтобы упростить работу с моделями из файлов .x.

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



Функция C3DAnimation::iNextFrame()


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

int C3DAnimation::iNextFrame(void) { // Переход к следующему кадру m_iCurFrame++; // Если кадр был последним, переходим к началу анимации if(m_iCurFrame >= m_iNumFrames) { m_iCurFrame = 0; } // Возвращаем номер кадра return(m_iCurFrame); }

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



Функция C3DAnimation::iPrevFrame()


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

int C3DAnimation::iPrevFrame(void) { // Переход к предыдущему кадру m_iCurFrame--; // Если номер кадра меньше нуля, переходим к последнему кадру. // Если кадров нет, переходим к нулевому кадру if(m_iCurFrame < 0) { // Проверяем есть ли кадры if(m_iNumFrames) { // Переход к последнему кадру m_iCurFrame = m_iNumFrames - 1; } // Кадров нет else { // Переход к нулевому кадру m_iCurFrame = 0; } } // Возвращаем номер кадра return(m_iCurFrame); }

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


Рис. 11.21. Выполнение функции перехода к предыдущему кадру



Функция C3DAnimation::iStartFrame()


Функция перехода к начальному кадру просто перематывает анимацию до первого кадра. Вот ее код:

int C3DAnimation::iStartFrame(void) { // Переход к первому кадру m_iCurFrame = 0; // Возвращаем номер кадра return(m_iCurFrame); }

Я не буду объяснять самодокументируемый код. Разве вы любите это?



Функция C3DAnimation::vLoad()


Функция загрузки читает ранее сохраненные функцией записи данные. Звучит просто, не так ли? А вот и код функции:

void C3DAnimation::vLoad(char *szFileName) { FILE *fp; int i, j; int iNumObjs; int iNumFrames; char szFullFileName[512];

// Создание квалифицированного имени файла sprintf(szFullFileName, "Data\\Anims\\%s.anim", szFileName); // Открытие файла для чтения fp = fopen(szFullFileName, "rb"); if(fp == NULL) { return; } // Сброс объектов в состояние по умолчанию vReset(); // Чтение заголовка // Количество объектов fread(&iNumObjs, 1, sizeof(int), fp); // Количество кадров fread(&iNumFrames, 1, sizeof(int), fp); // Загрузка информации об объектах for(i = 0; i < iNumObjs; i++) { // Чтение имени объекта fread(&m_szObjectName[i][0], 32, sizeof(char), fp); // Загрузка данных объекта iNewObj(&m_szObjectName[i][0]); } // Выделение памяти для кадров for(i = 0; i < iNumFrames; i++) { vNewFrame(); } // Чтение информации о ключевом кадре for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { // Задержка fread(&m_keyFrames[i][j]->m_lTimeDelay, 1, sizeof(long), fp); // Вращение fread(&m_keyFrames[i][j]->m_vecRot, 1, sizeof(D3DXVECTOR3), fp); // Масштаб fread(&m_keyFrames[i][j]->m_vecScale, 1, sizeof(D3DXVECTOR3), fp); // Местоположение fread(&m_keyFrames[i][j]->m_vecTrans, 1, sizeof(D3DXVECTOR3), fp); } } // Закрываем файл анимации fclose(fp); // Сохраняем имя анимации strcpy(m_szAnimName, szFileName); }

Ход выполнения функции загрузки показан на рис. 11.23.


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

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

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

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

И в самом конце код закрывает файл и сохраняет имя анимации в данных класса анимации. Вот и все!



Функция C3DAnimation::vNewFrame()


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

void C3DAnimation::vNewFrame(void) { int iFrame = 0; stKeyFrame *ptrFrame; stKeyFrame *ptrPrevFrame;

// Увеличение количества кадров в анимации m_iNumFrames++; // Получение индекса ключевого кадра iFrame = m_iNumFrames - 1; // Создаем новый кадр для каждого объекта // в анимации. for(int iObj = 0; iObj < m_iNumObjects; iObj++) { // Выделяем память для кадра m_keyFrames[iObj][iFrame] = new stKeyFrame; // Получаем указатель на новый кадр ptrFrame = m_keyFrames[iObj][iFrame]; // Присваиваем первому кадру значения по умолчанию if(iFrame == 0) { ptrFrame->m_vecScale = D3DXVECTOR3(1.0, 1.0, 1.0); ptrFrame->m_vecRot = D3DXVECTOR3(0.0, 0.0, 0.0); ptrFrame->m_vecTrans = D3DXVECTOR3(0.0, 0.0, 0.0); ptrFrame->m_lTimeDelay = 10; } // Остальным кадрам присваиваем значения, // скопированные из предыдущего кадра else { // Получаем указатель на предыдущий кадр ptrPrevFrame = m_keyFrames[iObj][(iFrame - 1)];

// Копируем данные из предыдущего кадра в текущий ptrFrame->m_vecScale = ptrPrevFrame->m_vecScale; ptrFrame->m_vecRot = ptrPrevFrame->m_vecRot; ptrFrame->m_vecTrans = ptrPrevFrame->m_vecTrans; ptrFrame->m_lTimeDelay = ptrPrevFrame->m_lTimeDelay; } } m_iCurFrame = iFrame; }

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

Затем я инициализирую временную переменную, присваивая ей значение на единицу меньшее количества кадров. В результате значение переменной iFrame будет равно номеру создаваемого кадра.

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

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



Функция C3DAnimation::vReset()


Функция vReset() вызывается для установки содержимого объекта анимации в исходное состояние. Она полезна, когда требуется загрузить новую анимацию поверх существующего объекта. Также она используется при уничтожении объекта анимации. Вот ее код:

void C3DAnimation::vReset(void) { int i, j;

// Освобождение объектов for(i = 0; i < m_iNumObjects; i++) { if(m_objObject[i]) { delete m_objObject[i]; m_objObject[i] = NULL; } }

// Освобождение данных ключевых кадров for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { if(m_keyFrames[i][j]) { delete m_keyFrames[i][j]; m_keyFrames[i][j] = NULL; } } }

// Установка количества объектов m_iNumObjects = 0; // Установка количества кадров m_iNumFrames = 0; // Установка начального состояния анимации m_iCurFrame = 0; m_lCurTime = 0;

// Инициализация объектов, имен и информации о ключах for(i = 0; i < g_iMaxObjects; i++) { // Объекты m_objObject[i] = NULL; // Имена strcpy(&m_szObjectName[i][0], ""); // Ключи for(j = 0; j < g_iMaxKeys; j++) { m_keyFrames[i][j] = NULL; } } }

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

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

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

Заключительный цикл очищает строки с именами трехмерных объектов.



Функция C3DAnimation::vSave()


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

void C3DAnimation::vSave(char *szFileName) { FILE *fp; int i, j; char szFullFileName[512];

// Создаем квалифицированное имя файла sprintf(szFullFileName, "Data\\Anims\\%s.anim", szFileName); // Открываем файл для записи fp = fopen(szFullFileName, "wb"); if(fp == NULL) { return; } // Вывод заголовка // Количество объектов fwrite(&m_iNumObjects, 1, sizeof(int), fp); // Количество кадров fwrite(&m_iNumFrames, 1, sizeof(int), fp); // Вывод имен объектов for(i = 0; i < m_iNumObjects; i++) { fwrite(&m_szObjectName[i][0], 32, sizeof(char), fp); } // Вывод информации о ключевых кадрах for(i = 0; i < m_iNumObjects; i++) { for(j = 0; j < m_iNumFrames; j++) { // Задержка fwrite(&m_keyFrames[i][j]->m_lTimeDelay, 1, sizeof(long), fp); // Поворот fwrite(&m_keyFrames[i][j]->m_vecRot, 1, sizeof(D3DXVECTOR3), fp); // Масштаб fwrite(&m_keyFrames[i][j]->m_vecScale, 1, sizeof(D3DXVECTOR3), fp); // Местоположение fwrite(&m_keyFrames[i][j]->m_vecTrans, 1, sizeof(D3DXVECTOR3), fp); } } // Закрываем файл анимации fclose(fp); // Сохраняем имя анимации strcpy(m_szAnimName, szFileName); }

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


Рис. 11.22. Структура формата файла анимации

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

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

Далее следует информация о кадрах. Код в цикле перебирает все кадры анимации и выводит информацию о задержке, вращении, масштабировании и местоположении каждого присутствующего в кадре объекта. Цикл продолжается до тех пор, пока не будет выведена информация обо всех кадрах.

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



Функция C3DAnimation::vSet3DDevice()


Функция задания трехмерного устройства применяется для установки внутреннего указателя на устройство Direct3D. Этот указатель необходим для загрузки трехмерных моделей из X-файлов. Вот код функции:

void C3DAnimation::vSet3DDevice(LPDIRECT3DDEVICE9 pd3dDevice) { m_pd3dDevice = pd3dDevice; }



Функция C3DAnimation::vUpdateRot()


Функция изменения поворота получает вектор поворота, номер кадра и номер объекта и и добавляет значение вращения к текущему вектору поворота заданного объекта в указанном кадре. Ее хорошо применять для изменения угла поворота объектов. Вот код функции:

void C3DAnimation::vUpdateRot(int iObj, int iKey, D3DXVECTOR3 vecRot) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecRot += vecRot; } }

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



Функция C3DAnimation::vUpdateScale()


Функция изменения масштаба получает вектор масштабирования, номер кадра и номер объекта и и добавляет значение масштабирования к текущему вектору масштабирования заданного объекта в указанном кадре. Функция полезна для изменения масштаба объектов. Вот ее код:

void C3DAnimation::vUpdateScale( int iObj, int iKey, D3DXVECTOR3 vecScale) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecScale += vecScale; } }

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



Функция C3DAnimation::vUpdateTrans()


Функция изменения позиции получает вектор перемещения, номер кадра и номер объекта и добавляет значение перемещения к текущему вектору местоположения заданного объекта в указанном кадре. Ее полезно использовать для изменения местоположения объектов. Вот как выглядит код функции:

void C3DAnimation::vUpdateTrans( int iObj, int iKey, D3DXVECTOR3 vecTrans) { // Проверяем правильность номеров ключа и объекта if(iObj < m_iNumObjects && iObj >= 0 && iKey < m_iNumFrames && iKey >= 0) { // Обновляем вектор m_keyFrames[iObj][iKey]->m_vecTrans += vecTrans; } }

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



Функция vInitAnimation()


Функция инициализации анимации всего лишь устанавливает внутренний указатель на устройство Direct3D, присваивая ему значение, полученное при вызове функции InitD3D(). Вот как выглядит код этой процедуры:

void vInitAnimation(void) { // Установка трехмерного устройства animTest.vSet3DDevice(g_pd3dDevice); }

В функции я обращаюсь к глобальному объекту класса анимации. Функция vSet3DDevice() вызывается чтобы установить его внутренний указатель на устройство визуализации. Я делаю это потому, что объекту класса анимации необходим указатель на устройство Direct3D для загрузки трехмерных объектов, образующих сцену. Вам надо вызвать эту функцию только один раз, так что процедура инициализации — самое подходящее для этого место.



Функция vLoadObject()

Чтобы начать редактирование анимации, вам необходима анимируемая сцена. Как я говорил ранее, сцена состоит из трехмерных объектов, поэтому вы должны загрузить какие-нибудь объекты для редактирования. Здесь в игру вступает функция vLoadObject(). Она загружает составляющие сцену объекты. Когда вы щелкаете по расположенной на панели команд кнопке LoadObjects выполняется следующий код:

void vLoadObject(void) { // Сброс текущей анимации animTest.vReset(); // Загрузка заданных объектов animTest.iNewObj("droid_still"); animTest.iNewObj("radar_dish"); // Визуализация сцены vRender(); // Обновление панели команд vUpdateToolbarStats(); }

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

ВНИМАНИЕ!

Не щелкайте по кнопке Load Objects, если у вас есть не сохраненные данные анимации. Все они будут стерты!

Следующий фрагмент кода обращается к функции создающему новый объект методу класса анимации и загружает объекты из двух X-файлов с именами droid_still и radar_dish. Объект droid_still — это механоид, а объект radar_dish — это небольшая антена радара.

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

В конце я вызываю функцию vUpdateToolbarStats(). Она выводит в панели инструментов значения вращения и местоположения объекта в текущем кадре.



Функция vRender()


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

// Очистка вторичного буфера синим цветом g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_RGBA(200, 250, 255, 255), 1.0f, 0); // Начало сцены g_pd3dDevice->BeginScene(); // Установка материала по умолчанию g_pd3dDevice->SetMaterial(&g_mtrlDefault); // Установка режима сплошной заливки g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); // Визуализация пола vDraw3DObject(D3DXVECTOR3( 0.0, 0.0, 0.0) , D3DXVECTOR3(200.0, 200.0, 200.0), D3DXVECTOR3( 90.0, 0.0, 0.0), 0); // Визуализация трехмерных объектов if(animTest.m_iNumFrames && animTest.m_iNumObjects) { for(int i = 0; i < animTest.m_iNumObjects; i++) { // Объекты не являющиеся текущими отображаем в каркасном режиме if(i != g_iCurObj) { g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); } // Текущий объект отображаем в режиме сплошной заливки else { g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); } // Установка текущего кадра iCFrame = animTest.m_iCurFrame; // Визуализируем объекты, используя информацию кадра // хранящуюся в объекте анимации animTest.m_objObject[i]->vDisplayXYZ( animTest.m_keyFrames[i][iCFrame]->m_vecTrans.x, animTest.m_keyFrames[i][iCFrame]->m_vecTrans.y, animTest.m_keyFrames[i][iCFrame]->m_vecTrans.z, animTest.m_keyFrames[i][iCFrame]->m_vecRot.x, animTest.m_keyFrames[i][iCFrame]->m_vecRot.y, animTest.m_keyFrames[i][iCFrame]->m_vecRot.z, animTest.m_keyFrames[i][iCFrame]->m_vecScale.x, animTest.m_keyFrames[i][iCFrame]->m_vecScale.y, animTest.m_keyFrames[i][iCFrame]->m_vecScale.z); // Анимация объекта if(g_iAnimActive) { animTest.m_lCurTime++; // Переход к следующему кадру if(animTest.m_lCurTime >= animTest.m_keyFrames[i] [animTest.m_iCurFrame]->m_lTimeDelay) { animTest.m_iCurFrame++; bFrameChanged = 1; animTest.m_lCurTime = 0; // Сброс счетчика кадров if(animTest.m_iCurFrame >= animTest.m_iNumFrames) { animTest.m_iCurFrame = 0; } } } } }


Я знаю, что код визуализации объектов может выглядеть сложным, но все не так плохо. Сперва я вызываю мою функцию vDraw3DObject() чтобы нарисовать пол. Если вы не заметили, пол я рисую для того, чтобы облегчить расположение объектов. Он представляет собой большую серую решетку, расположенную вдоль оси Y.

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

Поскольку анимационная сцена может содержать несколько объектов, я создал цикл, который перебирает каждый объект сцены. Логика цикла изображена на рис. 11.25.



Рис. 11.25. Логика визуализации

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

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

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


Готовые редакторы анимации


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

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



Импорт содержимого

Поскольку для анимации абсолютно необходимы трехмерные объекты, следует рассмотреть несколько способов создания моделей. Обычно эта работа выполняется с помощью профессиональных пакетов трехмерного моделирования, таких как Softimage, Maya или 3dsmax. Есть несколько свободно распространяемых или более дешевых программ моделирования, таких как trueSpace, MilkShape и Rhino.

СОВЕТ

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

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



Интерфейс редактора анимации


Для редактора анимации вам потребуется область редактирования и панель инструментов для кнопок команд. Эти базовые компоненты интерфейса показаны на рис. 11.15.


Рис. 11.15. Интерфейс редактора анимации

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



Изменение местоположения объекта


Теперь, когда антена радара выбрана, необходимо переместить ее. Возможно, вы уже заметили, что антена находится на земле, а не наверху механоида! Это легко исправить, ведь все что надо сделать — ввести соответствующие значения в текстовых полях, задающих местоположение объекта. В данном случае нам нужно среднее поле в левом столбце. Укажите в нем значение 20.0, и увидите как антена займет свое место на верхушке робота. Вуаля! Разве не здорово? Если все сделано правильно, вы увидите антену радара на предназначенном для нее месте. Код, обрабатывающий это перемещение, выглядит так:

case ID_EDITBOX_Y: memset(szBuffer, 0x00, 32); GetWindowText(hEDITBOX_Y, szBuffer, 32); if(animTest.m_iNumFrames > 0) { animTest.m_keyFrames [g_iCurObj][animTest.m_iCurFrame]->m_vecTrans.y = (float)atof(szBuffer); } break;

В коде я извлекаю значение из поля редактирования и преобразую его в число с плавающей запятой. Затем я устанавливаю новое значение смещения по оси Y выбранного объекта в текущем кадре. Фактически, все что я сделал — взял новое значение местоположения из поля редактирования и применяю его к антене радара. Здорово?

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

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



Экспорт из 3ds max


Парни в Microsoft были достаточно любезны и снабдили вас исходным кодом программы экспорта файлов из 3ds max в DirectX. Его можно загрузить со страницы DirectX SDK и он замечательно работает. Скомпилировав и установив его вы сможете экспортировать модели 3ds max несколькими щелчками мыши. Не хотите взглянуть на пример экспорта?



Элементы управления программы D3D_AnimationEditor


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

Вы можете остановить воспроизводимую анимацию, еще раз щелкнув по кнопке Start/Stop Anim. Фактически эта кнопка переключает состояние анимации между воспроизведением и паузой. Пока анимация воспроизводится вы можете даже добавлять в нее ключевые кадры и изменять данные. Это подводит меня к следующему набору элементов управления — элементы управления кадрами. На панели команд есть три команды для работы с кадрами: Prev, Next и New. Щелчок по кнопке Prev Frame приводит к переходу от текущего кадра анимации к предшествующему в списке. Если вы достигли начала анимации, последовательность кадров замыкается и вы перейдете к последнему кадру последовательности. Перебирая кадры вы будете видеть их данные, выводимые в области отладочной информации главного окна редактирования. Кроме того, данные местоположения и вращения появляются в текстовых полях, расположенных в окне панели команд.

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

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

Следом на панели инструментов расположены команды для работы с объектами: Prev Obj, Next Obj и Load Objects. Щелчок по кнопке Prev Obj приведет к тому, что текущим станет объект, который был добавлен к сцене перед выбранным в данный момент. Если вы достигли начала списка объектов, текущим станет последний добавленный к сцене объект. Кнопка Next Obj работает точно так же, но перебирает объекты в прямом, а не в обратном направлении. В рассматриваемом примере есть всего лишь два объекта (механоид и антена радара). Кнопка Load Objects используется для загрузки в сцену трехмерных объектов. Чтобы упростить пример я жестко запрограммировал эту кнопку на загрузку механоида и антены радара. В реальном редакторе вам надо будет добавить возможность выбора произвольного объекта для загрузки, а не прописывать все в коде.

И, наконец, кнопки Load Anim и Save Anim. Кнопка Load Anim загружает файл анимации с именем RobotIdle. Он содержит анимацию, специально созданную мной для данного примера. В реальном редакторе вам надо указывать имя загружаемого файла, а в примере для простоты я его жестко прописал в коде. Кнопка Save Anim записывает данные о созданной в редакторе анимации в файл с именем RobotIdle. Будьте очень аккуратны, чтобы по неосторожности не перезаписать файл!



Класс C3DAnimation


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


Рис. 11.19. Структура класса C3DAnimation

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

const int g_iMaxObjects = 16; const int g_iMaxKeys = 1024;

struct stKeyFrame { D3DXVECTOR3 m_vecRot; D3DXVECTOR3 m_vecTrans; D3DXVECTOR3 m_vecScale; long m_lTimeDelay; };

class C3DAnimation { public: stKeyFrame *m_keyFrames[g_iMaxObjects][g_iMaxKeys]; Object3DClass *m_objObject[g_iMaxObjects]; char m_szObjectName[g_iMaxObjects][32]; int m_iNumFrames; int m_iNumObjects; char m_szAnimName[64]; int m_iCurFrame; long m_lCurTime; LPDIRECT3DDEVICE9 m_pd3dDevice; C3DAnimation(); ~C3DAnimation(); void vNewFrame(void); int iNextFrame(void); int iPrevFrame(void); int iStartFrame(void); int iNewObj(char *szObjName); void vUpdateTrans(int iObj, int iKey, D3DXVECTOR3 vecTrans); void vUpdateRot(int iObj, int iKey, D3DXVECTOR3 vecRot); void vUpdateScale(int iObj, int iKey, D3DXVECTOR3 vecScale); void vSave(char *szFileName); void vLoad(char *szFileName); void vSet3DDevice(LPDIRECT3DDEVICE9 pd3dDevice); void vReset(void); };



Ключевые кадры


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


Рис. 11.3. Два кадра танка

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


Рис. 11.4. Более детальная анимация танка

Здесь в анимации башни больше ключевых кадров. Вместо двух их стало шесть. Это повышает качество анимации.



Команды работы с файлами


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



Команды работы с кадрами


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



Команды работы с объектами


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



Команды редактора анимации


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

Команды работы с кадрами

Команды работы с объектами

Команды воспроизведения

Команды работы с файлами



Команды воспроизведения


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



Конструктор класса C3DAnimation


Конструктор вызывается в момент создания нового объекта класса анимации. Вот как выглядит его код:

C3DAnimation::C3DAnimation() { int i, j; // Установка количества объектов m_iNumObjects = 0; // Установка количества кадров m_iNumFrames = 0; // Установка начального состояния анимации m_iCurFrame = 0; m_lCurTime = 0; // Инициализация объектов, имен и информации о ключах for(i = 0; i < g_iMaxObjects; i++) { // Объекты m_objObject[i] = NULL; // Имена strcpy(&m_szObjectName[i][0], ""); // Ключи for(j = 0; j < g_iMaxKeys; j++) { m_keyFrames[i][j] = NULL; } } }

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



Методы класса C3DAnimation


Методы класса анимации реализуют базовый набор действий, необходимых для редактирования. Ниже приводится их краткое описание.

Функция C3DAnimation()— это стандартный конструктор класса, который инициализирует члены данных класса анимации.

Функция ~C3DAnimation() — это деструктор, освобождающий выделенную память при уничтожении объекта.

Функция vSave() вызывается для записи данных анимации в указанный файл.

Функция vLoad() вызывается для загрузки указанного файла анимации.

Функция vSet3Ddevice() применяется для установки внутреннего указателя на устройство Direct3D.

Функция vReset() освобождает всю выделенную для объекта анимации память и выполняет инициализацию данных. Ее вызывает конструктор во время инициализации объекта.

Функция vNewFrame() создает новый кадр для каждого объекта анимации.

Функция iNextFrame() осуществляет переход к следующему кадру.

Функция iPrevFrame() осуществляет возврат к предыдущему кадру.

Функция iStartFrame() выполняет возврат анимации к начальному кадру.

Функция iNewObj() добавляет трехмерный объект к анимируемой сцене.

Функция vUpdateTrans() получает вектор и прибавляет его к текущему вектору местоположения выбранного объекта в заданном кадре.

Функция vUpdateRot() получает вектор и прибавляет его к текущему вектору вращения выбранного объекта в заданном кадре.

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



Объекты


Объекты— это источник жизненной силы трехмерной анимации. Без объектов не было бы ни сцен ни анимации. Что такое объект? В этой главе объектом будет называться трехмерная модель, состоящая из полигонов, цветов и текстур. Возьмем к примеру механоида, упомянутого мной минутой раньше. Механоид, как вы хорошо знаете, представляет собой бронированного боевого робота. К тому же у рассматриваемого в примере механоида наверху есть вращающаяся антена радара. Поскольку антена вращается независимо от движения тела механоида, она является отдельным объектом. Итак, в этом примере у нас два трехмерных объекта: тело механоида и антена радара. Хотя эти два объекта всегда существуют вместе, они должны быть отдельными, чтобы вы могли анимировать их независимо друг от друга. Другой пример, танк, изображен на рис. 11.2.


Рис. 11.2. Танк

Как видите, танк состоит из башни, корпуса, гусениц и колес. Поскольку башня может вращаться независимо от корпуса, она должна быть отдельным объектом. То же самое справедливо для гусениц и колес.



Обзор трехмерной анимации


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

Сцена

Объекты

Ключевые кадры

Анимационные наборы



Основы моделирования


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


Рис. 11.7. Интерфейс программы 3ds max

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

Поскольку в DirectX высота объекта задается вдоль оси Y, выберите в 3ds max окно вида сверху (Top view) и нажмите клавишу W, чтобы развернуть его на весь экран. В результате интерфейс программы будет выглядеть так, как изображено на рис. 11.8.


Рис. 11.8. Вид сверху в 3ds max

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


Рис. 11.9. Контур бокала для вина

На рис. 11.9 изображен фрагмент поперечного сечения бокала. Я знаю, что он выглядит достаточно угловато, но имея дело с трехмерной графикой в реальном времени надо стараться уменьшить количество полигонов в моделях. Если вы ошиблись, размещая вершины, отредактируйте модель, выбрав пункт Edit Mesh в списке Modifier List. Перемещайте вершины, пока не будете удовлетворены получившимся контуром, а затем выберите пункт Lathe в списке Modifier List. Если вы все сделали правильно, то увидите картинку, похожую на рис. 11.10.


Рис. 11.10. Результат вращения контура бокала

На рис. 11. 10 изображен результат вращения контура в 3ds max. Объект действительно вращается, но ось вращения задана неправильно. К счастью, это легко исправить, щелкнув по кнопке Max расположенной в рамке Align панели Parameters. Щелкните по кнопке Max и изображение станет выглядеть так, как изображено на рис. 11.11.



Рис. 11.11. Правильное вращение контура бокала

Трехмерные модели довольно скучны без текстур, так что убедитесь, что в команде Lathe вы задали координаты текстур объекта. Для этого установите флажок Generate Mapping Coordinates в панели Parameters. Затем в той же панели установите флажок Weld Core. Этот параметр объединяет вершины, образующие ось вращения модели.

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



Рис. 11.12. Изображение бокала в окне Perspective

На рис. 11.12 вы видите изображение бокала в окнах Top и Perspective. Перед тем, как экспортировать склянку, нужно преобразовать ее в редактируемую сетку. Это необходимо потому, что DirectX не знает как работать непосредственно с объектами 3ds max. Щелкните правой кнопкой мыши по изображению бокала, выберите в появившемся меню команду Convert To, а затем Convert To Editable Mesh. Эта операция не вызовет никаких изменений во внешнем виде бокала, но изменит то, как на него будет реагировать DirectX. После того, как конвертирование завершено, пришло время экспортировать модель. Раскройте меню File и выберите команду Export Selected. На экран будет выведено диалоговое окно, в котором следует указать имя объекта и выбрать тип файла. Введите имя файла и в выпадающем списке Save as выберите формат X-File (*.X). Сохраните объект и вам будет предложено окно, изображенное на рис. 11.13.



Рис. 11.13. Параметры экспорта X-файлов

Снимите флажки Include Animation Data и Looping Animation Data, а затем щелкните по кнопке Go!.

ПРИМЕЧАНИЕ
Чтобы в выпадающем списке Save as присутствовал формат X-файлов, вы должны установить программу экспорта X-файлов от Microsoft. Если у вас в списке нет пункта для сохранения X-файлов, установите программу экспортера.

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


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

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


Рис. 11.16. Процесс редактирования

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

Чтобы изменять ключевые кадры вам требуется по крайней мере возможность менять местоположение и угол поворота объектов сцены. Позднее вы, возможно, захотите добавить возможность менять масштаб объектов. Я предпочитаю предоставлять пользователям комбинации клавиш, нажатие на которые меняет местоположение и разворот объекта, а также поля редактирования для ввода точных значений. Поля редактирования облегчают ввод повторяющихся команд. Гораздо проще ввести в поле число 180, чем 180 раз нажимать на кнопку «плюс».



Проект D3D_AnimationEditor


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


Рис. 11.18. Структура файлов проекта D3D_AnimationEditor

На рисунке видно, как в файл main.cpp включается файл main.h. Файл main.h в свою очередь включает заголовочный файл C3DAnimation.h. Заголовочный файл C3DAnimation.h содержит сведения о классе анимации и заголовочный файл Object3DClass.h. Заголовочный файл Object3Dclass.h включает информацию для загрузки объектов из файлов .x и их визуализации. Кроме того, он включает заголовочный файл ExceptionClass.h. Заголовочный файл ExceptionClass.h содержит информацию о классе исключений и заголовочный файл DXUtil.h, предоставляемый DirectX SDK.

Если посмотреть на файлы, которые не являются заголовочными, то основная логика редактора находится в файле main.cpp. Класс анимации расположен в файле C3DAnimation.cpp, класс трехмерных объектов — в файле Object3DClass.cpp, а класс исключений — в файле ExceptionClass.cpp.

Что касается библиотек, то для успешной компиляции проекта нам потребуются: d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib, winmm.lib и dinput8.lib.

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



Программа Convert 3DS


Если у вас нет доступа к 3ds max или вы не хотите компилировать плагин для экспорта файлов .x, можно воспользоваться программой Conv3ds.exe, поставляемой вместе с DirectX SDK. Это старая утилита и на данный момент она может быть уже недоступна, но она позволяет вам преобразовывать файлы .3ds из командной строки.

Самое замечательное в Conv3ds.exe то, что для создания файла .x ей необходим только объект в формате .3ds. К счастью, большинство программ позволяют экспортировать объекты в файлы .3ds. Благодаря этому создание файлов .x в программах, отличающихся от 3ds max оказывается достаточно простым делом.

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



Программа D3D_AnimationEditor


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

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


Рис. 11.24. Ход выполнения программы D3D_AnimationEditor

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



Программирование редактора

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

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

ВНИМАНИЕ!

Кнопка Save Anim перезапишет пример анимации, который я создал для вас. Не выполняйте запись, если не хотите чтобы пример анимации был перезаписан.

ВНИМАНИЕ!

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

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


Рис. 11.17. Окно программы D3D_AnimationEditor

На рис. 11.7 изображено как реализация редактора анимации выглядит в реальной жизни. Вы, возможно, скажете, что она очень похожа на интерфейс редактора, созданного в предыдущей главе. Окно слева содержит область редактирования с отладочной информацией, а окно справа — панель команд и информацию. В окне редактирования вы видите загруженные и готовые к использованию объекты механоида и антены радара. Если вы скомпилировали и запустили программу, чтобы она стала похожа на рис. 11.17, щелкните по кнопке Load Anim.



Программирование собственного редактора


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

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

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

Интерфейс

Команды

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

Сохранение и загрузка



Реализация анимации


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

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


Рис. 11.26. Окно программы D3D_AnimationPlayback

Теперь загрузите проект, скомпилируйте его и посмотрите как он работает.

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



Реализация методов класса C3DAnimation


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

Файл C3DAnimation.cpp содержит код реализации методов класса, и первыми нам на глаза попадаются конструктор и деструктор.



Редактирование анимации


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



Сцена


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


Рис. 11.1. Трехмерная сцена визуализированная в 3ds max

На иллюстрации показан результат визуализации созданной в 3ds max сцены. (Компания Discreet была настолько любезна, что предоставила старому доброму автору свое программное обеспечение для этой книги, так что пожалуйста рассмотрите их программы, когда будете выбирать пакет трехмерного моделирования для себя.) Обратите внимание на составляющие сцену трехмерные объекты. Самыый бросающийся в глаза объект — вода. Кроме того, здесь есть небо, несколько островов и одинокая рыбацкая лодка. Все эти объекты вместе и составляют сцену. Поняли? Хорошо, идем дальше.



Сохранение и загрузка


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

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



Создание кадров


Теперь, когда на вашей сцене есть трехмерные объекты, вам требуются данные кадров. Щелкните по кнопке New Frame и в обработчике сообщений будет выполнен следующий код:

case ID_BUTTON_NEWFRAME: // Создание нового кадра анимации animTest.vNewFrame(); SetActiveWindow(g_hWnd); // Обновление информации на панели инструментов vUpdateToolbarStats(); break;

Как видно в коде, при щелчке по кнопке New Frame вызываются несколько функций объекта класса анимации. Вызов метода класса vNewFrame() создает новый кадр для каждого объекта сцены. Поскольку у вас загружено два объекта, будет создано два кадра.

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

И в самом конце я вызываю функцию обновления состояния панели инструментов. Замечательно то, что теперь вы должны увидеть объекты сцены на экране. У вас появились данные кадров и программе есть что показать!



Структура stKeyFrame


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

Член m_vecRot содержит информацию о вращении для ключа.

Член m_vecTrans содержит информацию о перемещении, или местоположении, для ключа.

Член m_vecScale содержит информацию о масштабировании для ключа.

Член m_lTimeDelay содержит информацию о временной задержке для ключа.



Управление с клавиатуры


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



Выбор необходимого объекта


У вас есть кадр и объекты, что теперь? Давайте займемся редактированием! Щелкните по кнопке NextObj, пока антена не станет отображаться нормально, а не в виде каркаса. При каждом щелчке по кнопке Next Obj будет выполняться следующий код в обработчике сообщений:

case ID_BUTTON_NEXTOBJ: // Увеличение номера текущего объекта g_iCurObj++; // Если достигли конца, вернемся к началу if(g_iCurObj >= animTest.m_iNumObjects) { g_iCurObj = 0; } SetActiveWindow(g_hWnd); vUpdateToolbarStats(); break;

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



Загрузка объекта


Теперь, когда у вас есть замечательный X-файл, загрузите его в программу D3DFrame_ObjectLoader, которая вместе с исходными кодами находится среди сопроводительных файлов. Вам потребуется лишь слегка изменить код, чтобы считывался файл wineglass.x. Внесите изменения в код, скомпилируйте его и перед тем, как запустить программу, убедитесь, что в каталоге с программой есть файл wineglass.x. Если все сделано правильно, вы увидите окно, похожее на то, что изображено на рис. 11.14.


Рис. 11.14. Просмотр бокала в Direct3D

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



Базовая стоимость узла


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

Таблица 12.1. Базовая стоимость узлов

Тип узла Стоимость
Трава1
Грязь2
Песок3
Скалы4
Болото5

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



Функция CPathFinder::bFindPath()


Я могу потратить 50 страниц на описание кода, но в классе CPathFinder есть только одна заслуживающая внимания функция. Это функция bFindPath(), которая выполняет всю работу по нахождению наиболее эффективного пути из одного пункта в другой. Взгляните на рис. 12.12, где изображено как работает эта функция.


Рис. 12.12. Ход выполнения функции bFindPath()

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

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

ПРИМЕЧАНИЕ

Код поиска пути не оптимизирован. Не используйте его в своих проектах без предварительной оптимизации.

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



Функция инициализации пути


На рис.12.11 изображен ход выполнения кода поиска пути.


Рис. 12.11. Ход выполнения кода поиска пути в main.cpp

Обратите внимание, как функция vInitPathing() использует при вычислении пути объект класса CPathFinder. Кроме того, на рисунке изображена функция iGetMapCost(), которая вычисляет базовую стоимость для данного узла карты. Вот ее код:

int iGetMapCost(int iX, int iY) { // Узел непроходим, если находится вне горизонтальных границ карты if(iX < 0 || iX >= g_iTilesWide) return(-1);

// Узел непроходим, если находится вне вертикальных границ карты if(iY < 0 || iY >= g_iTilesHigh) return(-1);

// Узел непроходим, если номер блока карты отличается от 0 if(g_iTileMap[iX + (iY * g_iMapWidth)][1] != 0) { return(-1); } // Для всех остальных случаев возвращаем стоимость блока else { return(g_iTileMap[iX + (iY * g_iMapWidth)][1]); } }

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

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

void vInitPathing(void) { bool bRet; int iTempX; int iTempY; int iDir; // Начальная и конечная позиции на карте int iNodeStartX; int iNodeStartY; int iNodeEndX; int iNodeEndY; // Таймеры DWORD dwStartTime; DWORD dwTotalTime; // Объект класса пути CPathFinder pathMyPath;

// Очистить карту со стрелками // Она используется в дальнейшем для отображения пути for(int i = 0; i < g_iMapWidth * g_iMapHeight; i++) { g_iArrowMap[i] = -1; }

// Ищем на карте исходный пункт for(int y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 19) { g_iRabbitXPos = x; g_iRabbitYPos = y; // Сохраняем исходное состояние iNodeStartX = g_iRabbitXPos; iNodeStartY = g_iRabbitYPos; break; } } } // Ищем на карте конечный пункт for(y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 20) { iNodeEndX = x; iNodeEndY = y; break; } } }


// Обновляем отображаемое сообщение sprintf(g_szPathStatus, "CALCULATING PATH"); vRender();

// Задаем функцию получения стоимости pathMyPath.vSetCostFunction(iGetMapCost); // Запуск таймера dwStartTime = timeGetTime(); // Задаем начальную и конечную позиции pathMyPath.vSetStartState(iNodeStartX, iNodeStartY, iNodeEndX, iNodeEndY); // Ищем путь - максимальная длина 300 узлов bRet = pathMyPath.bFindPath(300); // Остановка таймера dwTotalTime = timeGetTime() - dwStartTime;

// Выход в случае сбоя if(!bRet) { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "FAILED, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); return; } else { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "COMPLETE, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); }

// Теперь следуем по пути CPathNode *GoalNode = pathMyPath.m_CompletePath->m_Path[0]; int iTotalNodes = 0;

// Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY;

// Старт из позиции 1, а не 0 iTotalNodes++; GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes];

// Перебираем в цикле составляющие путь узлы // Для каждого шага рисуем стрелку while(iTotalNodes < pathMyPath.m_CompletePath->m_iNumNodes) { // Определяем направление стрелки iDir = vFindDirection(iTempX, iTempY, GoalNode->m_iX, GoalNode->m_iY);

// Сохраняем стрелку в карте стрелок g_iArrowMap[GoalNode->m_iX + (GoalNode->m_iY * g_iMapWidth)] = iDir;

// Визуализируем сцену vRender();

// Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY;

// Увеличиваем счетчик узлов iTotalNodes++;

// Получаем следующий узел GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes]; }; }

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



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

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

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


Итоги и оптимизация


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

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

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

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

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

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

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

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

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



Начало поиска


Вот вы и узнали о терминологии, применяемой в алгоритме А*, но как использовать сам алгоритм? Первое, что делает алгоритм А*— это добавление начального узла в закрытый список. Это делается потому, что начальный узел всегда будет первым узлом полученного пути. Сделав это вы должны найти все узлы, которые являются смежными с начальным и в которые может переместиться игрок. Если смежный узел доступен, он добавляется в открытый список. Так как в самом начале нет никаких открытых узлов, перед началом работы алгоритма открытый список пуст.

Итак, вот этапы поиска:

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

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

На рис. 12.7 я выполнил эти два шага и теперь у меня один узел в закрытом списке и восемь узлов в открытом. Что дальше?



Обратная трассировка для нахождения пути


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


Рис. 12.10. Найденный путь

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

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



Общая стоимость


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


Рис. 12.8. Стоимость узлов из открытого списка

На рис. 12.8 показаны узлы из открытого списка с их стоимостью. Из чего составляется стоимость каждого узла показано на рис. 12.9.


Рис. 12.9. Составляющие стоимости узла

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



Основы A*


Давайте познакомимся с терминами, которые используются при описании алгоритма A*.

Узел — Позиция на карте.

Открытый список — Список узлов в которые может переместиться игрок и которые являются смежными с закрытыми узлами.

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

Чтобы понять, как эти термины применяются, взгляните на рис. 12.6.


Рис. 12.6. Терминология в алгоритме A*

На рис 12.6 изображены узлы, составляющие карту. Фактически, узлом является каждый квадрат карты. Я понимаю, что термин «узел» может звучать странно, но он подходит больше, чем «квадрат» или «клетка». Дело в том, что алгоритм A* может применяться и для тех карт, где форма блоков отличается от квадрата.

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

Узлы, соседствующие с единственным узлом из закрытого списка будут помещены в открытый список. В результате у вас будет один узел в закрытом списке и восемь узлов в открытом. Это показано на рис. 12.7.


Рис. 12.7. Добавление узлов в открытый список

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



Поиск наилучшего узла


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

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

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

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

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

Удалить узел с наименьшей общей стоимостью из открытого списка.



Поиск пути по алгоритму A*


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

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


Рис. 12.5. Окно программы D3D_PathFinding

Запустите программу и щелкните по расположенной на панели команд кнопке Go. В результате будет запущен алгоритм поиска пути. Как видно на рис. 12.5 программа ищет путь из начальной точки в конечную и отображает решение в виде стрелок. Вы можете загружать различные ландшафты, находящиеся в сопроводительных файлах, и смотреть, как алгоритм справляется с ними. Самое лучшее, что алгоритм A* всегда находит лучший путь с учетом имеющегося времени и ресурсов.

Как работает программа D3D_PathFinding? Не беспокойтесь; на этот раз я не буду сразу переходить к описанию исходного кода. Вместо этого я сначала приведу теоретическое описание работы алгоритма A*.



Продолжение поиска


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



Простое решение


Простейшее решение задачи, изображенной на рис. 12.1 можно записать в виде следующего псевдокода:

Если мы слева от цели, перемещаемся вправо Если мы справа от цели, перемещаемся влево Если мы выше цели, перемещаемся вниз Если мы ниже цели, перемещаемся вверх

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


Рис. 12.2. Работа простого алгоритма поиска пути

На рис. 12.2 вы проверяете местоположение игрока и, выяснив, что он находится слева от цели, перемещаете его вправо на одну клетку. Этот процесс повторяется, пока вы не достигнете цели, как показано на рис. 12.3.


Рис. 12.3. Общее решение проблемы поиска пути

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


Рис. 12.4. Более сложный путь

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

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

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

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



Реализация в коде


Теперь, продравшись через дебри теории, загрузите проект D3D_PathFinding и следуйте за мной. Проект содержит следующие файлы с исходным кодом: main.h, main.cpp, CPathFinder.h и CPathFinder.cpp. Наиболее важны два файла с именами CPathFinder. Они содержат код класса поиска пути.

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



Стоимость относительно цели


Последний компонент стоимости— это стоимость достижения цели из данного узла. Она вычисляется путем сложения количества строк и столбцов на которые текущий узел отстоит от цели. Предположим, текущий узел расположен на один ряд ниже и на десять столбцов левее цели. Стоимость этого узла относительно цели будет 10 + 1 = 11. Правда, просто?



Стоимость относительно начального узла


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



Вычисление стоимости узлов


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

Базовая стоимость узла.

Стоимость возврата к начальному узлу.

Стоимость достижения цели.



Задача поиска пути


Для начала взгляните на рис. 12.1, где изображен общий случай задачи поиска пути.


Рис. 12.1. Задача поиска пути

На рис. 12.1 изображена карта, на которой отмечены начальная и конечная точки. Начальная точка выглядит как набор концентрических окружностей, а конечная — как большая буква Х. Чтобы переместиться от начальной точки к конечной вы должны определить, в каком именно месте карты вы находитесь и принять обоснованное решение о том, в каком направлении следует двигаться. Поскольку в игре определить свое местоположение (координаты X, Y и Z) достаточно просто, остается решить только куда нам двигаться.

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