Реализация в коде
Теперь, продравшись через дебри теории, загрузите проект D3D_PathFinding и следуйте за мной. Проект содержит следующие файлы с исходным кодом: main.h, main.cpp, CPathFinder.h и CPathFinder.cpp. Наиболее важны два файла с именами CPathFinder. Они содержат код класса поиска пути.
Помимо поиска пути в программе нет ничего новаторского. Код выполняет обычную работу создавая окно и инициализируя графику. После загрузки изображений программа загружает карту, на которой будет выполняться поиск пути. Поиск выполняется после того, как пользователь щелкнет по расположенной на панели команд кнопке Go. Кроме того, пользователь может загружать другие карты, чтобы посмотреть как алгоритм поиска пути ведет себя в разных обстоятельствах.
Редактирование карты
К данному моменту у вас появилась лишь возможность интерактивного просмотра карты. Как насчет того, чтобы действительно поредактировать ее? Заучит заманчиво, а? В этом разделе я покажу вам как написать редактор карт, который позволит вам самим размещать блоки на карте. Ушли те дни, когда вы задавали карты в виде набора значений в коде программы! Взгляните на Рисунок 10.7, где изображено окно редактора карт, о котором я буду рассказывать.
Панель инструментов теперь видима, но как же вычислить, какой блок на ней выбран? В этом случае мышь — лучший друг человека, и именно она будет использоваться для выбора блоков. Процесс может показаться легким, если не учесть тот факт, что панель инструментов может свободно перемещаться. Поскольку вы не можете гарантировать, что панель инструментов всякий раз будет находиться в одном и том же месте, необходимо принимать во внимание ее текущее местоположение и уже основываясь на этой информации проверять координаты указателя мыши.
В программе D3D_MapEditorLite есть два режима работы мыши. В режиме редактирования вы шелкаете левой кнопкой мыши по карте в области просмотра и редактируете выбранный блок. В режиме выбора блока вы щелкаете по изображениям блоков на панели инструментов, чтобы выбрать новый блок для использования при редактировании. Чтобы выяснить в каком режиме работает пользователь, я проверяю координаты указателя мыши и смотрю, находятся ли они внутри области панели инструментов или внутри области просмотра карты. Если координаты находятся внутри области панели инструментов, я перехожу к логике для панели инструментов, чтобы определить, какой блок находится под указателем мыши. Если координаты находятся внутри области просмотра карты, в игру вступает логика редактирования карты, вычисляющая какой именно блок карты редактируется. Вот как выглядит код, выполняющий эти задачи:
void vCheckMouse(void) { RECT rcWindow; POINT Point; int iMouseX; int iMouseY; int iTileX; int iTileY; // Получаем координаты указателя мыши GetCursorPos(&Point); iMouseX = Point.x; iMouseY = Point.y; // Определяем местонахождение рабочей области панели инструментов GetWindowRect(hWndToolBar, &rcWindow); // Проверяем, находится ли указатель мыши внутри окна панели инструментов if(iMouseX > rcWindow.left && iMouseX < rcWindow.right && iMouseY > rcWindow.top && iMouseY < rcWindow.bottom) { // Преобразуем координаты указателя мыши // в локальные координаты панели инструментов iMouseX -= rcWindow.right; iMouseY -= rcWindow.top; // Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize; // Вычисляем, какой блок выбран g_iCurTile = (g_iCurTileSet * 21) + (iTileX + (iTileY * 3)) - 1; // Проверяем, что выбран существующий блок if(g_iCurTile < 0|| g_iCurTile >= g_iTotalTiles) { g_iCurTile = 0; } vRenderTileSet(); } // Проверяем, находится ли указатель мыши в окне редактирования else { GetWindowRect(g_hWnd, &rcWindow); if(iMouseX > rcWindow.left && iMouseX < rcWindow.right && iMouseY > rcWindow.top && iMouseY < rcWindow.bottom) { // Преобразуем координаты указателя мыши // в локальные координаты окна редактирования iMouseX -= rcWindow.left + g_iXOffset; iMouseY -= rcWindow.top + g_iYOffset; // Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize; g_iTileMap[iTileX + g_iXPos + ((iTileY + g_iYPos) * g_iMapWidth)] = g_iCurTile; } } }Чтобы определить местоположение панели инструментов и окна редактирования я воспользовался функцией GetWindowRect(). После того, как я узнал где они расположены, достаточно простой проверки, чтобы определить находится ли указатель мыши в заданной области.
Если указатель мыши находится внутри панели инструментов, я беру координаты указателя мыши и делю их на размеры блока, чтобы увидеть, какой именно блок выбирает пользователь. Как только я узнал, какой блок выбран, я заношу в переменную g_iCurTile новое значение. Затем я вызываю функцию vRenderTileSet(), чтобы переместить красный квадрат, отмечающий выбранный блок на новое место.
Если указатель мыши находится в окне редактирования, я корректирую координаты указателя мыши с учетом местоположения клиентской области окна. Затем я делю координаты указателя мыши на размеры блока, чтобы вычислить какой блок карты выбран для редактирования. Последний шаг к Валгалле — помещение значения из переменной g_iCurTile в соответствующий элемент массива g_iTileMap.
Редактор уровней игры Warcraft III
На Рисунок 10.1 представлено окно редактора уровней игры Warcraft III, поставляемого вместе с игрой и являющегося очень мощным инструментом. Он позволяет вам редактировать карты, поставляемые с игрой, или создавать свои собственные с нуля. На рисунке вы можете видеть мини-карту, представляющую высокоуровневый взгляд на карту и крупный план редактируемой в данный момент области, занимающий большую часть окна редактора. Вокруг области редактирования есть различные панели инструментов, позволяющие выбирать текстуры и производимые с картой действия.
В этой главе я покажу вам как создать собственный редактор карт. Он будет не таким мощным и сложным, как редактор Blizzard для Warcraft III, но, по крайней мере, предоставит вам хорошую отправную точку.
ПРИМЕЧАНИЕРедактор уровней Warcraft III с включенной сеткой
На рисунке показана область редактирования редактора уровней игры Warcraft III с включенной сеткой редактирования. Как видите, сетка ускоряет выравнивание плиток.
Рисование подразделений
Все эти классы великолепны, но как насчет визуализации? Если вы откроете файл main.cpp из проекта D3DFrame_UnitTemplate, я покажу вам! Спускайтесь вниз до функции vInitTileVB().
Вы, возможно, еще помните пример визуализации блоков, который я демонстрировал в главе 5. Там я создавал отдельный буфер вершин для хранения геометрии текстуры. Базовая точка геометрии находилась в левом нижнем углу квадрата. Это упрощает выравнивание квадратов при визуализации блоков, но не слишком подходит для поворотов. Взгляните на Рисунок 8.28, чтобы понять что я имею в виду.
Скорость передвижения
На Рисунок 8.11 присутствуют несколько переменных, контроллирующих передвижение боевых единиц. Первая из них, скорость передвижения, указывает на сколько блоков игрового поля может переместиться данное подразделение за один раунд игры. Я здесь использую значение с плавающей запятой, поскольку, вероятно, вы не захотите, чтобы за каждый раунд подразделение перемещалось на целое число блоков.
Скорость поворота
Последний уникальный элемент данных класса передвижения сообщает вам насколько быстро подразделение поворачивает. Это число с плавающей точкой, указывающее на сколько градусов может развернуться подразделение за один раунд. Подразделению, скорость поворота которого равна 10.0 потребуется 36 раундов, чтобы сделать полный круг. Если скорость поворота равна 30, подразделению на полный круг потребуется лишь двенадцать раундов. Преимушества более быстрого разворота показаны на Рисунок 8.12.
Скорость снаряда
Переменная m_fProjectileSpeed задает с какой скоростью снаряд покидает ствол оружия. Это значение применимо только для снарядов и ракет, поскольку в рукопашной схватке снаряды отсутствуют, а лазерный луч распространяется со скоростью света.
Скорость снаряда указывает сколько блоков игрового поля может преодолеть снаряд за раунд игры. По этой причине диапазон допустимых значений будет от 0.0 до 0.99. Если вы не хотите, чтобы снаряд пересекал несколько блоков игрового поля за один раунд, максимальным значением должно быть именно 0.99.
Скорость восстановления
Раньше я не упоминал о восстановлении здоровья подразделений, так что эта идея может показаться вам новой. В классе типа защиты есть переменная с именем m_iRegenRate, позволяющая создавать подразделения, которые могут сами устранять причиненные им повреждения. Возможно, это подразделение снабжено аптечками, или это мифический зверь, способный заращивать раны. Так или иначе, это значение позволяет добавить к вашей игре самовосстанавливающиеся боевые единицы.
Ключевым моментом является настройка соответствия между диапазоном значений очков повреждений и скоростью восстановления. Поскольку количество очков повреждений у подразделения будет увеличиваться один раз за раунд игры на значение, равное скорости восстановления, последнюю величину надо выбрать сравнительно небольшой. Я рекомендую принять диапазон допустимых значений от 0 до 100. Если скорость восстановления равна 100, подразделение восстановит свои параметры от предсмертного состояния до полного здоровья за десять раундов. Если же значение равно 1, то и через 100 раундов подразделение будет в двух шагах от гибели.
Скорострельность
Переменная m_iRateOfFire сообщает вам, сколько раундов игры должно пройти, прежде чем оружие сможет снова выстрелить. Быстродействующее оружие, такое как автомат, может стрелять залпами в каждом раунде игры. Более медленное оружие, например, катапульты, будут стрелять один раз в пять раундов, или что-то подобное. Конечно, автомат может выпускать сразу несколько пуль, и именно поэтому я использовал термин «стрелять залпами».
Не существует однозначного ответа на вопрос сколько раундов игры должно пройти , пока оружие сново может выстрелить. Чтобы получить сбалансированный тип атаки, вам придется поиграть с разными значениями.
Сохранение и загрузка карты
Вау! Мы уже узнали как редактировать карту, так что не осталось никаких препятствий на пути к вершинам картографии. Редактирование карт — великолепная возможность, но она бессмысленна, если вы не можете сохранить результаты своей работы. Здесь на сцену выходит функция сохранения карты. В этом разделе я покажу вам как добавить к программе D3D_MapEditorLite полнофункциональные кнопки сохранения и загрузки. Загрузите новый улучшенный проект D3D_MapEditorPlus и пойдемте дальше. Сперва взгляните на Рисунок 10.10, где изображено окно этой программы.
Составляющие стоимости узла
Как видно на Рисунок 12.8 и Рисунок 12.9, общая стоимость узла показана в левом верхнем углу. В правом верхнем углу приводится базовая стоимость, в левом нижнем — стоимость относительно начального узла и в правом нижнем — стоимость относительно цели.
Создание подразделений
Теперь, после того как базовая информация о подразделених загружена, вы можете создавать подразделения, которые будут использоваться в игре. Вы не можете модифицировать базовые типы, так что следует создавать новые объекты подразделений. Здесь в игру вступает член данных диспетчера подразделений с именем m_UnitObjs. Данный массив хранит модифицируемые объекты подразделений, использующиеся в игре. Для управления этими объектами применяются две функции: iAddUnit() и vRemoveUnit().
Способ передвижения
Данное поле сообщает вам какой именно способ использует подразделение для своего передвижения. Летает оно, плавает или ползает? Может быть оно ходит? Может быть оно катится? Переменная, задающая способ перемещения отвечает на этот вопрос.
Стоимость относительно цели
Последний компонент стоимости — это стоимость достижения цели из данного узла. Она вычисляется путем сложения количества строк и столбцов на которые текущий узел отстоит от цели. Предположим, текущий узел расположен на один ряд ниже и на десять столбцов левее цели. Стоимость этого узла относительно цели будет 10 + 1 = 11. Правда, просто?
Стоимость относительно начального узла
Следующая стоимость позволяет отследить во сколько обойдется игроку возвращение из данного узла к начальному. Она необходима для того, чтобы вы знали насколько труден путь из начального узла до данного. Вычисляется эта стоимость очень просто — достаточно взять стоимость относительно начального узла для родительского узла и прибавить к ней базовую стоимость текущего узла. В результате вы получите общую стоимость текущего узла относительно начального.
Стоимость узлов из открытого списка
На Рисунок 12.8 показаны узлы из открытого списка с их стоимостью. Из чего составляется стоимость каждого узла показано на Рисунок 12.9.
Структура функции проверки ввода
На Рисунок 9.9 видно как функция проверки ввода проверяет активные зоны, в случае необходимости обновляет меню, а также проверяет клавиатуру. Ключевой особенностью рассматриваемого примера является активная зона MAINMENU_NEWGAME. При активации данной зоны программа вызывает функцию установки активных зон для инициализации экрана начала новой игры.
Структура класса частиц
В качестве примера работы с классом частиц я включил в сопроводительные файлы книги проект с именем D3D_Particles. Загрузите его и следуйте вперед к созданному мной примеру класса. Код класса находится в двух файлах проекта: CParticle.cpp и CParticle.h. Вот как выглядит заголовочный файл:
class CVector { public: float fX; float fY; float fZ; CVector() { fX=0.0f, fY=0.0f, fZ=0.0f; }; }; class CParticle { public: CVector m_vecPos; CVector m_vecCurSpeed; CVector m_vecAcceleration; CVector m_vecGravity; int m_iLife; int m_iTextureStart; int m_iTextureEnd; int m_iTextureType; int m_iTextureCur; int m_iTextureSteps; int m_iTextureCurStep; CParticle(); ~CParticle(); void vUpdate(void); bool bIsAlive(void); void vSetTextures(int iType, int iStart, int iStop, int iSteps); void vSetPos(float x, float y, float z); void vSetAcceleration(float x, float y, float z); void vSetGravity(float x, float y, float z); void vSetSpeed(float x, float y, float z); void vSetLife(int iLife); };Структура объекта подразделения
На Рисунок 8.20 видно, что класс подразделения состоит из базовых классов и данных состояния. В блоке данных состояния находятся переменные для различных параметров, таких как текущее количество очков повреждений, направление поворота, местоположение и текущая скорость. Обратите внимание на пунктирную линию, соединяющую максимальное количество очков повреждений в базовом объекте защиты и текущее количество очков повреждений в данных состояния. Текущее количество очков повреждений показывает сколько еще повреждений может получить подразделение до его уничтожения. Это значение изменяется когда подразделение получает повреждения или восстанавливается. Поскольку подразделения не могут совместно использовать одно общее значение здоровья, текущее значение здоровья хранится каждым подразделением локально в его данных состояния. Базовый тип защиты вступает в игру, когда вычисляется максимально возможный для подразделения показатель здоровья. Это значение никогда не изменяется, и поэтому базовый класс — наилучшее место для него.
Держа в уме информацию с Рисунок 8.20, взглянем на исходный код:
class CUnit { public: CUnitDefense *m_Defense; CUnitOffense *m_Offense1; CUnitOffense *m_Offense2; CUnitOffense *m_Offense3; CUnitMovement *m_Movement; CUnitAnimation *m_Animation; int m_iType; int m_iCurHitPoints; float m_fCurSpeed; float m_fXPos; float m_fYPos; float m_fRot; float m_fScale; int m_iUnitID; int m_iParentID; char m_szName[64]; bool m_bActive; int m_iOwner; int m_iCurAnimFrame; int m_iCurAttackFrame; int m_iCurStillFrame; int m_iCurMoveFrame; int m_iCurDieFrame; public: CUnit(); ~CUnit(); virtual void vReset(void); virtual void vSetBaseValues( CUnitDefense* ptrDef, CUnitOffense* ptrOff1, CUnitOffense* ptrOff2, CUnitOffense* ptrOff3, CUnitMovement* ptrMove, CUnitAnimation* ptrAnim); virtual void vSetPosition(float fX, float fY); };С точки зрения количества функций класс не выглядит слишком сложным. Большая часть кода состоит из объявлений необходимых для игры переменных состояния. Это ни в коем случае нельзя считать достаточным для завершенного класса подразделения. Перечисленных переменных состояния достаточно только для рассматриваемого примера. В реальной игре их будет гораздо больше!
Структура панели инструментов
На Рисунок 10.9 видно как отображение блоков начинается в левом верхнем углу и продолжается сверху вниз. На иллюстрации текущим блоком является блок с номером 10 и поэтому именно вокруг него нарисован красный квадрат. Тот же самый блок скопирован и в нижней части области просмотра блоков, что так же указывает какой именно блок выбран. Еще ниже на панели инструментов выводятся кнопки для навигации по набору блоков.
Теперь, когда выполнена визуализация всех блоков, надо поместить их на панель инструментов. Это выполняется путем задания исходной и целевой областей и вызова функции Present(). Как видите, в качестве параметров этой функции передаются исходная прямоугольная область и прямоугольная область места назначения. Сама функция сообщает системе визуализации, что необходимо взять изображение из одной области и скопировать его в другую. В этом случае вы можете визуализировать блоки в буфере трехмерной графики, а затем скопировать их на панель инструментов для показа. Посмотрите на код, и вы увидите как я копирую исходную область и перемещаю ее на панель инструментов.
Структура проекта D3D_Particles
Ход выполнения программы демонстрации частиц следует тому же шаблону, который лежит в основе большинства примеров из этой книги. Взгляните на Рисунок 13.4, чтобы увидеть ход выполнения программы.
Терминология в алгоритме A*
На рис 12.6 изображены узлы, составляющие карту. Фактически, узлом является каждый квадрат карты. Я понимаю, что термин «узел» может звучать странно, но он подходит больше, чем «квадрат» или «клетка». Дело в том, что алгоритм A* может применяться и для тех карт, где форма блоков отличается от квадрата.
На карте как обычно отмечены начальная и конечная позиции. Помимо этого, начальная позиция обведена тонкой рамкой. Это показывает, что данный узел находится в закрытом списке. Поскольку начальная позиция всегда будет являться частью искомого пути, она автоматически включается в закрытый список пройденных узлов.
Узлы, соседствующие с единственным узлом из закрытого списка будут помещены в открытый список. В результате у вас будет один узел в закрытом списке и восемь узлов в открытом. Это показано на Рисунок 12.7.
Тип атаки
Переменная m_iType хранит число, соответствующее данному типу атаки. Это работает точно так же, как и для типов защиты.
Тип защиты
В переменной m_iType хранится число, определяющее тип защиты. Например, ноль может соответствовать броне легкого танка, а единица — броне среднего танка. Диапазон значений зависит от того, как много различных типов защиты будет в вашей игре. Общее их количество редко превышает несколько десятков, но никогда не знаешь точно. Чтобы увидеть пример двух подразделений, использующих два различных типа защиты, вернитесь назад к Рисунок 8.6.
Управление текстурами
Я уже показывал вам управление текстурами ранее, в разделе посвященном импорту данных подразделений. Поскольку функция iLoadBaseTypes() загружает все тербуемые текстуры, можно считать, что управление текстурами уже реализовано. Тем не менее, я добавил еще одну функцию управления, которая подсчитывает количество загруженных текстур и возвращает полученное значение. Она полезна при вычислении объема используемой для хранения текстур памяти. Функция называется iCountTotalTextures(), и вот как выглядит ее код:
int CUnitManager::iCountTotalTextures(void) { int iCount = 0; // Цикл перебора объектов анимации и подсчета текстур for(int i = 0; i < m_iTotalAnimationObjs; i++) { iCount += m_AnimationObjs[i].m_iTotalTextures; } return(iCount); }В функции я перебираю все загруженные базовые типы анимации и суммирую количество текстур, содержащееся в каждом из них. После того, как цикл завершен я возвращаю итоговое значение вызывающей программе. Поскольку каждая текстура в данной игре имеет размер 128x128 точек и глубину цвета 32 бит, для вычисления объема занимаемой текстурами памяти вам достаточно умножить возвращаемое функцией общее количество текстур на 65536 (128 x 128 x 4).
Ускорение и торможение
Чтобы добавить сложности, я включил параметры, задающие ускорение и торможение. Ускорение определяет на сколько возрастает скорость подразделения за раунд игры, когда оно разгоняется. Торможение сообщает вам на сколько уменьшается подразделения за раунд игры, когда оно тормозит. Этот параметр позволяет увеличить реализм сражений. Одни подразделения могут и должны быть медленнее (или быстрее), чем другие. Рассмотрим следующий пример:
Легкая конница: Ускорение = 0.3, Торможение = 0.5, Скорость = 0.5
Катапульта: Ускорение = 0.1, Торможение = 0.2, Скорость = 0.3
В данном примере легкая конница может разогнаться до максимальной скорости за два раунда. Для полной остановки этому подразделению потребуется еще меньше времени — один раунд. Катапульты двигаются медленнее. Чтобы разогнаться до полной скорости им потребуется три раунда, а чтобы остановиться — два. Это действительно имеет смысл, ведь катапульта не может двигаться так же быстро как лошадь. Вы можете не использовать параметры ускорения и торможения в ваших играх, если они показались вам слишком сложными, но помните, что они добавляют вашей игре значительную толику реализма.
Установка формата данных клавиатуры
Затем вы должны задать формат данных клавиатуры. Это простая формальность, для соблюдения которой достаточно вызвать функцию IDirectInputDevice8::SetDataFormat(). Функция получает один параметр, задающий формат данных устройства. Для клавиатуры используйте значение c_dfDIKeyboard. Если же вам необходимо задать формат данных для мыши, воспользуйтесь значением c_dfDIMouse.
Установка уровня кооперации
Поскольку DirectX предоставляет прямой доступ к аппаратуре, очень важен уровень кооперации устройства. Он определяет как программа может использовать данный ресурс совместно с другими приложениями. Если вы установите монопольный режим, больше никто не сможет воспользоваться данным ресурсом. Если вы установите совместный режим, то доступ к клавиатуре смогут получить все желающие. Уверен, вы можете вспомнить игры, которые не делят клавиатуру ни с кем. Мне на ум приходит EverQuest. Поскольку создатели игры не хотели, чтобы сторонние разработчики писали приложения для их игры, они заблокировали использование клавиатуры вне их программы. Это не слишком хорошо и может вызвать настоящие проблемы, если вы переключитесь из игры на другое приложение, чтобы проверить почту или сделать что-нибудь еще.
Для установки уровня кооперации применяется функция IDirectInputDevice8::SetCooperativeLevel(). Вот ее прототип:
HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwFlags );В ее первом параметре, hwnd, передается дескриптор окна, которое будет связано с устройством. Я в этом параметре передаю дескриптор, который был возвращен мне при создании главного окна.
Второй параметр, dwFlags, задает уровень кооперации устройства. Доступные уровни перечислены в таблице 9.1.
Таблица 9.1. Уровни кооперации устройств | |
Значение | Описание |
DISCL_BACKGROUND | Доступ к клавиатуре будет предоставлен даже если окно свернуто. |
DISCL_EXCLUSIVE | Предоставляется монопольный доступ к клавиатуре, для всех остальных клавиатура недоступна. |
DISCL_FOREGROUND | Доступ к данным клавиатуры предоставляется только когда окно активно. |
DISCL_NONEXCLUSIVE | Устройство используется совместно с другими программами. |
DISCL_NOWINKEY | Блокирует клавишу Windows. |
Для рассматриваемого примера я устанавливаю флаги уровня кооперации DISCL_NONEXCLUSIVE и DISCL_FOREGROUND. Благодаря этому программа использует клавиатуру совместно с другими приложениями, а сама может читать данные клавиатуры только когда ее окно активно.
Визуализация блоков
Функция vRender() занимается отображением блочной карты. В ней я в цикле перебираю блоки карты и отображаю текстуры, соответствующие номерам блоков. Вот код цикла визуализации:
// Сверху вниз for(iY = 0; iY < g_iTilesHigh; iY++) { // Справа налево for(iX = 0; iX < g_iTilesWide; iX++) { // Вычисляем смещение в буфере iBufferPos = iX + g_iXPos + ((iY + g_iYPos) * g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos]; // отображаем блок vDrawInterfaceObject((iX * g_iTileSize), (iY * g_iTileSize), (float)g_iTileSize, (float)g_iTileSize, iCurTile); } }В функции визуализации присутствуют два цикла. Первый цикл осуществляет перебор блоков вдоль оси Y. Внутренний цикл перебирает блоки вдоль оси X. Таким образом я отображаю все необходимые блоки. Это тот же самый метод, который я описывал в главе 5.
Визуализация частиц
Теперь, когда частицы инициализированы, пришло время отображать их. Это делается в привычной функции vRender(). Вот часть ее кода, отвечающая за визуализацию частиц:
// Цикл перебора частиц for(int i = 0; i < TOTAL_PARTICLES; i++) { // Проверяем, жива ли частица if(g_partExplosion[i].bIsAlive()) { // Визуализация частицы vDrawInterfaceObject(g_partExplosion[i].m_vecPos.fX, g_partExplosion[i].m_vecPos.fY, (float)g_iParticleSize, (float)g_iParticleSize, g_pTexture[ g_partExplosion[i].m_iTextureCur ]); // Обновление частицы g_partExplosion[i].vUpdate(); } else { // Сброс частицы, если она уничтожена vInitParticles(); } }Функция визуализации в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы функция сперва проверяет жива ли она еще. Если частица жива, функция отображает ее в текущем местоположении. Чтобы сообщить функции визуализации, какую текстуру следует использовать, применяется хранящийся в объекте частицы номер текущей текстуры. После визуализации частицы ее данные обновляются путем вызова функции vUpdate().
СОВЕТЕсли функция визуализации обнаруживает, что частица прекратила свое существование, она вызывает функцию инициализации, чтобы создать новую частицу. Благодаря этому на экране отображается бесконечный цикл анимации частиц, продолжающийся до тех пор, пока вы не выйдите из приложения.
Ввод с клавиатуры
То, что вы прочитали можно назвать самым коротким обзором DirectInput. Причина подобной краткости в том, что стратегические игры не требуют сложных устройств ввода. Нет никакой необходимости использовать устройства с обратной связью, джойстики, игровые пульты и другие подобные устройства. В стратегических играх непревзойденными остаются старые добрые клавиатура и мышь.
Ввод текста в игре
Решение задачи ввода текста в игре может казаться лежащим на поверхности, но есть множество моментов, на которые следует обратить внимание. Как, например, вы будете обрабатывать ввод текста в разгаре игры? Стратегическая игра реального времени останавливающаяся каждый раз, когда пользователь хочет ввести текст, выглядит не слишком хорошо! Кроме того, стоит выбрать способ отображения текста. Что вы будете применять для отображения текста: двухмерные шрифты или трехмерные карты текстур? Читайте дальше и вы найдете ответы на эти и другие вопросы.
Для начала взгляните на Рисунок 9.7, где изображен пример ввода текста в игре.
Вычисление стоимости узлов
В мире А* узлы не равны между собой. Одни из них лучше подходят для создания пути, чем другие. Чтобы выяснить, какой узел является самым лучшим, необходимо каждому узлу в закрытом и открытом списках назначить его стоимость. Как только всем узлам назначена стоимость, достаточно простой сортировки, чтобы выяснить какой узел является самым дешевым. Для вычисления стоимости узлов в алгоритме А* необходимы следующие значения:
Базовая стоимость узла. Стоимость возврата к начальному узлу. Стоимость достижения цели.Взаимосвязь между переменными состояния и базовыми типами
На Рисунок 8.21 вы можете видеть как переменные состояния связаны с соответствующими базовыми типами. Например, максимальное значение номера кадра анимации ожидания берется из находящегося в классе анимации поля с количеством кадров анимации ожидания.
Взаимосвязь между статическими
На Рисунок 8.27 видно, как динамические данные подразделений из массива m_UnitObjs используют в качестве основы данные, хранящиеся в базовых типах.
Задача поиска пути
Для начала взгляните на Рисунок 12.1, где изображен общий случай задачи поиска пути.
На Рисунок 12. 1 изображена карта, на которой отмечены начальная и конечная точки. Начальная точка выглядит как набор концентрических окружностей, а конечная — как большая буква Х. Чтобы переместиться от начальной точки к конечной вы должны определить, в каком именно месте карты вы находитесь и принять обоснованное решение о том, в каком направлении следует двигаться. Поскольку в игре определить свое местоположение (координаты X, Y и Z) достаточно просто, остается решить только куда нам двигаться.
Загрузка базовых типов
У вас есть базовые классы для хранения данных подразделения, но как загрузить в них информацию? Один из способов — жестко задать все значения параметров подразделений в коде программы. Подобное сляпанное наспех решение не позволит создать гибкую систему. Я предпочитаю использовать конфигурационные файлы, которые загружаются во время работы программы. Вы можете редактировать конфигурационные файлы и снова запускать игру без повторной компиляции. Это неоценимое преимущество, поскольку вы наверняка будете менять параметры вашей игры во время разработки. Это также позволяет легко создавать расширения для игры, поскольку для создания новых типов подразделений достаточно изменить значения нескольких параметров в конфигурационных файлах.
В классе есть функция iLoadBaseTypes(), которая загружает значения из конфигурационных файлов. Перед тем, как перейти к рассмотрению этой функции, взглянем на приведенный ниже код заголовка класса:
const int UNITMANAGER_MAXBASEOBJS= 256; const int UNITMANAGER_MAXUNITS = 1024; class CUnitManager { public: CUnitDefense *m_DefenseObjs; CUnitOffense *m_OffenseObjs; CUnitMovement *m_MovementObjs; CUnitAnimation *m_AnimationObjs; CUnit *m_UnitBaseObjs; CUnit *m_UnitObjs; int m_iTotalDefObjs; int m_iTotalOffObjs; int m_iTotalMovObjs; int m_iTotalAnimationObjs; int m_iTotalUnitBaseObjs; int m_iTotalUnitObjs; int m_iOwnerTotal[UNITMANAGER_MAXOWNERS]; // Указатель Direct 3D для загрузки текстур LPDIRECT3DDEVICE9 m_pd3dDevice; CUnitManager(); ~CUnitManager(); virtual void vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d); virtual void vReset(void); virtual void vClearMem(void); virtual int iLoadBaseTypes( char *szDefFileName, char *szOffFileName, char *szMovFileName, char *szUnitFileName, char *szAnimFileName); virtual CUnitDefense* ptrGetDefenseType(char *szName); virtual CUnitOffense* ptrGetOffenseType(char *szName); virtual CUnitMovement* ptrGetMoveType(char *szName); virtual CUnitAnimation* ptrGetAnimType(char *szName); virtual int iAddUnit(char *szName, int iOwner); virtual void vRemoveUnit(int iUnitID); virtual int iCountTotalTextures(void); };Большинство членов данных класса имеет отношение к объектам базовых типов. Поля m_DefenseObjs, m_OffenseObjs, m_MovementObjs, m_AnimationObjs и m_UnitBaseObjs используются как массивы для хранения загружаемых впоследствии базовых типов. Переменные m_iTotalDefObjs, m_iTotalOffObjs, m_iTotalMovObjs, m_iTotalAnimationObjs и m_iTotalUnitBaseObjs отслеживают кличество загруженных в память объектов каждого типа. Это показано на Рисунок 8.22.
Загрузка и создание подразделений
Я показал вам как написать классы подразделений, управлять подразделениями и анимировать их. Последняя тема, которую я затрону — загрузка и создание новых подразделений. В рассматриваемом примере программы есть функция с именем vInitializeUnits(). Она отвечает за загрузку информации о базовых типах и добавляет в игру несколько активных подразделений. Вот ее код:
void CD3DFramework::vInitializeUnits(void) { int iUnit; // Инициализация диспетчера подразделений m_UnitManager.vReset(); // Установка устройства Drect3D m_UnitManager.vSetRenderDevice (m_pd3dDevice); // Импорт базовых данных подразделений m_UnitManager.iLoadBaseTypes( "UnitData\\BaseType_Defense.csv", "UnitData\\BaseType_Offense.csv", "UnitData\\BaseType_Movement.csv", "UnitData\\BaseType_Unit.csv", "UnitData\\BaseType_Animation.csv"); // Добавление нескольких подразделений к "игре" iUnit = m_UnitManager.iAddUnit("Apache Attack Helicopter", 0); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(-180.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Apache Attack Helicopter", 1); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(-70.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Spirit Scout Helicopter", 2); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(50.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Spirit Scout Helicopter", 3); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(180.0f, -80.0f); }В начале кода я вызываю функцию инициализации диспетчера подразделений. В результате освобождается выделенная ранее память и диспетчер подготавливается к загрузке данных.
Затем для диспетчера подразделений я устанавливаю указатель на устройство визуализации DirectX. Если вы помните, ранее говорилось, что это необходимо для загрузки текстур.
Чтобы загрузить подготовленную информацию о подразделениях я вызываю функцию загрузки базовых типов диспетчера. Она получает имена файлов с подготовленными данными и загружает их в базовые типы, которыми управляет объект диспетчера подразделений.
Пришло время для веселья! Следующая часть кода создает подразделения с помощью функции добавления подразделений. Диспетчер создает и активирует запрошенные подразделения, чтобы они появились в игровом цикле. Сразу после создания каждого подразделения я инициализирую данные о его местоположении, чтобы подразделение появилось в требуемом месте экрана. Обратите внимание, создавая подразделения я назначаю каждому из них собственный цвет владельца. Это позволит вам увидеть как различные цвета владельца отображаются во время визуализации.
После того как подразделения созданы в диспетчере и активированы, они могут модифицироваться и отображаться согласно вашим пожеланиям. В качестве упражнения попробуйте создать на экране еще несколько сотен подразделений и посмотрите, что получится!
Загрузка изображений блоков
Вы уже посмотрели, как программа осуществляет навигацию на карте, но что насчет отображения графики? Программа бесполезна без визуальной обратной связи. Старая добрая функция vInitInterfaceObjects() заботится о загрузке используемых в программе изображений блоков. Вот фрагмент кода, выполняющий этот трюк:
for(int i = 0; i < 3; i++) { // Установка имени sprintf(szTileName, "tile%d.bmp", i); // Загрузка if(FAILED(D3DXCreateTextureFromFile( g_pd3dDevice, szTileName, &g_pTexture[i]))) { return; } }Загрузка блоков довольно проста. Я просто запускаю цикл, перебирающий доступные блоки и загружающий файлы с именами от tile0.bmp до tile2.bmp. Поскольку загружаются только три блока, процесс выполняется очень быстро. Для последующего использования загруженные блоки сохраняются в массиве g_pTexture.
Захват клавиатуры
Последний, относящийся к DirectX этап — вызов функции IDirectInputDevice8::Acquire(). Эта функция необходима для привязки приложения к используемому им устройству ввода. Всякий раз когда окно теряет фокус клавиатура должна быть захвачена снова.
B Ресурсы для разработчика
Существует множество ресурсов для разработчиков, так что здесь я перечислю только свои любимые.
Двухмерная графикаAdobe Photoshop — это наилучший из имеющихся на рынке редакторов двухмерной графики. Огромная гибкость и возможность сделать с двухмерным изображением все, что вам может потребоваться. URL: www.a...
Трехмерная графика
3ds max — эта программа, созданная компанией Discreet, мой любимый инструмент для трехмерного моделирования и анимации. Вы можете использовать его для создания используемых в игре моделей, их ани...
Сообщества разработчиков игр
GameDev.net — это мой любимый сайт разработчиков игр, содержащий множество статей и великолепный форум. Вы можете найти меня среди модераторов раздела Multiplayer. URL: www.gamedev.net. Flip Code...
Аппаратное обеспечение
ATI — ATI делает самые лучшие видеокарты, и их новая серия Radeon вне конкуренции. Если вы ищете хорошую видеокарту для практики в программировании шейдеров, загляните на www.ati.com. Nvidia — Nv...