Анимация атаки
Переменная, m_iNumAttackFrames, указывает, сколько кадров присутствует в анимационной последовательности, показываемой когда подразделение кого-нибудь атакует. Этот момент иллюстрирует рис.8.15.
Рис. 8.15. Кадры анимации танковой атаки
На рис. 8.15 видно, что для анимации атакующего танка используется два кадра. На первом кадре изображен обычный танк, а на втором кадре к его изображению добавляется вспышка выстрела. Красота этой системы в том, что при желании вы можете ее детализировать или упрощать произвольным образом. Для этого вам достаточно создать собственные кадры анимации и указать их количество.
Анимация гибели
Переменная m_iNumDieFrames сообщает вам сколько кадров содержится в анимационной последовательности, показываемой при гибели боевой единицы. Пример показан на рис. 8.16.
Рис. 8.16. Кадры гибели танка
Обратите внимание, что на рис. 8.16 для анимации гибели танка используются три кадра. В первом кадре изображен обычный танк, во втором кадре нарисован красивый взрыв, а в третьем кадре мы видим искореженный и обгоревший танк. Эта анимационная последовательность будет воспроизводиться всякий раз, когда гибнет подразделение, так что убедитесь, что она выглядит впечатляюще!
Я уверен, что для своих боевых единиц вы придумаете еще множество различных типов анимации. Самое замечательное, что для этого вам достаточно добавить несколько переменных в базовый класс анимации для хранения необходимой информации в вашей игре.
Анимация ожидания
Переменная m_iNumStillFrames сообщает сколько кадров используется в анимационной последовательности, изображающей боевую единицу в состоянии ожидания. Множество подразделений в состоянии ожидания ничего не делают, но поведение некоторых боевых единиц может быть очень сложным. Например, у радиолокационной станции скорее всего будет вращаться антена, что потребует нескольких кадров анимации. Танк, с другой стороны, в состоянии ожидания не выполняет никаких видимых действий. Это показано на рис.8.13.
Рис. 8.13. Кадры анимации ожидания для танка
Обратите внимание, что для танка, находящегося в состоянии ожидания, достаточно одного кадра. Это вызвано тем, что в состоянии ожидания танк ничего не делает!
Анимация передвижения
Следующая переменная, m_iNumMoveFrames, сообщает сколько кадров в анимационной последовательности, показываемой при передвижении боевой единицы. Пример показан на рис. 8.14.
Рис. 8.14. Кадры анимации передвижения танка
Как видите, при передвижении танка используются три кадра анимации. Положение колес на каждом из кадров слегка отличается. В результате, показываемые один за другим, эти кадры создают иллюзию движения.
Члены данных класса CTexture
Переменная m_szName хранит имя файла с текстурой, а переменная m_pTexture хранит загруженные данные. Еще раз упомяну переменную m_pd3dDevice. Она необходима для загрузки данных текстуры.
Члены данных класса CUnit
Давайте начнем с указателей на базовые типы. В рассматриваемом примере есть тип защиты, три типа атаки, тип перемещения и тип анимации. Я использую три типа атаки потому что на одной боевой единице может быть установлено несколько типов оружия. Например, на танке обычно установлена пушка и пулемет. Имея три типа атаки подразделение в игре может стрелять из трех различных типов оружия. Если вам не нужна такая сложность, просто удалите лишние типы атаки. Если же вы хотите усложнить игровой процесс, добавьте их!
Переменная m_iCurHitPoints хранит текущий показатель здоровья подразделения. Когда ее значение достигает 0, подразделение погибает. Максимально возможное значение этого поля хранится в переменной m_iHitPoints класса защиты.
Переменная m_fCurSpeed показывает текущую скорость подразделения. Чтобы вычислить куда переместилось подразделение следует умножить текущую скорость на вектор направления. Когда движение подразделения замедляется, выполняется вычитание из этого значения, а чтобы подразделение двигалось быстрее, увеличьте значение данного поля. Максимальное значение данного поля хранится в переменной базового класса типа перемещения с именем m_fMovementSpeed.
Переменные m_fXPos и m_fYPos хранят местоположение подразделения на карте. В рассматриваемом примере используется двухмерная графика и поэтому координат требуется тоже две — X и Y.
Переменная m_fRot указывает угол поворота боевой единицы в градусах. Это значение используется, когда необходимо развернуть подразделение по направлению к противнику или определить направление перемещения. Поскольку значение изменяется в градусах, допустимый диапазон значений — от 0.0 до 359.0.
Поле m_fScale задает текущий размер подразделения. Оно применяется для того, чтобы в двухмерной графике создать эффект приближения подразделения к камере. Обычно значение данной переменной равно 1.0, чтобы подразделение выглядело таким же, как и при разработке.
Переменная m_iUnitID хранит уникальный идентификатор подразделения. Он необходим для реализации многопользовательской игры. Очень трудно приказать другому компьютеру уничтожить подразделение, если вы не можете сообщить его идентификатор.
Поле m_iParentID указывает какое подразделение является владельцем данного. Переменная используется для транспортных средств, таких как десантные самолеты и авианосцы. Если значение переменной отличается от –1, значит данное подразделение перевозится другой боевой единицей. Если же значение равно –1, — подразделение не имеет назначенного родителя.
Массив символов m_szName хранит название подразделения. Оно используется для отображения в интерфейсе пользователя и других информационных целей.
Поле m_bActive сообщает вам, является ли подразделение активным в данный момент. Поскольку в игре для каждого игрока выделяется лишь ограниченное количество боевых единиц, погибшие подразделения должны отмечаться как неактивные, чтобы освободившиеся записи можно было использовать для других подразделений. Когда подразделение отмечено как активное, оно используется в игре и его запись не может использоваться для других целей.
Поле m_iOwner сообщает кто является владельцем данного подразделения. Одно из его применений — назначение цветов владельца при отображении графики.
Поле m_iCurAnimFrame указывает, какой именно кадр анимационной последовательности отображается в данное время.
Поле m_iCurAttackFrame следит за анимацией подразделения во время воспроизведения анимационной последовательности атаки. Это необходимо потому что у вас может быть несколько кадров для каждого типа анимации.
Поле m_iCurStillFrame работает так же как и предыдущее, но следит за анимацией ожидания, а не атаки. Оно используется в те моменты, когда подразделение ничем не занято.
Поле m_iCurMoveFrame похоже на остальные счетчики кадров анимации, но используется когда подразделение перемещается.
Переменная m_iCurDieFrame работает также как и предыдущие счетчики кадров анимации и используется только при гибели подразделения. О-ох, взгляните на эти взрывы!
Чтобы увидеть как переменные состояния связаны с базовыми типами, взгляните на рис. 8.21.
Рис. 8.21. Взаимосвязь между переменными состояния и базовыми типами
На рис. 8.21 вы можете видеть как переменные состояния связаны с соответствующими базовыми типами. Например, максимальное значение номера кадра анимации ожидания берется из находящегося в классе анимации поля с количеством кадров анимации ожидания.
Члены данных класса CUnitAnimation
В классе анимации есть несколько интересных членов данных. Первые из них хранят количество графическх кадров в различных анимационных последовательностях подразделения. Существует четыре типа анимации: ожидание, движение, атака и гибель.
Члены данных класса CUnitDefense
Класс очень простой и содержит только открытые члены данных, конструктор, деструктор и единственный метод. Главным образом я использую классы как структуры данных, так что не ожидайте наличия сотен методов. Так же помните, что данный пример значительно упрощен, чтобы сделать его более легким для понимания. В реальном приложении вы можете сделать члены данных закрытыми и добавить методы для доступа к ним. Так или иначе, продолжаем разговор. Взгляните на рис8.8, где показана структура переменных класса способа защиты.
Рис. 8.8. Структура переменных класса CUnitDefense
Члены данных класса CUnitMovement
Класс содержит переменные, аналогичные тем, которые находятся в классе атаки, за исключением того, что их значения относятся к перемещению, а не атаке. Члены данных класса показаны на рис.8.11.
Рис. 8.11. Переменные класса CUnitMovement
Члены данных класса CUnitOffense
Члены данных класса выглядят очень знакомо, за исключением того, что эти значения относятся к атаке, а не к обороне. Члены данных класса показаны на рис.8.9.
Рис. 8.9. Структура переменных класса CUnitOffense
Цвета владельца
Здесь начинаются хитрости анимационной графики. Функция загрузки текстур выделяет необходимую для текстур память, а затем в цикле перебирает кадры каждой анимационной последовательности, загружая данные текстур. Но для чего нужна константа UNITMANAGER_MAXOWNERS? Очень хороший вопрос!
Давайте еще раз взглянем на код, вычисляющий общее количество кадров:
m_Textures = new CTexture[ (m_iNumStillFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumMoveFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumAttackFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumDieFrames * (UNITMANAGER_MAXOWNERS + 1))];
Все выглядит нормально, за исключением операций умножения. Константа UNITMANAGER_MAXOWNERS содержит общее количество доступных в игре цветов игроков. Я использую это значение, чтобы узнать, сколько различных цветов для игроков поддерживает игра. Если вы раньше уже играли в стратегические игры, то знаете, что подразделения разных игроков отмечаются разными цветами. У одного игрока на боевых единицах могут быть красные полоски, в то время как у другого игрока эти же полоски будут пурпурными. Для этого необходимы дополнительные кадры анимации: для каждого кадра анимации столько, сколько доступно цветов.
Например, если в анимационной последовательности ожидания один кадр, вам необходим этот кадр плюс по одному кадру для каждого доступного цвета владельца. Общее количество кадров вычисляется по формуле:
Количество_кадров_анимации * (Количество_цветов + 1)
Я прибавляю к количеству цветов 1, чтобы учесть исходный кадр. Кадры с цветами содержат только информацию о раскраске боевой единицы, в то время как исходный кадр содержит изображение самого подразделения. Если вам трудно это понять, взгляните на рис.8.18.
Рис. 8.18. Кадры с цветами владельца для вертолета Apache
На рис. 8.18 показаны кадры состояния ожидания для вертолета Apache. Первый кадр содержит изображение самой боевой единицы. На нем вы видите корпус вертолета, оружие, механизмы и лопасти пропеллера. На следующих кадрах изображена только накладываемая на исходное изображение раскраска. В примере поддерживается только четыре варианта раскраски, так что вы видите четыре кадра, каждый со своим цветом. Черно-белые изображения вам не слишком помогут, так что лучше загрузить графику из сопроводительных файлов. Она находится в каталоге D3DFrame_UnitTemplate\UnitData. Загрузите файлы apache0_0.tga, apache0_1.tga, apache0_2.tga, apache0_3.tga и apache0_4.tga. Файл apache0_0.tga содержит базовое изображение, а остальные файлы содержат только данные о цветах владельца.
Спрашивается, как это влияет на анимационную последовательность? Весьма сильно! И снова одна картинка гораздо лучше тысячи слов, так что смотрите на рис. 8.19.
Рис. 8.19. Анимационная последовательность для танка с учетом цветов владельца
На рис. 8.19 показаны анимационная последовательность ожидания и анимационная последовательность передвижения для танка, которые я уже демонстрировал ранее. Однако здесь в них внесено несколько изменений. Во-первых увеличилось количество кадров анимации. Это вызвано тем, что помимо основного кадра теперь в последовательности присутствуют и кадры с цветами владельца. Результат ясно виден на кадре анимации ожидания. Анимация ожидания состоит из одного кадра, но вместе с ним хранятся данные кадров с четырьмя цветами владельцев. Теперь только для анимации ожидания требуется целых пять кадров.
Взгляните таже на приведенную на рис. 8.19 анимационную последовательность передвижения. В предыдущем примере я показал вам, что кадры анимации передвижения размещаются один за другим. На самом деле между ними располагаются кадры с цветами владельца. Первый кадр анимации передвижения находится в кадре с номером 5, и за ним следуют четыре кадра с цветами владельца. Следующий кадр анимационной последовательности передвижения расположен в десятом кадре, и за ним так же следуют четыре кадра с цветами. Последний кадр анимации передвижения находится в кадре с номером 15 и за ним следуют последние четыре кадра с цветами владельца, необходимые для анимации.
Давайте еще раз взглянем на цикл, загружающий кадры анимации ожидания:
m_iStartStillFrames = 0; for(i = 0; i < m_iNumStillFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j);
// Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; }
Сначала я указываю, что анимация ожидания начинается с кадра с номером 0. Поскольку анимационная последовательность ожидания является самой первой в массиве, она начинается с кадра с индексом 0.
Затем следует внешний цикл. Он перебирает все кадры, входящие в заданную анимационную последовательность. В примере с танком для анимации ожидания требуется только один кадр, поэтому тело цикла выполнится только один раз.
Теперь начинается внутрений цикл. Количество его выполнений равно количеству цветов владельца плюс один. Благодаря этому он загружает базовый кадр анимации и все кадры с цветами владельца для каждого кадра анимационной последовательности. Внутири цикла на лету создаются имена загружаемых файлов по следующему шаблону:
UnitData\\ПрефиксТекстуры_НомерКадра_НомерЦвета.tga
Вместо поля ПрефиксТекстуры подставляется префикс имени файла с текстурой. Для танка вы можете выбрать префикс «TankGraphic». Для вертолета Apache я использую префикс «Apache».
Поле НомерКадра заменяется на номер кадра в анимационной последовательности. Поскольку анимационная последовательность для ожидания состоит из одного кадра, в это поле помещается 0.
Поле НомерЦвета содержит номер загружаемого кадра с цветами владельца. Базовому кадру с изображением боевой единицы соответствует номер 0.
После того, как название файла создано, я задаю устройство визуализации для объекта текстуры, после чего вызываю функцию загрузки объекта текстуры. Завершив эти действия я увеличиваю счетчик общего количества загруженных текстур и заканчиваю цикл.
Дальнобойность
Переменная m_iRange сообщает вам количество блоков игрового поля, на которое может выстрелить данный тип оружия. Это применимо только к вооружению, действующему на расстоянии, поскольку для ручного холодного оружия дальнобойность равна нулю.
Данные текстуры
Указатель m_Textures применяется для хранения кадров анимации подразделения. Он указывает на массив объектов CTexture и замечательно справляется с задачей хранения информации.
Переменная m_iTotalTextures сообщает вам, сколько всего кадров анимации требуется для данного подразделения. Она, помимо всего прочего, полезна для контроля за расходованием памяти.
Последний относящийся к текстурам член данных — m_pd3dDevice. Он содержит указатель на графическую систему Direct3D используемый при загрузке текстур. Поскольку этот указатель необходим функциям загрузки текстур в DirectX, я включил его в класс текстуры.
Функция CTexture::vLoad()
Функция загрузки пользуется весьма полезной вспомогательной библиотекой DirectX чтобы загрузить графическое изображение из файла в буфер данных текстуры. Вот как выглядит код этой функции:
void CTexture::vLoad(char *szName) { // Сохраняем имя файла strcpy(m_szName, szName); // загружаем текстуру D3DXCreateTextureFromFile(m_pd3dDevice, m_szName, &m_pTexture); }
Первая строка кода функции сохраняет переданное в параметре имя файла текстуры для последующего использования. Я в дальнейшем не использую это имя, но его наличие очень полезно, если вдруг потребуется заново загрузить данные текстуры.
Затем функция загружает текстуру с помощью вспомогательной функции DirectX. Вы уже видели аналогичный код ранее, так что здесь никаких сюрпризов возникнуть не должно.
Функция CTexture::vRelease()
Функция освобождения ресурсов очень проста, поскольку ей необходимо только освободить выделенную для хранения текстуры память. Вместо оператора delete используется метод Release, поскольку это требование DirectX. Код функции приведен ниже:
void CTexture::vRelease(void) { // Удаление текстуры, если она есть в памяти if(m_pTexture) { m_pTexture->Release(); m_pTexture = NULL; } }
Сначала я проверяю, была ли выделена память для объекта текстуры; если да, то я вызываю метод для освобождения памяти, занятой данными текстуры. В результате данные удаляются из памяти.
Функция CTexture::vSetRenderDevice()
Чтобы обеспечить возможность установки внутреннего указателя на устройство визуализации, я предоставляю функцию задания устройства визуализации. Она получает указатель на основное устройство трехмерной визуализации и сохраняет его в локальной переменной объекта текстуры. Если вы хотите взглянуть на код, откройте файл UnitTemplateClasses.cpp.
Вот и все, ребята! Я стремительно пролетел сквозь класс текстуры, но он действительно очень прост и не требует особого внимания. Надеюсь, вы согласны. Если нет, загрузите Age of Mythology и пришлите мне ICQ с предложением поиграть!
Функция CUnit::vReset()
Функция установки начальных значений работает точно так же, как и в других, рассмотренных в этой главе классах, присваивая данным подразделения значения по умолчанию. Здесь нет ничего сложного, поэтому я пропущу описание этой функции, посмотреть на которую можно в коде проекта.
Функция CUnit::vSetBaseValues()
Данная функция устанавливает указатели на базовые классы для подразделения. Вы можете сделать это вручную, но наличие одной простой функкции сделает вашу жизнь чуть легче. Вот как выглядит код функции:
void CUnit::vSetBaseValues(CUnitDefense* ptrDef, CUnitOffense* ptrOff1, CUnitOffense* ptrOff2, CUnitOffense* ptrOff3, CUnitMovement* ptrMove, CUnitAnimation* ptrAnimation) { // Указатели на переданные классу объекты m_Defense = ptrDef; m_Offense1 = ptrOff1; m_Offense2 = ptrOff2; m_Offense3 = ptrOff3; m_Movement = ptrMove; m_Animation = ptrAnimation; }
В коде я присваиваю внутренним указателям на базовые типы переданные функции параметры. Параметров всего шесть: один для защиты, один для передвижения, один для анимации и три для атаки.
Функция CUnit::vSetPosition()
Функция задания местоположения позволяет установить координаты X и Y подразделения с помощью одного вызова. Она получает данные о новом местоположении подразделения и сохраняет их во внутренних переменных. Вот как выглядит код:
void CUnit::vSetPosition(float fX, float fY) { m_fXPos = fX; m_fYPos = fY; }
Вот так, красиво и просто!
Функция CUnitAnimation::vLoadTextures()
Функция загрузки текстур получает информацию, хранящуюся в относящихся к кадрам анимации членах данных класса и загружает соответствующие файлы с текстурами. Вот ее код:
void CUnitAnimation::vLoadTextures(void) { // Загрузка анимаций int i, j; int iLocalCount = 0; char szBitmapFileName[128];
// Выделение памяти для текстур m_Textures = new CTexture[ (m_iNumStillFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumMoveFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumAttackFrames * (UNITMANAGER_MAXOWNERS + 1))+ (m_iNumDieFrames * (UNITMANAGER_MAXOWNERS + 1))];
// Графика для ожидания (покоя) m_iStartStillFrames = 0; for(i = 0; i < m_iNumStillFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j);
// Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; }
// Графика для перемещения m_iStartMoveFrames = m_iTotalTextures; for(i = 0; i < m_iNumMoveFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j);
// Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; }
// Графика для атаки m_iStartAttackFrames = m_iTotalTextures; for(i = 0; i < m_iNumAttackFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j);
// Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; }
// Графика для гибели m_iStartDieFrames = m_iTotalTextures; for(i = 0; i < m_iNumDieFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j);
// Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } }
Пожалуйста, не бейте меня! Я понимаю, что это большой фрагмент кода, но к счастью в нем много повторяющихся фрагментов. В работе кода можно выделить два основных этапа. На первом этапе осуществляется выделение памяти для объектов текстур. Здесь вычисляется количество текстур, необходимых для добавления всех кадров анимации. На втором этапе для каждой анимационной последовательности выполняется цикл в котором загружаются необходимые для нее текстуры.
Функция CUnitAnimation::vReset()
Поскольку в класс анимации включены графические данные, функция установки начальных значений стала сложнее. Это вызвано тем, что функция должна освобождать память, выделенную для хранения текстур. Вот как выглядит код:
void CUnitAnimation::vReset(void) { memset(m_szName, 0x00, 64); memset(m_szBitmapPrefix, 0x00, 64); // Освобождаем память текстур if(m_iTotalTextures) { delete [] m_Textures; m_Textures = NULL; m_iTotalTextures = 0; } m_iNumStillFrames = 0; m_iNumMoveFrames = 0; m_iNumAttackFrames = 0; m_iNumDieFrames = 0; m_iType = 0; m_iStartStillFrames = 0; m_iStartMoveFrames = 0; m_iStartAttackFrames = 0; m_iStartDieFrames = 0; }
Как видно в коде, чтобы определить наличие текстур я проверяю значение переменной m_iTotalTextures. Если какие-либо текстуры загружены, я удаляю массив m_Textures и устанавливаю количество загруженных текстур равным 0. Просто, не так ли?
Функция CUnitAnimation::vSetRenderDevice()
Поскольку DirectX для загрузки текстуры необходимо устройство визуализации, я добавил функцию установки устройства визуализации, которая инициализирует указатель на устройство. В единственном параметре этой функции передается указатель LPDIRECT3DDEVICE9, который сохраняется в члене данных m_pd3dDevice. Позднее он будет использован для загрузки данных текстуры.
Вот как выглядит код этой функции:
void CUnitAnimation::vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d) { m_pd3dDevice = pd3d; }
Функция CUnitManager::iAddUnit()
Когда вы хотите ввести в игру новое подразделение, следует вызвать функцию добавления подразделения. Она находит неактивное подразделение и инициализирует его данные для использования в игре. Вот как выглядит код этой функции:
int CUnitManager::iAddUnit(char *szName, int iOwner) { int i; int iFoundID = -1;
// Ищем соответствующий тип for(i = 0; i < m_iTotalUnitBaseObjs; i++) { if(!stricmp(szName, m_UnitBaseObjs[i].m_szName)) { iFoundID = i; break; } } // Возвращаемся, если базовый тип не найден if(iFoundID == -1) { return(-1); } // Ищем свободный блок данных подразделения for(i = 0; i < m_iTotalUnitObjs; i++) { // Проверяем, является ли блок неактивным if(!m_UnitObjs[i].m_bActive) { // Активируем подразделение m_UnitObjs[i].m_bActive= 1; // Устанавливаем его внутренние типы m_UnitObjs[i].vSetBaseValues( m_UnitBaseObjs[iFoundID].m_Defense, m_UnitBaseObjs[iFoundID].m_Offense1, m_UnitBaseObjs[iFoundID].m_Offense2, m_UnitBaseObjs[iFoundID].m_Offense3, m_UnitBaseObjs[iFoundID].m_Movement, m_UnitBaseObjs[iFoundID].m_Animation); // Устанавливаем тип подразделения m_UnitObjs[i].m_iType = iFoundID; // Устанавливаем владельца подразделения m_UnitObjs[i].m_iOwner = iOwner; // Увеличиваем количество подразделений у владельца m_iOwnerTotal[iOwner]++;
return(i); } } return(-1); }
Первая часть кода функции в цикле перебирает все базовые типы подразделений и пытается найти тот из них, название которого совпадает со строкой, переданной в параметре функции. Если совпадение найдено, сохраняется идентификатор подразделения и выполнение функции продолжается.
ПРИМЕЧАНИЕ
Следующая часть кода в цикле перебирает список всех подразделений игрока, ища неактивную запись. Данный этап необходим потому что активные подразделения уже присутствуют в игре и мы не можем использовать здесь относящиеся к ним записи. После обнаружения неактивного подразделения, оно делается активным и выполняется установка его базовых типов. В конце задается тип подразделения и его владелец. Я также отслеживаю сколько подразделений находится у каждого владельца, чтобы не превысить установленный лимит, если он существует.
Еще раз проясню ситуацию: массив m_UnitObjs хранит данные подразделений, которые изменяются во время игры, а массив m_UnitBaseObjs хранит шаблоны подразделений, которые никогда не меняются. Объекты m_UnitObjs меняют свои данные состояния, а объекты m_UnitBaseObjs— нет. Взаимосвязь между базовыми типами и динамическими объектами показана на рис. 8.27.
Рис. 8.27. Взаимосвязь между статическими базовыми данными и динамическими объектами подразделений
На рис. 8.27 видно, как динамические данные подразделений из массива m_UnitObjs используют в качестве основы данные, хранящиеся в базовых типах.
Функция CUnitManager::iLoadBaseTypes()
Вы узнали, качие элементы класса хранят информацию базовых типов, но как загружаются данные? Здесь вступает в игру функция iLoadBaseTypes(). Она получает пять параметров, каждый из которых является именем файла, содержащего импортируемые данные. Отдельные файлы требуются для данных защиты, данных атаки, данных передвижения, данных анимации и данных подразделений. Функция загрузки базовых типов получает имена пяти файлов и импортирует данные из них в диспетчер подразделений. На рис. 8.23 показана взаимосвязь между классом диспетчера подразделений и импортируемыми файлами.
Рис. 8.23. Импорт данных из пяти различных файлов в базовые типы CUnitManager
На рис. 8.23 показано как диспетчер подразделений загружает информацию в базовые типы из пяти различных файлов данных. Имена этих файлов BaseType_Defense.csv, BaseType_Offense.csv, BaseType_Movement.csv, BaseType_Unit.csv и BaseType_Animation.csv. Расширение имени файла .csv обозначает, что это файлы в формате с разделенными запятыми значениями. Такие файлы содержат значения, разделенные запятыми. Это общепринятый формат, поддерживаемый электронными таблицами, поскольку он позволяет сохранять данные в простом для импортирования формате. Лично я для ввода и редактирования информации о подразделениях использую программу работы с электронными таблицами Excel. Вот пример данных для базовых типов защиты:
Medium Heli Armor, 20, 2, 2, 30, 30, 0
Heavy Heli Armor, 30, 2, 2, 50, 100, 0
Light Heli Armor, 10, 2, 2, 20, 70, 0
Числа не имеют особого смысла, пока вы не увидите соответствующие им названия столбцов. В приведенном выше примере первый столбец содержит название типа защиты. Последующие столбцы содержат коэффициент защиты от пуль, коэффициент защиты от ракет, коэффициент защиты от лазера, коэффициент защиты от ручной схватки, максимальное количество очков повреждений и скорость восстановления.
Как видно из приведенных чисел, тяжелая броня обеспечивает лучшую защиту от пуль и рукопашной схватки, чем средняя или легкая. Это становится еще более очевидным, если загрузить данные в программу работы с электронными таблицами. Взгляните на рис. 8.24, чтобы увидеть как типы защиты выглядят в Excel.
Рис. 8.24. Данные защиты в электронной таблице Excel
На рис. 8. 24 показаны уже представленные ранее данные, но в виде гораздо лучше выглядящей электронной таблицы с названиями столбцов. Если у вас есть программа для работы с электронными таблицами или базами данных, экспорт в формат CSV осуществляется очень легко. Загляните в папку проекта D3DFrame_UnitTemplate, находящуюся среди сопроводительных файлов на CD-ROM и вы найдете там папку UnitData, содержащую csv-файлы с информацией о подразделениях, необходимой для данного примера.
Я дал вам краткое изложение, а теперь настало время для кода. В первой части функции я с помощью следующего кода открываю файл с данными типов защиты:
// Открываем файл с данными базового типа fp = fopen(szDefFileName, "r"); if(fp == NULL) { return(-1); } // Читаем строку с заголовками столбцов и игнорируем ее fgets(szTempBuffer, 512, fp); szTempBuffer[strlen(szTempBuffer) - 1] = '\0'; // Устанавливаем общее количество объектов равным 0 m_iTotalDefObjs = 0;
После того, как файл открыт, я считываю первую строку текста. Она содержит названия столбцов, так что после чтения эти данные игнорируются. Затем количество объектов защиты устанавливается равным 0. После завершения описанных действий я последовательно считываю каждую строку файла, анализирую ее и инициализирую полученными данными очередной тип защиты. Вот код, выполняющий эти действия:
// Последовательный перебор строк файла while(!feof(fp)) { // Получаем следующую строку fgets(szTempBuffer, 512, fp); if(feof(fp)) { break; } // Добавляем разделитель szTempBuffer[strlen(szTempBuffer)-1] = '\0'; iStart = 0; iEnd = 0; iCurPos = 0; iCurValue = 0; // Извлекаем значение while(szTempBuffer[iCurPos] != '\0' && iCurPos < 512) { // Проверяем достигли ли конца значения if(szTempBuffer[iCurPos] == ',') { iEnd = iCurPos; memset(&szValue[iCurValue][0], 0x00, 32); memcpy(&szValue[iCurValue], &szTempBuffer[iStart], iEnd - iStart); iStart = iEnd + 1; iCurValue++; } iCurPos++; }; // Импорт последнего столбца iEnd = iCurPos; memset(&szValue[iCurValue][0], 0x00, 32); memcpy(&szValue[iCurValue], &szTempBuffer[iStart], iEnd - iStart); iStart = iEnd + 1; iCurValue++; ...
Как видите, я извлекаю значения, находящиеся между запятыми и сохраняю их во временном символьном массиве с именем szValue. Как только все значения из строки помещены во временный массив, я копирую их в объект типа защиты. Это происходит в следующем фрагменте кода:
// Идентификатор типа m_DefenseObjs[m_iTotalDefObjs].m_iType = m_iTotalDefObjs; // Название strcpy(m_DefenseObjs[m_iTotalDefObjs].m_szName, &szValue[0][0]); // Коэффициент защиты от пуль m_DefenseObjs[m_iTotalDefObjs].m_iBulletArmorRating = atoi(&szValue[1][0]); // Коэффициент защиты от ракет m_DefenseObjs[m_iTotalDefObjs].m_iMissileArmorRating = atoi(&szValue[2][0]); // Коэффициент защиты от лазера m_DefenseObjs[m_iTotalDefObjs].m_iLaserArmorRating = atoi(&szValue[3][0]); // Коэффициент защиты в рукопашной m_DefenseObjs[m_iTotalDefObjs].m_iMeleeArmorRating = atoi(&szValue[4][0]); // Очки повреждений m_DefenseObjs[m_iTotalDefObjs].m_iMeleeArmorRating = atoi(&szValue[5][0]); // Скорость восстановления m_DefenseObjs[m_iTotalDefObjs].m_iMeleeArmorRating = atoi(&szValue[6][0]); // Увеличиваем количество объектов m_iTotalDefObjs++; } fclose(fp);
В приведенном выше коде видно как значения получаются из временного буфера и сохраняются в массиве m_DefenseObj. Как только все значения сохранены, я увеличиваю общее количество объектов типов защиты и вновь повторяю тело цикла. Эти действия повторяются, пока есть информация, которую можно считать из файла; после этого файл закрывается.
Абсолютно так же происходит обработка данных для типов защиты и передвижения. Данные анимации обрабатываются слегка отличным образом. Поскольку данные анимации связханы с графикой, процедура загрузки данных анимации должна загружать не только данные базового типа, но и текстуры. Вот фрагмент кода, который загружает данные для анимации:
// Тип идентификатора m_AnimationObjs[m_iTotalAnimationObjs].m_iType = m_iTotalAnimationObjs; // Имя memset(m_AnimationObjs[m_iTotalAnimationObjs].m_szName, 0x00, 64); strcpy(m_AnimationObjs[m_iTotalAnimationObjs].m_szName, &szValue[0][0]); // Префикс memset(m_AnimationObjs[m_iTotalAnimationObjs].m_szBitmapPrefix, 0x00, 64); strcpy(m_AnimationObjs[m_iTotalAnimationObjs].m_szBitmapPrefix, &szValue[1][0]); // Количество кадров ожидания m_AnimationObjs[m_iTotalAnimationObjs].m_iNumStillFrames = atoi(&szValue[2][0]); // Количество кадров перемещения m_AnimationObjs[m_iTotalAnimationObjs].m_iNumMoveFrames = atoi(&szValue[3][0]); // Количество кадров атаки m_AnimationObjs[m_iTotalAnimationObjs].m_iNumAttackFrames = atoi(&szValue[4][0]); // Количество кадров гибели m_AnimationObjs[m_iTotalAnimationObjs].m_iNumDieFrames = atoi(&szValue[5][0]); // Установка устройства визуализации m_AnimationObjs[m_iTotalAnimationObjs].vSetRenderDevice(m_pd3dDevice); // Загрузка текстур m_AnimationObjs[m_iTotalAnimationObjs].vLoadTextures(); // Увеличение количества объектов m_iTotalAnimationObjs++;
Приведенный выше код похож на остальные фрагменты кода за исключением вызовов двух методов объекта анимации. Первый из них, vSetRenderDevice(), устанавливает внутренний указатель объекта анимации на устройство визуализации Direct3D. Это позволяет объекту загружать текстуры. Второй метод, vLoadTextures(), использует информацию, хранящуюся в csv-файле данных анимации для загрузки необходимых для анимации текстур. Он формирует имена файлов, комбинируя заданный в данных анимации префикс растровой графики со значением счетчика кадров. На рис. 8.25 показаны данные для типов атаки.
Рис. 8.25. Данные атаки хранящиеся в электронной таблице Excel
Следом загружаются данные подразделений. Здесь все происходит так же, как и при загрузке типов защиты, атаки и передвижения, за исключением того, что логика загрузки данных подразделений использует другие загруженные ранее базовые типы. Вот как выглядит выполняющий эту задачу фрагмент кода:
// Тип защиты ptrDefense = ptrGetDefenseType(&szValue[1][0]); // Первый тип атаки ptrOffense1 = ptrGetOffenseType(&szValue[2][0]); // Второй тип атаки ptrOffense2 = ptrGetOffenseType(&szValue[3][0]); // Третий тип атаки ptrOffense3 = ptrGetOffenseType(&szValue[4][0]); // Тип передвижения ptrMovement = ptrGetMoveType(&szValue[5][0]); // Тип анимации ptrAnimation = ptrGetAnimType(&szValue[6][0]); // Установка базовых типов m_UnitBaseObjs[m_iTotalUnitBaseObjs].vSetBaseValues( ptrDefense, ptrOffense1, ptrOffense2, ptrOffense3, ptrMovement, ptrAnimation);
В приведенном выше коде я устанавливаю для подразделения типы защиты, атаки, передвижения и анимации. Это делается с помощью вызова различных методов диспетчера подразделений, задачей которых является получение экземпляра базового типа по его имени. Первым вызывается метод с именем ptrGetDefenseType(). Данные, о которых я только что рассказывал представлены на рис. 8.26.
Рис. 8.26. Данные подразделений хранящиеся в электронной таблице Excel
Функция CUnitManager::ptrGetDefenseType()
Данная функция получает в своем единственном параметре строку и ищет тип защиты с указанным именем. Если такой тип найден, функция возвращает указатель на него. Вот как выглядит код этого бриллианта:
CUnitDefense* CUnitManager::ptrGetDefenseType(char *szName) { int i; CUnitDefense *ptrUnitDefense = NULL;
for(i = 0; i < m_iTotalDefObjs; i++) { if(!stricmp(szName, m_DefenseObjs[i].m_szName)) { ptrUnitDefense = &m_DefenseObjs[i]; return(ptrUnitDefense); } }
return(ptrUnitDefense); }
Код представляет собой простой цикл, перебирающий все загруженные типы защиты. Название каждого типа защиты сравнивается с переданной функции строкой. Если строки совпадают, возвращается указатель на тип защиты. Это позволяет вызывающему коду использовать данные типа защиты без создания копии данных, благодаря чему уменьшается объем занимаемой памяти.
Функции, подобные рассмотренной выше, используются для получения указателей на типы атаки, передвижения и анимации. Я не буду приводить их здесь, поскольку они практически идентичны уже рассмотренному коду и вы можете увидеть их в файле UnitTemplateClasses.cpp.
Вернемся к нашей на время отложенной программе. Теперь, когда у нас есть указатели на различные типы, мы можем сохранить их в объекте боевой единицы с помощью функции vSetBaseValues(). После этого базовый тип подразделения готов к использованию.
Вот и все, что я хотел рассказать о коде, импортирующем данные базовых типов в диспетчер подразделений. Я знаю, что материал достаточно сложен и вам возможно придется несколько раз прочитать его, прежде чем все станет понятно.
Функция vDrawUnit()
Теперь, когда у вас есть буфер вершин для подразделения, необходимо место для выполнения визуализации. Здесь выходит на сцену функция рисования подразделения. Она работает точно так же, как функция рисования блока игрового поля, которую я показывал в главе 5, за исключением некоторых добавленных функций. Вот ее код:
void CD3DFramework::vDrawUnit( float fXPos, float fYPos, float fXSize, float fYSize, float fRot, CUnitAnimation *animObj, int iTexture, int iOwner) { D3DXMATRIX matWorld; D3DXMATRIX matRotation; D3DXMATRIX matTranslation; D3DXMATRIX matScale;
// Установка значений по умолчанию для // местоположения, масштабирования и вращения D3DXMatrixIdentity(&matTranslation); // Масштабирование блока D3DXMatrixScaling(&matScale, fXSize, fYSize, 1.0f); D3DXMatrixMultiply(&matTranslation,&matTranslation,&matScale); // Вращение блока D3DXMatrixRotationZ(&matRotation, (float)DegToRad(-fRot)); D3DXMatrixMultiply(&matWorld, &matTranslation, &matRotation); // Перемещение блока matWorld._41 = fXPos - 0.5f; // X-Pos matWorld._42 = fYPos + 0.5f; // Y-Pos // Установка матрицы m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); // Используем буфер вершин блока m_pd3dDevice->SetStreamSource( 0, m_pVBUnit, 0, sizeof(TILEVERTEX)); // Используем фрмат вершин блока m_pd3dDevice->SetFVF(D3DFVF_TILEVERTEX); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture + iOwner + 1].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Разыменовываем текстуру m_pd3dDevice->SetTexture(0, NULL); }
Первое отличие данной функции от ранее рассмотренной vDrawTile() — добавление параметра вращения. Он позволяет вам развернуть двухмерное изображение на любой необходимый угол. Само вращение реализуется перемножением матриц поворота и преобразования. Матрица поворота создается вспомогательной функцией DirectX D3DXMatrixRotationZ().
СОВЕТ
Самое большое отличие данной функции — добавление параметра CUnitAnimation. Он сообщает функции откуда она должна брать текстуры. Указатель на класс анимации необходим потому, что именно в нем хранятся используемые для визуализации текстуры.
Я задаю используемую для визуализации текстуру путем передачи в функцию параметра, указывающего ее местоположение во внутреннем массиве класса анимации. В результате отображается базовое изображение подразделения. В следующием вызове функции визуализации изменена позиция в массиве для того, чтобы отображались цвета владельца. Если вы помните, раньше я говорил о том, что цвета владельца хранятся следом за кадром анимации. Данные о цветах накладываются на базовое изображение подразделения, чтобы в результате отображалось изображение подразделения, раскрашенного в цвета его владельца.
Функция vRender()
Сейчас у вас есть буфер вершин для подразделения и функция, помогающая при визуализации. Белым пятном остается место, где создается изображение каждого кадра. Встречайте старого знакомого— функцию vRender().
Функция визуализации в программе D3DFrame_UnitTemplate работает во многом так же, как и одноименная функция из программы D3DFrame_2DTiles. В первой части выполняется визуализация карты посредством цикла, в котором перебираются и отображаются отдельные блоки карты. На этом подобие заканчивается.
Использование альфа-канала
Первые отличия кода функции проявляются в том месте, где я включаю альфа-смешивание. Это действие позволяет видеть блоки карты сквозь текстуры подразделений. Сам процесс достаточно прямолинеен и осуществляется путем изменения нескольких состояний визуализации. Вот как выглядит код:
// Включение прозрачности m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE); m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
Первый вызов функции установки состояния визуализации сообщает системе визуализации DirectX о необходимости включить альфа-смешивание. Второй вызов сообщает системе о том, что будет выполняться смешивание текстуры подразделения с ее альфа-каналом. Последний вызов функции установки состояния визуализации сообщает системе что будет выполняться смешивание изображения-приемника с инвертированным альфа-каналом текстуры подразделения.
Класс CTexture
Как я упоминал ранее, класс текстур используется мной для хранения данных Почему я использую отдельный класс текстур? Я думаю, что такой подход упрощает переход к новым версиям DirectX. Вместо того, чтобы изменять во многих местах тип указателя на текстуру, я просто внесу изменения в класс текстуры. Кроме того, это позволяет мне абстрагироваться от используемых методов загрузки. Взгляните как выглядит заголовок класса:
class CTexture { public: // Название текстуры char m_szName[64]; // Указатель на текстуру LPDIRECT3DTEXTURE9 m_pTexture; // Указатель на устройство Direct3D для загрузки текстуры LPDIRECT3DDEVICE9 m_pd3dDevice;
CTexture(); ~CTexture(); virtual void vLoad(char *szName); virtual void vRelease(void); virtual void vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d); };
Класс не очень сложен, поскольку он всего лишь загружает и хранит данные текстуры.
Класс CUnit
Вот мы и рассмотрели все необходимые для подразделения базовые классы. У нас есть готовые к использованию данные защиты, атаки, передвижения и анимации. Отсутствует только клей, который соединит эти разрозненные компоненты вместе. По отдельности эти детали не слишком полезны, но собранные вместе они образуют подразделение. Здесь и вступает в игру класс CUnit. Он содержит указатели на различные базовые типы, а также ряд переменных состояния. Базовые типы хранят те данные подразделения, которые никогда не меняются, а данные состояния могут изменяться в зависимости от того, что происходит с подразделением. Все это иллюстрирует рис.8.20.
Рис. 8.20. Структура объекта подразделения
На рис. 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); };
С точки зрения количества функций класс не выглядит слишком сложным. Большая часть кода состоит из объявлений необходимых для игры переменных состояния. Это ни в коем случае нельзя считать достаточным для завершенного класса подразделения. Перечисленных переменных состояния достаточно только для рассматриваемого примера. В реальной игре их будет гораздо больше!
Класс CUnitAnimation
Также как и класс способов передвижения, класс анимации помогает организовать ваши подразделения. Я использую класс с именем CUnitAnimation. Вот как выглядит его заголовок:
const int UNITMANAGER_MAXOWNERS = 4; class CUnitAnimation { public: char m_szName[64]; char m_szBitmapPrefix[64]; int m_iNumStillFrames; int m_iNumMoveFrames; int m_iNumAttackFrames; int m_iNumDieFrames; int m_iType; int m_iStartStillFrames; int m_iStartMoveFrames; int m_iStartAttackFrames; int m_iStartDieFrames; // Данные текстуры CTexture *m_Textures; int m_iTotalTextures; // Указатель на устройство Direct3D для загрузки текстур LPDIRECT3DDEVICE9 m_pd3dDevice;
CUnitAnimation(); ~CUnitAnimation(); virtual void vReset(void); virtual void vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d); virtual void vLoadTextures(void); };
Ух ты, этот класс действительно отличается от предыдущих! Верно, класс анимации более сложный, чем его предшественники. Данный класс содержит графические изображения подразделения, а также информацию, необходимую для его анимации.
Изображения подразделения хранятся в массиве объектов класса CTexture. Класс CTexture— это отдельный класс, который я создал в данном приложении для хранения графической инфоримации. Мы обсудим его в этой главе чуть позже.
Класс CUnitDefense
Помните, как типы защиты помогают структурировать данные подразделений? Теперь вы добрались до практического примера, показывающего как реализовать эту концепцию в виде класса. Откройте заголовочный файл UnitTemplateClasses.h, входящий в проект D3DFrame_UnitTemplate. В начале этого файла вы увидите следующий код:
class CUnitDefense { public: int m_iType; unsigned int m_iMissileArmorRating; unsigned int m_iBulletArmorRating; unsigned int m_iLaserArmorRating; unsigned int m_iMeleeArmorRating; unsigned int m_iHitPoints; unsigned int m_iRegenRate; char m_szName[64];
public: CUnitDefense(); ~CUnitDefense(); virtual void vReset(void); };
Класс CUnitManager
Теперь у вас есть класс атаки, класс защиты, класс передвижения, класс анимации и даже класс подразделения, чтобы объединить все предыдущие классы в единое целое. Чего же не хватает? Класса для управления всей этой информацией! Все эти классы великолепны, но ручное управление ими подобно камешку в ботинке. Класс диспетчера выполняет эту работу за вас, объединяя различные строительные блоки из которых состоят подразделения в одном месте. Класс диспетчера решает за вас следующие задачи:
Загрузка базовых типов
Создание подразделений
Управление текстурами
Класс CUnitMovement
Класс способов передвижения также помогает организовать ваши боевые единицы. Для выполнения этой работы я использую класс CUnitMovement. Вот как выглядит его заголовок:
class CUnitMovement { public: int m_iType; float m_fMovementSpeed; unsigned int m_iMovementType; float m_fAcceleration; float m_fDeacceleration; float m_fTurnSpeed; char m_szName[64];
public: CUnitMovement(); ~CUnitMovement(); virtual void vReset(void); };
Класс CUnitOffense
Подобно классу типов защиты, класс типов атаки помогает вам организовать данные о подразделениях. Я пользуюсь классом с именем CUnitOffense, который выполняет за меня всю необходимую работу. Посмотрите на заголовок этого класса:
class CUnitOffense { public: int m_iType; unsigned int m_iMissileDamageRating; unsigned int m_iBulletDamageRating; unsigned int m_iLaserDamageRating; unsigned int m_iMeleeDamageRating; unsigned int m_iSplashRadius; unsigned int m_iRateOfFire; float m_fProjectileSpeed; unsigned int m_iRange; char m_szName[64];
public: CUnitOffense(); ~CUnitOffense(); virtual void vReset(void); };
Коэффициенты поражения
На рис 8.9 показаны четыре коэффициента поражения: для ракет, для пуль, для лазера и для рукопашной схватки. Точно также как и в классе обороны, эти значения относятся к тем типам атаки, которые упоминаются в их названии. Например, коэффициент поражения от пуль показывает, сколько повреждений наносит выпущенная из оружия пуля. Он может применяться для автоматической винтовки M-16, или для любого другого оружия, которое стреляет пулями. Я предпочитаю использовать для данного коэффициента тот же диапазон значений, что и для коэффициеттов защиты (в данном примере — от 0 до 1000). Это значительно упрощает вычисления, так как в этом случае для того, чтобы определеить полученные подразделением повреждения достаточно сравнить коэффициент защиты и коэффициент поражения. Взгляните на следующий пример:
У бронежилета коэффициент защиты от пуль равен 50.
У автоматической винтовки M-16 коэффициент поражения пулями равен 60.
60 - 50 = 10 единиц проникает сквозь защиту.
Из этого примера видно, что бронежилет поглощает 50 единиц наносимого пулей ущерба, а пуля, выпущенная из M-16 наносит 60 единиц повреждений. В результате 10 единиц повреждений проходят сквозь защиту и портят здоровье тому, на ком одет бронежилет. В результате у данного подразделения вычитается 10 очков повреждений, после чего оно, будем надеяться, остается в живых. Вот другой пример:
У бронежилета коэффициент защиты от пуль равен 50.
У 105-мм гаубицы коэффициент поражения пулями равен 650.
650 - 50 = 600 единиц проникает сквозь защиту.
Здесь видно, что против 105-мм гаубицы у бронежилета нет практически ни одного шанса. Подразделение получает 600 единиц повреждений и, скорее всего, будет уничтожено. И еще один, последний, пример:
У бронежилета коэффициент защиты от пуль равен 50.
У кольта 45 калибра коэффициент поражения пулями равен 30.
30 - 50 = -20 повреждений нет.
В данном примере коэффициент поражения кольта 45 калибра недостаточен, чтобы причинить какие-либо повреждения подразделению. Это показывает, что способ атаки может оказаться бесполезным против используемой защиты. При желании вы можете добавить для таких случаев модификатор удачи, чтобы бронежилет не всегда обеспечивал стопроцентную защиту от выпущенных из пистолета пуль — решать вам.
Как и в случае с коэффициентами защиты, вы можете изменять приведенный список, чтобы он соответствовал вашим потребностям.
Коэффициенты защиты
Как видно из рис. 8.8, я предусмотрел четыре коэфиициента защиты: для ракет, для пуль, для лазера и для рукопашной схватки. Это придает игре достаточную гибкость для поддержки четырех различных типов атаки. Возьмем, к примеру, бронежилет. Он достаточно хорош в качестве защиты от пуль, но недолго устоит против лазера. Поэтому для учета данной особенности при инициализации вы можете задать для бронежилета средний уровень защиты от пуль и низкий уровень защиты от лазера.
Вы всегда можете сократить или увеличить используемое в данном примере количество коэффициентов защиты. Мой пример приспособлен для футуристической военной игры, но вы, возможно будете реализовывать другой сценарий. Если вы решили создать игру в жанре фэнтези, вам потребуется заменить коэффициент защиты от лазера коэффициентом защиты от магии. Защита от ракет превратится в защиту от стрел, а вот защиту от пуль может придется оставить, по крайней мере, если у вас в игре будут пищали или другое огнестрельное оружие. Впрочем, возможно, вы решите заменить коэффициент защиты от пуль на коэффициент защиты от пороховых бомб.
Каждому коэффициенту защиты я присваиваю целочисленное значение. При этом я выбрал допустимый диапазон значений от 0 до 1000. Если коэффициент защиты равен 0, подразделение совершенно беззащитно перед данным типом атаки. Значение 1000 означает, что для данного способа атаки подразделение практически неуязвимо.
Методы класса CTexture
Помимо конструктора и деструктора в классе текстуры присутствуют три функции: vLoad(), vRelease() и vSetRenderDevice().
Методы класса CUnit
В классе CUnit я реализовал сравнительно мало методов. Идея проста— вы сами добавите необходимые вам методы, базируясь на потребностях собственного проекта. Итак, вот та часть работы, которую я проделал за вас.
Методы класса CUnitAnimation
В классе анимации есть уже ставшие привычными конструктор, деструктор и функция установки начальных значений, но к ним добавились две новые функции: vSetRenderDevice() и vLoadTextures().
Методы класса CUnitDefense
В классе есть только конструктор, деструктор и функция сброса значений переменных, так что в плане функциональности он весьма ограничен. Вот как выглядит код, реализующий перечисленные функции:
// Конструктор CUnitDefense::CUnitDefense() { // Установка внутренних переменных vReset(); } // Деструктор CUnitDefense::~CUnitDefense() { } // Сброс внутренних переменных void CUnitDefense::vReset(void) { m_iType = 0; m_iMissileArmorRating = 0; m_iBulletArmorRating = 0; m_iLaserArmorRating = 0; m_iMeleeArmorRating = 0; m_iHitPoints = 0; m_iRegenRate = 0; strcpy(m_szName, "N/A"); }
В приведенном выше коде видно, что для установки начальных значений внутренних переменных класса конструктор вызывает функцию vReset(). Деструктор не делает ничего полезного, а просто занимает место. Однажды он может понадобиться для каких-либо целей, но не сегодня.
Что может быть проще? Если же вам нравится сложный код, просто немного потерпите.
Методы класса CUnitMovement
В классе есть только констуктор, деструктор и функция установки начальных значений, работающая во многом так же как одноименная функция класса типов атаки. Вот как выглядит код реализации этих функций:
// Конструктор CUnitMovement::CUnitMovement() { // Установка внутренних значений vReset(); } // Деструктор CUnitMovement::~CUnitMovement() { } // Установка внутренних переменных void CUnitMovement::vReset(void) { m_iType = 0; m_fMovementSpeed = 0.0f; m_iMovementType = 0; m_fAcceleration = 0.0f; m_fDeacceleration = 0.0f; m_fTurnSpeed = 0.0f; strcpy(m_szName, "N/A"); }
В приведенном выше коде видно, что конструктор для установки начальных значений членов данных класса вызывает функцию vReset(), точно так же как это делалось в классе типов атаки. Я не пытаюсь нагрузить вас дубликатами одного и того же кода, просто сама природа классов делает их код очень похожим.
Методы класса CUnitOffense
В классе есть только констуктор, деструктор и функция установки начальных значений, действующая во многом так же как одноименная функция класса типов защиты. Вот как выглядит код реализации функций:
// Конструктор CUnitOffense::CUnitOffense() { // Установка внутренних переменных vReset(); } // Деструктор CUnitOffense::~CUnitOffense() { } // Сброс внутренних переменных void CUnitOffense::vReset(void) { m_iType = 0; m_iMissileDamageRating = 0; m_iBulletDamageRating = 0; m_iLaserDamageRating = 0; m_iMeleeDamageRating = 0; m_iSplashRadius = 0; m_iRateOfFire = 0; m_fProjectileSpeed = 0.0f; m_iRange = 0; strcpy(m_szName, "N/A"); }
В приведенном выше коде видно, что конструктор для установки начальных значений членов данных класса вызывает функцию vReset(), точно так же как это делалось в классе типов защиты. Вот и все, что можно сказать о работе класса типов атаки.
Начальный кадр анимации
Четыре переменных сообщают вам какой кадр является начальным для каждого типа анимации. Сперва это может звучать странно, и чтобы облегчить понимание взгляните на рис.8.17.
Рис. 8.17. Полная анимационная последовательность для танка
На рис. 8.17 показаны сразу все кадры анимации танка. Первый кадр — это изображение ожидающего танка. Следующие три кадра содержат анимационную последовательность для движения. Следующие два кадра содержат анимационную последовательность атаки. Последние три кадра содержат анимационную последовательность гибели. Вместо того, чтобы хранить кадры анимации в различных массивах, класс анимации сохраняет их все в одном непрерывном массиве. Это означает, что все кадры будут расположены один за другим. В результате, анимация ожидания начинается с нулевого кадра, а анимационная последовательность перемещения — нет. Стартовый кадр каждой последовательности зависит от того, сколько кадров находится перед ним. Рассмотрим для примера анимационную последовательность атаки. Она начинается с четвертого кадра в цепочке, поскольку перед ней расположены кадр для состояния ожидания и анимационная последовательность передвижения. Помните, что номер первого кадра в цепочке — 0, а не 1. Взглянув еще раз на рисунок, вы заметите, что под каждым кадром приведен связанный с ним порядковый номер. В данном примере анимационная последовательность ожидания начинается с кадра 0, анимационная последовательность передвижения — с кадра 1, анимационная последовательность атаки — с кадра 4 и анимационная последовательность гибели — с кадра 6. Если вы добавите кадры в середину набора, номера начальных кадров расположенных правее анимационных последовательностей должны быть увеличены.
Название подразделения
Очевидно, сперва следует выбрать название подразделения. Если ваша игра относится ко Второй Мировой войне, вы можете использовать названия «Танк Т-34» или «Танк "Тигр"». Возможно, сюжет вашей игры разворачивается в будущем и подразделения будут называться «Плазменный танк» или «Лазерный танк». Выбор названий может показаться легкой задачей, но я советую отнестись к нему ответственно. Если вы не создаете реалистичную игру с использованием названий реально существующих боевых единиц, я рекомендую выбирать такие названия, которые будут объяснять, что данное подразделение делает. Если вы назовете ваш фантастический танк «Змей», это почти ничего не скажет игроку и не будет служить подсказкой во время игры. Если вы чувствуете, что такие имена необходимы, пользуйтесь ими в качестве префиксов (например, «Змей: Тяжелый лазерный танк»).
Хорошим примером из реального мира является лучник из игры WarcraftIII: Reign of Chaos. Лучник — это эльфийский стрелок, выпускающий во врагов стрелы из своего лука. Название интуитивно понятно и соответствует подразделению. Изображение данного подразделения приведено на рис. 8.1.
Рис. 8.1. Лучник из игры Warcraft III: Reign of Chaos
Название типа атаки
Переменная m_szName хранит название типа атаки в виде последовательности символов. Это поле действует аналогично полю с названием типа защиты.
Название защиты
Переменная m_szName хранит название защиты в виде строки символов. Я использую ее чтобы было проще узнать тип защиты подразделения без необходимости запоминать соответствующие числовые значения. Это поле добавлено лишь для удобства.
Обновление кадра анимации
Перед тем как я погружусь в работу функции обновления подразделений, давайте взглянем на следующий код:
// Обновление подразделений if(timeGetTime() > dwLastUpdateTime) { vUpdateUnits(); dwLastUpdateTime = timeGetTime() + 33; }
Данный код перед тем как вызвать функцию обновления данных подразделений снова, проверяет прошло ли 33 миллисекунды с момента ее последнего вызова. Это позволяет ограничить частоту обновления графики. Если вы не поместите в код подобный ограничитель, анимация будет некорректно воспроизводиться на системах, которые работают быстрее чем ваша. Конечно, у вас может быть наилучшая на сегодняшний день система, но что будет через пару лет? Это напоминает мне о родственнике, который в давние времена написал игру для IBM PC. В программе была собственная встроенная операционная система. Он поступил так чтобы уменьшить занимаемый объем памяти, поскольку программа содержала около двух миллионов строк ассемблерного кода! В графические вызовы он не поместил никаких задержек, из за того, что подошел к самому пределу возможностей оборудования того времени. Недавно я посетил его, он стряхнул пыль со старой пятидюймовой дискеты, вставил ее в дисковод и загрузил ту самую программу. Верите или нет, но программа, которой исполнилось более десяти лет, без проблем загрузилась и запустилась в демонстрационном режиме. Мы попытались сыграть и игру, но графические и синхронизирующие функции выполнялись настолько быстро, что на экране мы увидели мешанину из различных изображений. Это выглядело забавно, но в то же время нам стало грустно из-за того, что мы не смогли насладиться игрой. Мораль этой длинной истории такова: всегда помещайте в ваши игры таймеры. (Если вам интересно, игра называлась Chain Reaction.)
Смыслом жизни функции vUpdateUnits() является определение для каждого активного подразделения того, какой кадр анимации должен выводиться следующим. Для этого требуется, чтобы функция в цикле перебирала все активные подразделения, определяла какая анимационная последовательность обновляется, и затем обновляла ее. Есть пять основных действий, которые следует учесть при обновлении данных подразделения:
Ожидание
Поворот
Атака
Гибель
Перемещение
Обработка атакующих подразделений
Третий тип анимации относится к атакующим подразделениям. Код работает точно так же, как и код для обработки ожидающих подразделений— в нем кадр с изображением атакующего подразделения последовательно меняется, пока не будет достигнут конец анимационной последовательности, после чего воспроизведение начинается сначала. А вот и сам код:
ptrUnit->m_iCurAttackFrame++; if(ptrUnit->m_iCurAttackFrame >= ptrUnit->m_Animation->m_iNumAttackFrames) { ptrUnit->m_iCurAttackFrame = 0; } ptrUnit->m_iCurAnimFrame = ptrUnit->m_Animation->m_iStartAttackFrames + (ptrUnit->m_iCurAttackFrame * (UNITMANAGER_MAXOWNERS + 1));
Обработка гибнущих подразделений
Четвертый тип анимации имеет дело с гибнущими подразделениями. Код работает точно так же, как и код для обработки атакующих подразделений — кадры последовательно меняются, пока не будет достигнут конец анимационной последовательности, после чего воспроизведение начинается сначала. Вот как выглядит этот фрагмент кода:
ptrUnit->m_iCurDieFrame++; if(ptrUnit->m_iCurDieFrame >= ptrUnit->m_Animation->m_iNumDieFrames) { ptrUnit->m_iCurDieFrame = 0; } ptrUnit->m_iCurAnimFrame = ptrUnit->m_Animation->m_iStartDieFrames + (ptrUnit->m_iCurDieFrame * (UNITMANAGER_MAXOWNERS + 1));
Обычно эта анимационная последовательность воспроизводится когда подразделение взрывается в блеске славы.
Обработка ожидающих подразделений
Первое действие представляет собой состояние «ничегонеделанья» или ожидания. Код для его обработки выглядит следующим образом:
ptrUnit->m_iCurStillFrame++; if(ptrUnit->m_iCurStillFrame >= ptrUnit->m_Animation->m_iNumStillFrames) { ptrUnit->m_iCurStillFrame = 0; } ptrUnit->m_iCurAnimFrame = ptrUnit->m_Animation->m_iStartStillFrames + (ptrUnit->m_iCurStillFrame * (UNITMANAGER_MAXOWNERS + 1));
Сперва в коде увеличивается номер текущего кадра ожидания. Это продвигает анимационную последовательность ожидания.
Следом идет проверка, которая должна гарантировать что полученный номер кадра не превышает количество доступных кадров в анимационной последовательности ожидания. Вы же наверняка не хотите, чтобы изображение подразделения, на которое ссылается указатель на кадр анимации оказалось отсутствующим!
И наконец я устанавливаю текущий кадр анимации для чего беру номер начального кадра анимации ожидания и прибавляю к нему текущее значение счетчика кадров ожидания, умноженное на количество поддерживаемых цветов владельца плюс единица. Гмм! Поскольку все данные анимационной графики подразделений хранятся в одном большом массиве, эти вычисления необходимы, чтобы получить требуемый кадр последовательности.
Обработка перемещающихся подразделений
Последний тип анимации относится к перемещающимся подразделениям. Код, который я поместил в пример выглядит очень похоже на обработку анимации других действий, но в нем отсутствует код для передвижения подразделения по экрану. Я удалил его чтобы максимально упростить пример, но абсолютно ничего не препятствует вам перемещать подразделения. Вот как выглядит код:
ptrUnit->m_iCurMoveFrame++; if(ptrUnit->m_iCurMoveFrame >= ptrUnit->m_Animation->m_iNumMoveFrames) { ptrUnit->m_iCurMoveFrame = 0; } ptrUnit->m_iCurAnimFrame = ptrUnit->m_Animation->m_iStartMoveFrames + (ptrUnit->m_iCurMoveFrame * (UNITMANAGER_MAXOWNERS + 1));
Если вы хотите добавить код для передвижения, попытайтесь увеличивать координату Y подразделения, пока изображение не выйдет за границу экрана, а затем повторить цикл с низу экрана. Мне кажется, вы разочарованы? Вот код с добавленной логикой для передвижения:
// Передвижение подразделения ptrUnit->m_fYPos += ptrUnit->m_Movement->m_fMovementSpeed; // Если вышли за верхнюю границу экрана, начинаем заново снизу if(ptrUnit->m_fYPos > 360.0f) ptrUnit->m_fYPos = -360.0f;
Приведенный выше код увеличивает координату Y подразделения, пока оно не скроется из виду, а затем устанавливает координаты подразделения, чтобы оно появилось в нижней части экрана. Это очень простой вариант перемещения снизу вверх, но он открывает перед вами много возможностей.
netlib.narod.ru | < Назад | Оглавление | Далее > |
Обработка поворачивающих подразделений
Второе действие представляет собой разворот подразделения. Вот предназначенный для этого код:
// Поворот ptrUnit->m_fRot += ptrUnit->m_Movement->m_fTurnSpeed; // Сброс, если завершен полный разворот if(ptrUnit->m_fRot > 360.0f) ptrUnit->m_fRot -= 360.0f;
В первой строке угол поворота подразделения увеличивается на величину, заданную в переменной, определяющей скорость поворота подрзделения. В результате изображение подразделения вращается в соответствии с заданными параметрами. Самое лучшее здесь то, что вы можете увеличивать или уменьшать частоту вращения просто регулируя скорость поворота.
Следующая строка кода проверяет не превысил ли угол поворота величину 360 градусов. Если да, то из величины угла поворота вычитается 360 градусов. Благодаря этому значение угла никогда не станет слишком большим.
Поскольку вращение происходит без изменения текстуры, текущий кадр анимации в этом коде не устанавливается.
Очки повреждений
Далее на рис.8.8 изображена переменная m_iHitPoints. Я использую ее для хранения общего количества очков повреждений, которое данное подразделение может получить во время битвы. Когда количество очков повреждений становится равным нулю, подразделение погибает. Для очков повреждений, как и для коэффициентов защиты, я использую целочисленные значения из диапазона от 0 до 1000.
Отображение активных подразделений
Осталось только выполнить цикл, который будет перебирать все подразделения и отображать те из них, которые в данный момент активны. Выполняющий эту задачу фрагмент кода приведен ниже:
// Цикл перебирающий подразделения for(int i = 0; i < m_UnitManager.m_iTotalUnitObjs; i++) { // Устанавливаем указатель на подразделение ptrUnit = &m_UnitManager.m_UnitObjs[i]; // Проверяем, активно ли подразделение if(ptrUnit->m_bActive) { // Рисуем подразделение vDrawUnit( ptrUnit->m_fXPos, ptrUnit->m_fYPos, ptrUnit->m_fScale * 128.0f, ptrUnit->m_fScale * 128.0f, ptrUnit->m_fRot, ptrUnit->m_Animation, ptrUnit->m_iCurAnimFrame, ptrUnit->m_iOwner ); } }
В приведенном выше коде я в цикле перебираю все подразделения, созданные в диспетчере подразделений. Если подразделение активно я вызываю функцию рисования и передаю ей параметры подразделения. Местоположение подразделения определяет в каком месте экрана оно будет находиться. Параметр, задающий поворот определяет ориентацию подразделения. Указатель анимации сообщает функции визуализации подразделения откуда ей брать данные текстуры. Номер текущего кадра анимации сообщает функции визуализации подразделения, какую именно текстуру ей следует выводить. Код владельца определяет, какая именно текстура с цветами владельца будет наложена поверх изображения подразделения. Хм-м-м... Мне кажется, я что-то забыл. Ах, да! Как вычисляется текущий кадр анимации? С помощью функции vUpdateUnits(). Взгляните на рис.8.31, чтобы увидеть ход выполнения функции визуализации до данного момента.
Рис. 8.31. Ход выполнения функции визуализации
На рис. 8.31 видно, что функция визуализации вызывает перед началом визуализации функцию обновления данных подразделений. Это важный шаг, так как кадры анимации подразделений должны быть обновлены до того, как начнется их визуализация. Технически вы можете выполнять обновление и позже, но главное, чтобы оно обязательно где-нибудь выполнялось!
Передвижение по воде
Морские подразделения могут перемещаться только по воде. В зависимости от игры это может быть ограничивающим фактором. Лично я всегда наслаждаюсь хорошей морской битвой. Есть два основных типа морских подразделений: надводные и подводные. Если вы хотите запрограммировать сражения подлодок с обычными судами, вам необходимо добавить оба этих типа подразделений. Чтобы повысить уровень реализма игры, вам следует также определить максимальную глубину, которой могут достигнуть ваши подводные лодки.
В игре WarcraftIII нет морских подразделений, так что данный тип перемещения не будет проиллюстрирован примером.
Передвижение по воздуху
Если вы играете в стратегическую игру на стороне Соединеных Штатов, скорее всего у вас будут мощные военно-воздушные силы. Передвижение по воздуху применимо ко всему, что может летать. Будь это вертолет, биплан или дракон — все они воздушные подразделения. У таких подразделений есть ряд уникальных параметров. Подумайте, например, о максимальной высоте полета. У вас может быть несколько подразделений неспособных достичь определенных высот. Это сделает игру более сложной и может привести к более увлекательному игровому процессу.
Одним из моих любимых воздушных подразделений в игре Warcraft III является химера. Это двухголовые летучие бестии, которых разводят ночные эльфы. Больше всего мне нравится способность химеры быстро разрушать вражеские постройки. Ее изображение приведено на рис. 8.3.
Рис. 8.3. Химера в игре Warcraft III: Reign of Chaos
Передвижение по земле
Любая боевая единица у которой есть колеса, гусеницы или ноги обычно классифицируется как наземная. Наиболее часто встречающимися наземными подразделениями являются пехота, танки и бронетранспортеры. При желании вы можете использовать и другие способы передвижения по земной поверхности. Возьмите, например, транспорт на воздушной подушке или подземные бурильные установки. Вы можете произвольным образом разнообразить способы передвижения, внося изменения в базовые типы.
Если вернуться к теме Warcraft, хорошим примером наземного подразделения является катапульта. Катапульта изготавливается орками и хорошо подходит для разрушения вражеских построек. Изображение катапульты приведено на рис.8.2.
Рис. 8.2. Катапульта в игре Warcraft III: Reign of Chaos
Полеты в космосе
Космические подразделения могут перемещаться во внешнем пространстве. Я не видел игры в которой бы космические войска использовались вместе с подразделениями других перечисленных типов, но не говорю, что она не может существовать. Фактически, я думаю было бы очень интересно объединить подразделения всех четырех типов в одной игре.
Warcraft III это игра в жанре фэнтези, так что в ней нет никаких космических подразделений.
Итак, как можно реализовать способы передвижения в коде? Проще всего воспользоваться перечислением. Приведенный ниже фрагмент кода демонстрирует данный подход:
enum UNIT_ATTR_MOVETYPE { MOVETYPE_LAND, MOVETYPE_SEA, MOVETYPE_AIR, MOVETYPE_SPACE };
В этом фрагменте кода я определил четыре основных способа передвижения: по земле, по воде, по воздуху и в космосе. Теперь мне достаточно только присвоить определяющей способ передвижения переменной в классе подразделения, значение соответствующее желаемому способу передвижения. Реализацию класса я опишу позже в этой главе.
Если вы хотите использовать более детализированный набор способов передвижения, используйте код, похожий на приведенный ниже:
enum UNIT_ATTR_MOVETYPE_ADV { MOVETYPE_LAND_WHEELED, MOVETYPE_LAND_TRACKED, MOVETYPE_LAND_HOVER, MOVETYPE_LAND_FOOT, MOVETYPE_SEA_SURFACE, MOVETYPE_SEA_SUBMERGED, MOVETYPE_AIR_LOW, MOVETYPE_AIR_HIGH, MOVETYPE_SPACE_INNER, MOVETYPE_SPACE_OUTER };
Проектирование подразделений
Если вам придется выбрать общие признаки для описания подразделений, какими они будут? Я думаю, что приведенный ниже список может служить хорошей отправной точкой:
Название.
Способ передвижения.
Скорость передвижения.
Тип атаки.
Тип защиты.
Программирование шаблона
Приготовьтесь к беспощадной драке, поскольку пришло время спуститься с небес на землю и заняться шаблонами подразделений. Данный раздел книги более сложен, чем остальные, так что будьте внимательны, чтобы извлечь максимум пользы из предоставленной информации. Не прерывайтесь, чтобы поиграть в Combat Mission!
Прежде чем погрузиться в глубины кода, взгляните на рис.8.7, где изображен результат работы программы, о которой я собираюсь рассказать.
Рис. 8.7. Окно программы D3DFrame_UnitTemplate
На рис. 8.7 показано окно программы D3DFrame_UnitTemplate, входящей в сопроводительные файлы к книге. На рисунке видны четыре вертолета летящих над травяным полем. В верхнем левом углу окна выводится отладочная информация. Возможно, все это выглядит не слишком впечатляюще, но лежащая в основе программы система управления подразделениями весьма сложна.
Загрузите с компакт-диска проект с именем D3DFrame_UnitTemplate и следуйте за мной дальше. Чтобы создать полнофункциональный шаблон подразделения, вам потребуются следующие классы:
Класс типа защиты
Класс типа атаки
Класс типа передвижения
Класс анимации подразделения
Класс текстур подразделения
Класс подразделения
Класс диспетчера подразделений
Радиус взрыва
Переменная m_iSplashRadius сообщает, какое количество повреждений может нанести разрыв снаряда, выпущенного из данного типа оружия. Это полезно для таких типов вооружения, как гранаты, катапульты ит.п. Радиус указывает количество блоков игрового поля, на которые распространяется действие взрыва. Взгляните на рис. 8.10.
Рис. 8.10. Радиус взрыва
На рис. 8.10 изображены три танка. Нижний танк стреляет из своего главного орудия по одному из двух верхних вражеских танков. Радиус взрыва танкового снаряда равен 2, а значит его сфера повреждений распространяется от точки взрыва на два блока игрового поля в каждом из направлений. Поскольку радиус взрыва достаточно велик, второй единице вражеской техники также наносятся повреждения. На иллюстрации в области взрыва есть темная область, где количество наносимых повреждений максимально, и более светлые области, где количество наносимых повреждений уменьшается. Это полезно, если вы хотите сделать модель взрыва более реалистичной, чтобы количество наносимых повреждений уменьшалось с удалением от центра взрыва.
Рисование подразделений
Все эти классы великолепны, но как насчет визуализации? Если вы откроете файл main.cpp из проекта D3DFrame_UnitTemplate, я покажу вам! Спускайтесь вниз до функции vInitTileVB().
Вы, возможно, еще помните пример визуализации блоков, который я демонстрировал в главе5. Там я создавал отдельный буфер вершин для хранения геометрии текстуры. Базовая точка геометрии находилась в левом нижнем углу квадрата. Это упрощает выравнивание квадратов при визуализации блоков, но не слишком подходит для поворотов. Взгляните на рис. 8.28, чтобы понять что я имею в виду.
Рис. 8.28. Два квадрата с различными базовыми точками
На рисунке изображены два квадрата. У левого квадрата базовая точка находится в левом нижнем углу. У правого квадрата базовая точка расположена в центре. Вращение левого квадрата бдет сопровождаться его перемещением, в то время как правый квадрат при вращении остается на месте.
Поскольку базовая точка левого квадрата не совпадает с центром, его вращение не подходит для текстур подразделений. Возникающий эффект показан на рис. 8.29.
Рис. 8.29. Два текстурированных квадрата с различными базовыми точками
На рис. 8.29 показаны те же два квадрата, что и ранее, но на них нанесена текстура с изображением танка. Танк слева поворачивается очень странно, поскольку его базовая точка расположена неверно. Танк справа поворачивается правильно, потому что его базовая точка расположена в центре квадрата.
Итак, какое отношение это имеет к моему примеру? Основной момент заключается в том, что вам необходимо иметь два буффера для геометрии: один для блоков игрового поля и один для подразделений. Чтобы создать квадрат, базовая точка которого находится в центре, вы должны создать вершины вокруг центра. Вот как выглядит код для этого:
// Создание вершин pVertices[0].position = D3DXVECTOR3(-0.5f, -0.5f, 0.0f); pVertices[0].tu = 0.0f; pVertices[0].tv = 1.0f; pVertices[0].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[1].position = D3DXVECTOR3(-0.5f, 0.5f, 0.0f); pVertices[1].tu = 0.0f; pVertices[1].tv = 0.0f; pVertices[1].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[2].position = D3DXVECTOR3(0.5f, -0.5f, 0.0f); pVertices[2].tu = 1.0f; pVertices[2].tv = 1.0f; pVertices[2].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[3].position = D3DXVECTOR3(0.5f, 0.5f, 0.0f); pVertices[3].tu = 1.0f; pVertices[3].tv = 0.0f; pVertices[3].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f);
Код создает четыре вершины — по одной для каждого из углов квадрата. Их расположение показано на рис. 8.30.
Рис. 8.30. Координаты вершин квадрата с базовой точкой в центре
На рис. 8.30 вы видите квадрат с базовой точкой, находящейся в центре. Так же там показано расположение осей X, Y и Z относительно вершин квадрата. Точки снизу и слева находятся в отрицательном пространстве, а точки сверху и справа — в положительном.
Скорость передвижения
Скорость передвижения подразделений очень важна для выбора игроком стратегии использования войск в игре. Обычно подразделения, оборудованные тяжелыми вооружениями, передвигаются медленнее, чем легковооруженные. Это имеет смысл, поскольку платой за более мощное вооружение является увеличение веса. Хотя бывают и исключения. Возьмите для примера 105-мм пушку и винтовку M-16. Пушка весит значительно больше, чем винтовка, но танк, оборудованный 105-мм пушкой передвигается быстрее, чем пехотинец с винтовкой M-16.
Поскольку в наши дни для визуализации подразделений все чаще используется трехмерная графика, для указания скорости перемещения лучше всего использовать единицы используемой трехмерной системы координат. Для примера установим, что размер блока игрового поля равен единице. В этом случае медленно движущиеся подразделения будут перемещаться за такт игры на 1/10 размера блока, а быстро перемещающиеся подразделения могут за то же время преодолевать 8/10 блока. Эта концепция проиллюстрирована на рис.8.4.
Рис. 8.4. Скороость передвижения
На рис. 8.4 изображены танк и космический корабль. Игровое поле представлено четырьмя блоками, длина каждого из которых равна единице. Космический корабль за каждый такт игры перемещается на 0,8 длины блока, а танк за то же время перемещается лишь на 0,25 длины блока. По мере увеличения количества прошедших тактов, космический корабль будет все больше обгонять танк. На рисунке показано, что космический корабль переместился на четыре блока игрового поля за то время, которое потребовалось танку, чтобы преодолеть два блока. При использовании данного метода определения скорости перемещения очень легко вычислить в каком месте будет находиться подразделение в любой указанный момент времени. Кроме того, вам дополнительно облегчит жизнь использование блоков одинакового размера.
СОВЕТ
На рис. 8.11 присутствуют несколько переменных, контроллирующих передвижение боевых единиц. Первая из них, скорость передвижения, указывает на сколько блоков игрового поля может переместиться данное подразделение за один раунд игры. Я здесь использую значение с плавающей запятой, поскольку, вероятно, вы не захотите, чтобы за каждый раунд подразделение перемещалось на целое число блоков.
Скорость поворота
Последний уникальный элемент данных класса передвижения сообщает вам насколько быстро подразделение поворачивает. Это число с плавающей точкой, указывающее на сколько градусов может развернуться подразделение за один раунд. Подразделению, скорость поворота которого равна 10.0 потребуется 36 раундов, чтобы сделать полный круг. Если скорость поворота равна 30, подразделению на полный круг потребуется лишь двенадцать раундов. Преимушества более быстрого разворота показаны на рис. 8.12.
Рис. 8.12. Два подразделения с разной скоростью поворота
На рис. 8.12 скорость поворота левого танка равна 45. Скорость поворота правого подразделения равна 22.5. За два раунда левый танк повернется вправо. И у него останется еще два раунда, прежде чем правый танк сможет повернуться к нему. Если эти два танка сражаются, левый танк сможет несколько раз выстрелить, прежде чем правый развернет свою пушку в его направлении! Вот почему скорость поворота так важна в сражениях.
Некоторые игры не беспокоятся о скорости поворота. Они просто считают, что подразделение может сразу передвигаться в заданном направлении не тратя времени на поворот. Это помогает сохранить быстрый темп игры, но отнимает у нее значительную долю реализма.
Скорость снаряда
Переменная m_fProjectileSpeed задает с какой скоростью снаряд покидает ствол оружия. Это значение применимо только для снарядов и ракет, поскольку в рукопашной схватке снаряды отсутствуют, а лазерный луч распространяется со скоростью света.
Скорость снаряда указывает сколько блоков игрового поля может преодолеть снаряд за раунд игры. По этой причине диапазон допустимых значений будет от 0.0 до 0.99. Если вы не хотите, чтобы снаряд пересекал несколько блоков игрового поля за один раунд, максимальным значением должно быть именно 0.99.
Скорость восстановления
Раньше я не упоминал о восстановлении здоровья подразделений, так что эта идея может показаться вам новой. В классе типа защиты есть переменная с именем m_iRegenRate, позволяющая создавать подразделения, которые могут сами устранять причиненные им повреждения. Возможно, это подразделение снабжено аптечками, или это мифический зверь, способный заращивать раны. Так или иначе, это значение позволяет добавить к вашей игре самовосстанавливающиеся боевые единицы.
Ключевым моментом является настройка соответствия между диапазоном значений очков повреждений и скоростью восстановления. Поскольку количество очков повреждений у подразделения будет увеличиваться один раз за раунд игры на значение, равное скорости восстановления, последнюю величину надо выбрать сравнительно небольшой. Я рекомендую принять диапазон допустимых значений от 0 до 100. Если скорость восстановления равна 100, подразделение восстановит свои параметры от предсмертного состояния до полного здоровья за десять раундов. Если же значение равно 1, то и через 100 раундов подразделение будет в двух шагах от гибели.
Скорострельность
Переменная m_iRateOfFire сообщает вам, сколько раундов игры должно пройти, прежде чем оружие сможет снова выстрелить. Быстродействующее оружие, такое как автомат, может стрелять залпами в каждом раунде игры. Более медленное оружие, например, катапульты, будут стрелять один раз в пять раундов, или что-то подобное. Конечно, автомат может выпускать сразу несколько пуль, и именно поэтому я использовал термин «стрелять залпами».
Не существует однозначного ответа на вопрос сколько раундов игры должно пройти , пока оружие сново может выстрелить. Чтобы получить сбалансированный тип атаки, вам придется поиграть с разными значениями.
Создание подразделений
Теперь, после того как базовая информация о подразделених загружена, вы можете создавать подразделения, которые будут использоваться в игре. Вы не можете модифицировать базовые типы, так что следует создавать новые объекты подразделений. Здесь в игру вступает член данных диспетчера подразделений с именем m_UnitObjs. Данный массив хранит модифицируемые объекты подразделений, использующиеся в игре. Для управления этими объектами применяются две функции: iAddUnit() и vRemoveUnit().
Способ передвижения
Как перемещается подразделение? Летает ли оно, плавает, перемещается в космосе или может использовать все эти три способа? Это очень важный вопрос ответ на который влияет на стратегию использования данного подразделения в ходе игры. Взгляните на список наиболее распространенных способов передвижения:
По земле
По воздуху
По воде
В космосе
Данное поле сообщает вам какой именно способ использует подразделение для своего передвижения. Летает оно, плавает или ползает? Может быть оно ходит? Может быть оно катится? Переменная, задающая способ перемещения отвечает на этот вопрос.
Тип атаки
Вы должны не только учесть объем наносимого боевой единицей ущерба, но также и подумать о том, каким именно способом этот ущерб будет наноситься. Будет ли подразделение выпускать во врага ракеты, или оно будет стрелять пулями, воспользуется лазером, или каким-либо другим оружием?
Есть несколько различных методов назначения боевым подразделениям типов атаки. Можно либо назначать параметры каждому подразделению индивидуально, либо создать несколько глобальных наборов параметров, и назначать их различным подразделениям. Я предпочитаю использовать глобальные параметры, поскольку это упрощает назначение одного и того же типа атаки нескольким различным подразделениям. Многие игры на рынке также следуют этим путем, поскольку он более эффективен. Взгляните, например, на следующую таблицу:
Название подразделения | Легкий танк | ||
Наносимый ущерб | 100 | ||
Скорострельность | 5 | ||
Косвенный ущерб от оружия | 0 | ||
Изображение атаки | laser.bmp | ||
Мощность брони | 100 | ||
Скорость передвижения | 50 |
В данном примере легкий танк может причинить вражескому подразделению до 100 единиц ущерба, стреляет пять раз в минуту, для изображения оружия используется файл laser.bmp, оборудован броней мощностью 100 единиц и за один ход может переместиться на 50 единиц. Что, если теперь вы захотите создать средний танк, оборудованный тем же вооружением, но имеющий броню мощьностью 150 единиц и способный передвинуться за один ход только на 40 единиц? Конечно, вы можете заново задать все значения параметров атаки. В результате ряд параметров будет дублироваться и вы получите настоящую головную боль, если решите внести глобальные изменения в параметры вооружений.
Гораздо лучше сгруппировать параметры нападения в типы атаки. Взгляните на следующий пример:
Тип атаки | лазер | ||
Наносимый ущерб | 100 | ||
Скорострельность | 5 | ||
Косвенный ущерб от оружия | 0 | ||
Изображение атаки | laser.bmp | ||
Название подразделения | легкий танк | ||
Тип атаки | лазер | ||
Мощность брони | 100 | ||
Скорость передвижения | 50 | ||
Название подразделения | средний танк | ||
Тип атаки | лазер | ||
Мощность брони | 150 | ||
Скорость передвижения | 40 |
В данном примере я создаю тип атаки с названием «лазер». Затем я создаю два подразделения, которые используют один и тот же тип атаки, но отличаются параметрами защиты и скоростью передвижения. Эта ситуация показана на рис. 8.5.
Рис. 8.5. Два типа подразделений используют один тип атаки
На рис. 8.5 оказаны легкий и средний танк, которые используют один и тот же тип атаки. Если вы используете данный метод, вам достаточн изменить тип атаки, и эти изменения будут действовать для всех использующих данный тип подразделений.
Переменная m_iType хранит число, соответствующее данному типу атаки. Это работает точно так же, как и для типов защиты.
Тип защиты
Каждому преступлению соответствует наказание. Так что для каждого типа атаки должен быть тип защиты, верно? Ладно, не всегда, но это утверждение можно принять в качестве отправной точки. Так или иначе, типы защиты работают точно так же как и типы атаки. Возьмем для примера рассматривавшиеся выше легкий и средний танки. Вместо того, чтобы в параметрах каждого из них задавать параметры брони, можно создать два типа защиты. Чтобы увидеть, как это действует, взгляните на рис. 8.6.
Рис. 8.6. Два подразделения с различными типами защиты
На рис. 8.6 видно, что привычные персонажи совместно используют один и тот же тип атаки. Отличие в том, что они используют два различных типа защиты. Вы можете подумать, что это приведет к лишенму расходу памяти, но давайте посмотрим что произойдет при добавлении третьего типа подразделений. Как насчет бронетранспортера? Он будет использовать ту же самую легкую броню, что и легкий танк. Он не будет вооружен лазером, и для него надо будет создать новый тип атаки, но по крайней мере, новое подразделение предоставит пример многократного использования одного типа защиты.
СОВЕТ
В переменной 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).
netlib.narod.ru | < Назад | Оглавление | Далее > |
Ускорение и торможение
Чтобы добавить сложности, я включил параметры, задающие ускорение и торможение. Ускорение определяет на сколько возрастает скорость подразделения за раунд игры, когда оно разгоняется. Торможение сообщает вам на сколько уменьшается подразделения за раунд игры, когда оно тормозит. Этот параметр позволяет увеличить реализм сражений. Одни подразделения могут и должны быть медленнее (или быстрее), чем другие. Рассмотрим следующий пример:
Легкая конница: Ускорение = 0.3, Торможение = 0.5, Скорость = 0.5
Катапульта: Ускорение = 0.1, Торможение = 0.2, Скорость = 0.3
В данном примере легкая конница может разогнаться до максимальной скорости за два раунда. Для полной остановки этому подразделению потребуется еще меньше времени— один раунд. Катапульты двигаются медленнее. Чтобы разогнаться до полной скорости им потребуется три раунда, а чтобы остановиться — два. Это действительно имеет смысл, ведь катапульта не может двигаться так же быстро как лошадь. Вы можете не использовать параметры ускорения и торможения в ваших играх, если они показались вам слишком сложными, но помните, что они добавляют вашей игре значительную толику реализма.
Загрузка базовых типов
У вас есть базовые классы для хранения данных подразделения, но как загрузить в них информацию? Один из способов— жестко задать все значения параметров подразделений в коде программы. Подобное сляпанное наспех решение не позволит создать гибкую систему. Я предпочитаю использовать конфигурационные файлы, которые загружаются во время работы программы. Вы можете редактировать конфигурационные файлы и снова запускать игру без повторной компиляции. Это неоценимое преимущество, поскольку вы наверняка будете менять параметры вашей игры во время разработки. Это также позволяет легко создавать расширения для игры, поскольку для создания новых типов подразделений достаточно изменить значения нескольких параметров в конфигурационных файлах.
В классе есть функция 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.
Рис. 8.22. Базовые типы в классе диспетчера подразделений
На рис. 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. Если вы помните, ранее говорилось, что это необходимо для загрузки текстур.
Чтобы загрузить подготовленную информацию о подразделениях я вызываю функцию загрузки базовых типов диспетчера. Она получает имена файлов с подготовленными данными и загружает их в базовые типы, которыми управляет объект диспетчера подразделений.
Пришло время для веселья! Следующая часть кода создает подразделения с помощью функции добавления подразделений. Диспетчер создает и активирует запрошенные подразделения, чтобы они появились в игровом цикле. Сразу после создания каждого подразделения я инициализирую данные о его местоположении, чтобы подразделение появилось в требуемом месте экрана. Обратите внимание, создавая подразделения я назначаю каждому из них собственный цвет владельца. Это позволит вам увидеть как различные цвета владельца отображаются во время визуализации.
После того как подразделения созданы в диспетчере и активированы, они могут модифицироваться и отображаться согласно вашим пожеланиям. В качестве упражнения попробуйте создать на экране еще несколько сотен подразделений и посмотрите, что получится!
netlib.narod.ru | < Назад | Оглавление | Далее > |