netlib.narod.ru | < Назад | Оглавление | Далее > |
Приготовьтесь к беспощадной драке, поскольку пришло время спуститься с небес на землю и заняться шаблонами подразделений. Данный раздел книги более сложен, чем остальные, так что будьте внимательны, чтобы извлечь максимум пользы из предоставленной информации. Не прерывайтесь, чтобы поиграть в Combat Mission!
Прежде чем погрузиться в глубины кода, взгляните на рис. 8.7, где изображен результат работы программы, о которой я собираюсь рассказать.
Рис. 8.7. Окно программы D3DFrame_UnitTemplate
На рис. 8.7 показано окно программы D3DFrame_UnitTemplate, входящей в сопроводительные файлы к книге. На рисунке видны четыре вертолета летящих над травяным полем. В верхнем левом углу окна выводится отладочная информация. Возможно, все это выглядит не слишком впечатляюще, но лежащая в основе программы система управления подразделениями весьма сложна.
Загрузите с компакт-диска проект с именем D3DFrame_UnitTemplate и следуйте за мной дальше. Чтобы создать полнофункциональный шаблон подразделения, вам потребуются следующие классы:
Помните, как типы защиты помогают структурировать данные подразделений? Теперь вы добрались до практического примера, показывающего как реализовать эту концепцию в виде класса. Откройте заголовочный файл 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); };
Класс очень простой и содержит только открытые члены данных, конструктор, деструктор и единственный метод. Главным образом я использую классы как структуры данных, так что не ожидайте наличия сотен методов. Так же помните, что данный пример значительно упрощен, чтобы сделать его более легким для понимания. В реальном приложении вы можете сделать члены данных закрытыми и добавить методы для доступа к ним. Так или иначе, продолжаем разговор. Взгляните на рис 8.8, где показана структура переменных класса способа защиты.
Рис. 8.8. Структура переменных класса CUnitDefense
Как видно из рис. 8.8, я предусмотрел четыре коэфиициента защиты: для ракет, для пуль, для лазера и для рукопашной схватки. Это придает игре достаточную гибкость для поддержки четырех различных типов атаки. Возьмем, к примеру, бронежилет. Он достаточно хорош в качестве защиты от пуль, но недолго устоит против лазера. Поэтому для учета данной особенности при инициализации вы можете задать для бронежилета средний уровень защиты от пуль и низкий уровень защиты от лазера.
Вы всегда можете сократить или увеличить используемое в данном примере количество коэффициентов защиты. Мой пример приспособлен для футуристической военной игры, но вы, возможно будете реализовывать другой сценарий. Если вы решили создать игру в жанре фэнтези, вам потребуется заменить коэффициент защиты от лазера коэффициентом защиты от магии. Защита от ракет превратится в защиту от стрел, а вот защиту от пуль может придется оставить, по крайней мере, если у вас в игре будут пищали или другое огнестрельное оружие. Впрочем, возможно, вы решите заменить коэффициент защиты от пуль на коэффициент защиты от пороховых бомб.
Каждому коэффициенту защиты я присваиваю целочисленное значение. При этом я выбрал допустимый диапазон значений от 0 до 1000. Если коэффициент защиты равен 0, подразделение совершенно беззащитно перед данным типом атаки. Значение 1000 означает, что для данного способа атаки подразделение практически неуязвимо.
Далее на рис. 8.8 изображена переменная m_iHitPoints. Я использую ее для хранения общего количества очков повреждений, которое данное подразделение может получить во время битвы. Когда количество очков повреждений становится равным нулю, подразделение погибает. Для очков повреждений, как и для коэффициентов защиты, я использую целочисленные значения из диапазона от 0 до 1000.
Раньше я не упоминал о восстановлении здоровья подразделений, так что эта идея может показаться вам новой. В классе типа защиты есть переменная с именем m_iRegenRate, позволяющая создавать подразделения, которые могут сами устранять причиненные им повреждения. Возможно, это подразделение снабжено аптечками, или это мифический зверь, способный заращивать раны. Так или иначе, это значение позволяет добавить к вашей игре самовосстанавливающиеся боевые единицы.
Ключевым моментом является настройка соответствия между диапазоном значений очков повреждений и скоростью восстановления. Поскольку количество очков повреждений у подразделения будет увеличиваться один раз за раунд игры на значение, равное скорости восстановления, последнюю величину надо выбрать сравнительно небольшой. Я рекомендую принять диапазон допустимых значений от 0 до 100. Если скорость восстановления равна 100, подразделение восстановит свои параметры от предсмертного состояния до полного здоровья за десять раундов. Если же значение равно 1, то и через 100 раундов подразделение будет в двух шагах от гибели.
В переменной m_iType хранится число, определяющее тип защиты. Например, ноль может соответствовать броне легкого танка, а единица — броне среднего танка. Диапазон значений зависит от того, как много различных типов защиты будет в вашей игре. Общее их количество редко превышает несколько десятков, но никогда не знаешь точно. Чтобы увидеть пример двух подразделений, использующих два различных типа защиты, вернитесь назад к рис. 8.6.
Переменная m_szName хранит название защиты в виде строки символов. Я использую ее чтобы было проще узнать тип защиты подразделения без необходимости запоминать соответствующие числовые значения. Это поле добавлено лишь для удобства.
В классе есть только конструктор, деструктор и функция сброса значений переменных, так что в плане функциональности он весьма ограничен. Вот как выглядит код, реализующий перечисленные функции:
// Конструктор 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(). Деструктор не делает ничего полезного, а просто занимает место. Однажды он может понадобиться для каких-либо целей, но не сегодня.
Что может быть проще? Если же вам нравится сложный код, просто немного потерпите.
Подобно классу типов защиты, класс типов атаки помогает вам организовать данные о подразделениях. Я пользуюсь классом с именем 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.
Рис. 8.9. Структура переменных класса CUnitOffense
На рис 8.9 показаны четыре коэффициента поражения: для ракет, для пуль, для лазера и для рукопашной схватки. Точно также как и в классе обороны, эти значения относятся к тем типам атаки, которые упоминаются в их названии. Например, коэффициент поражения от пуль показывает, сколько повреждений наносит выпущенная из оружия пуля. Он может применяться для автоматической винтовки M-16, или для любого другого оружия, которое стреляет пулями. Я предпочитаю использовать для данного коэффициента тот же диапазон значений, что и для коэффициеттов защиты (в данном примере — от 0 до 1000). Это значительно упрощает вычисления, так как в этом случае для того, чтобы определеить полученные подразделением повреждения достаточно сравнить коэффициент защиты и коэффициент поражения. Взгляните на следующий пример:
Из этого примера видно, что бронежилет поглощает 50 единиц наносимого пулей ущерба, а пуля, выпущенная из M-16 наносит 60 единиц повреждений. В результате 10 единиц повреждений проходят сквозь защиту и портят здоровье тому, на ком одет бронежилет. В результате у данного подразделения вычитается 10 очков повреждений, после чего оно, будем надеяться, остается в живых. Вот другой пример:
Здесь видно, что против 105-мм гаубицы у бронежилета нет практически ни одного шанса. Подразделение получает 600 единиц повреждений и, скорее всего, будет уничтожено. И еще один, последний, пример:
В данном примере коэффициент поражения кольта 45 калибра недостаточен, чтобы причинить какие-либо повреждения подразделению. Это показывает, что способ атаки может оказаться бесполезным против используемой защиты. При желании вы можете добавить для таких случаев модификатор удачи, чтобы бронежилет не всегда обеспечивал стопроцентную защиту от выпущенных из пистолета пуль — решать вам.
Как и в случае с коэффициентами защиты, вы можете изменять приведенный список, чтобы он соответствовал вашим потребностям.
Переменная m_iSplashRadius сообщает, какое количество повреждений может нанести разрыв снаряда, выпущенного из данного типа оружия. Это полезно для таких типов вооружения, как гранаты, катапульты и т.п. Радиус указывает количество блоков игрового поля, на которые распространяется действие взрыва. Взгляните на рис. 8.10.
Рис. 8.10. Радиус взрыва
На рис. 8.10 изображены три танка. Нижний танк стреляет из своего главного орудия по одному из двух верхних вражеских танков. Радиус взрыва танкового снаряда равен 2, а значит его сфера повреждений распространяется от точки взрыва на два блока игрового поля в каждом из направлений. Поскольку радиус взрыва достаточно велик, второй единице вражеской техники также наносятся повреждения. На иллюстрации в области взрыва есть темная область, где количество наносимых повреждений максимально, и более светлые области, где количество наносимых повреждений уменьшается. Это полезно, если вы хотите сделать модель взрыва более реалистичной, чтобы количество наносимых повреждений уменьшалось с удалением от центра взрыва.
Переменная m_iRateOfFire сообщает вам, сколько раундов игры должно пройти, прежде чем оружие сможет снова выстрелить. Быстродействующее оружие, такое как автомат, может стрелять залпами в каждом раунде игры. Более медленное оружие, например, катапульты, будут стрелять один раз в пять раундов, или что-то подобное. Конечно, автомат может выпускать сразу несколько пуль, и именно поэтому я использовал термин «стрелять залпами».
Не существует однозначного ответа на вопрос сколько раундов игры должно пройти , пока оружие сново может выстрелить. Чтобы получить сбалансированный тип атаки, вам придется поиграть с разными значениями.
Переменная m_fProjectileSpeed задает с какой скоростью снаряд покидает ствол оружия. Это значение применимо только для снарядов и ракет, поскольку в рукопашной схватке снаряды отсутствуют, а лазерный луч распространяется со скоростью света.
Скорость снаряда указывает сколько блоков игрового поля может преодолеть снаряд за раунд игры. По этой причине диапазон допустимых значений будет от 0.0 до 0.99. Если вы не хотите, чтобы снаряд пересекал несколько блоков игрового поля за один раунд, максимальным значением должно быть именно 0.99.
Переменная m_iRange сообщает вам количество блоков игрового поля, на которое может выстрелить данный тип оружия. Это применимо только к вооружению, действующему на расстоянии, поскольку для ручного холодного оружия дальнобойность равна нулю.
Переменная m_iType хранит число, соответствующее данному типу атаки. Это работает точно так же, как и для типов защиты.
Переменная m_szName хранит название типа атаки в виде последовательности символов. Это поле действует аналогично полю с названием типа защиты.
В классе есть только констуктор, деструктор и функция установки начальных значений, действующая во многом так же как одноименная функция класса типов защиты. Вот как выглядит код реализации функций:
// Конструктор 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(), точно так же как это делалось в классе типов защиты. Вот и все, что можно сказать о работе класса типов атаки.
Класс способов передвижения также помогает организовать ваши боевые единицы. Для выполнения этой работы я использую класс 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); };
Класс содержит переменные, аналогичные тем, которые находятся в классе атаки, за исключением того, что их значения относятся к перемещению, а не атаке. Члены данных класса показаны на рис. 8.11.
Рис. 8.11. Переменные класса CUnitMovement
На рис. 8.11 присутствуют несколько переменных, контроллирующих передвижение боевых единиц. Первая из них, скорость передвижения, указывает на сколько блоков игрового поля может переместиться данное подразделение за один раунд игры. Я здесь использую значение с плавающей запятой, поскольку, вероятно, вы не захотите, чтобы за каждый раунд подразделение перемещалось на целое число блоков.
Данное поле сообщает вам какой именно способ использует подразделение для своего передвижения. Летает оно, плавает или ползает? Может быть оно ходит? Может быть оно катится? Переменная, задающая способ перемещения отвечает на этот вопрос.
Чтобы добавить сложности, я включил параметры, задающие ускорение и торможение. Ускорение определяет на сколько возрастает скорость подразделения за раунд игры, когда оно разгоняется. Торможение сообщает вам на сколько уменьшается подразделения за раунд игры, когда оно тормозит. Этот параметр позволяет увеличить реализм сражений. Одни подразделения могут и должны быть медленнее (или быстрее), чем другие. Рассмотрим следующий пример:
Легкая конница: Ускорение = 0.3, Торможение = 0.5, Скорость = 0.5
Катапульта: Ускорение = 0.1, Торможение = 0.2, Скорость = 0.3
В данном примере легкая конница может разогнаться до максимальной скорости за два раунда. Для полной остановки этому подразделению потребуется еще меньше времени — один раунд. Катапульты двигаются медленнее. Чтобы разогнаться до полной скорости им потребуется три раунда, а чтобы остановиться — два. Это действительно имеет смысл, ведь катапульта не может двигаться так же быстро как лошадь. Вы можете не использовать параметры ускорения и торможения в ваших играх, если они показались вам слишком сложными, но помните, что они добавляют вашей игре значительную толику реализма.
Последний уникальный элемент данных класса передвижения сообщает вам насколько быстро подразделение поворачивает. Это число с плавающей точкой, указывающее на сколько градусов может развернуться подразделение за один раунд. Подразделению, скорость поворота которого равна 10.0 потребуется 36 раундов, чтобы сделать полный круг. Если скорость поворота равна 30, подразделению на полный круг потребуется лишь двенадцать раундов. Преимушества более быстрого разворота показаны на рис. 8.12.
Рис. 8.12. Два подразделения с разной скоростью поворота
На рис. 8.12 скорость поворота левого танка равна 45. Скорость поворота правого подразделения равна 22.5. За два раунда левый танк повернется вправо. И у него останется еще два раунда, прежде чем правый танк сможет повернуться к нему. Если эти два танка сражаются, левый танк сможет несколько раз выстрелить, прежде чем правый развернет свою пушку в его направлении! Вот почему скорость поворота так важна в сражениях.
Некоторые игры не беспокоятся о скорости поворота. Они просто считают, что подразделение может сразу передвигаться в заданном направлении не тратя времени на поворот. Это помогает сохранить быстрый темп игры, но отнимает у нее значительную долю реализма.
В классе есть только констуктор, деструктор и функция установки начальных значений, работающая во многом так же как одноименная функция класса типов атаки. Вот как выглядит код реализации этих функций:
// Конструктор 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(), точно так же как это делалось в классе типов атаки. Я не пытаюсь нагрузить вас дубликатами одного и того же кода, просто сама природа классов делает их код очень похожим.
Также как и класс способов передвижения, класс анимации помогает организовать ваши подразделения. Я использую класс с именем 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 — это отдельный класс, который я создал в данном приложении для хранения графической инфоримации. Мы обсудим его в этой главе чуть позже.
В классе анимации есть несколько интересных членов данных. Первые из них хранят количество графическх кадров в различных анимационных последовательностях подразделения. Существует четыре типа анимации: ожидание, движение, атака и гибель.
Переменная m_iNumStillFrames сообщает сколько кадров используется в анимационной последовательности, изображающей боевую единицу в состоянии ожидания. Множество подразделений в состоянии ожидания ничего не делают, но поведение некоторых боевых единиц может быть очень сложным. Например, у радиолокационной станции скорее всего будет вращаться антена, что потребует нескольких кадров анимации. Танк, с другой стороны, в состоянии ожидания не выполняет никаких видимых действий. Это показано на рис. 8.13.
Рис. 8.13. Кадры анимации ожидания для танка
Обратите внимание, что для танка, находящегося в состоянии ожидания, достаточно одного кадра. Это вызвано тем, что в состоянии ожидания танк ничего не делает!
Следующая переменная, m_iNumMoveFrames, сообщает сколько кадров в анимационной последовательности, показываемой при передвижении боевой единицы. Пример показан на рис. 8.14.
Рис. 8.14. Кадры анимации передвижения танка
Как видите, при передвижении танка используются три кадра анимации. Положение колес на каждом из кадров слегка отличается. В результате, показываемые один за другим, эти кадры создают иллюзию движения.
Переменная, m_iNumAttackFrames, указывает, сколько кадров присутствует в анимационной последовательности, показываемой когда подразделение кого-нибудь атакует. Этот момент иллюстрирует рис. 8.15.
Рис. 8.15. Кадры анимации танковой атаки
На рис. 8.15 видно, что для анимации атакующего танка используется два кадра. На первом кадре изображен обычный танк, а на втором кадре к его изображению добавляется вспышка выстрела. Красота этой системы в том, что при желании вы можете ее детализировать или упрощать произвольным образом. Для этого вам достаточно создать собственные кадры анимации и указать их количество.
Переменная m_iNumDieFrames сообщает вам сколько кадров содержится в анимационной последовательности, показываемой при гибели боевой единицы. Пример показан на рис. 8.16.
Рис. 8.16. Кадры гибели танка
Обратите внимание, что на рис. 8.16 для анимации гибели танка используются три кадра. В первом кадре изображен обычный танк, во втором кадре нарисован красивый взрыв, а в третьем кадре мы видим искореженный и обгоревший танк. Эта анимационная последовательность будет воспроизводиться всякий раз, когда гибнет подразделение, так что убедитесь, что она выглядит впечатляюще!
Я уверен, что для своих боевых единиц вы придумаете еще множество различных типов анимации. Самое замечательное, что для этого вам достаточно добавить несколько переменных в базовый класс анимации для хранения необходимой информации в вашей игре.
Четыре переменных сообщают вам какой кадр является начальным для каждого типа анимации. Сперва это может звучать странно, и чтобы облегчить понимание взгляните на рис. 8.17.
Рис. 8.17. Полная анимационная последовательность для танка
На рис. 8.17 показаны сразу все кадры анимации танка. Первый кадр — это изображение ожидающего танка. Следующие три кадра содержат анимационную последовательность для движения. Следующие два кадра содержат анимационную последовательность атаки. Последние три кадра содержат анимационную последовательность гибели. Вместо того, чтобы хранить кадры анимации в различных массивах, класс анимации сохраняет их все в одном непрерывном массиве. Это означает, что все кадры будут расположены один за другим. В результате, анимация ожидания начинается с нулевого кадра, а анимационная последовательность перемещения — нет. Стартовый кадр каждой последовательности зависит от того, сколько кадров находится перед ним. Рассмотрим для примера анимационную последовательность атаки. Она начинается с четвертого кадра в цепочке, поскольку перед ней расположены кадр для состояния ожидания и анимационная последовательность передвижения. Помните, что номер первого кадра в цепочке — 0, а не 1. Взглянув еще раз на рисунок, вы заметите, что под каждым кадром приведен связанный с ним порядковый номер. В данном примере анимационная последовательность ожидания начинается с кадра 0, анимационная последовательность передвижения — с кадра 1, анимационная последовательность атаки — с кадра 4 и анимационная последовательность гибели — с кадра 6. Если вы добавите кадры в середину набора, номера начальных кадров расположенных правее анимационных последовательностей должны быть увеличены.
Указатель m_Textures применяется для хранения кадров анимации подразделения. Он указывает на массив объектов CTexture и замечательно справляется с задачей хранения информации.
Переменная m_iTotalTextures сообщает вам, сколько всего кадров анимации требуется для данного подразделения. Она, помимо всего прочего, полезна для контроля за расходованием памяти.
Последний относящийся к текстурам член данных — m_pd3dDevice. Он содержит указатель на графическую систему Direct3D используемый при загрузке текстур. Поскольку этот указатель необходим функциям загрузки текстур в DirectX, я включил его в класс текстуры.
В классе анимации есть уже ставшие привычными конструктор, деструктор и функция установки начальных значений, но к ним добавились две новые функции: vSetRenderDevice() и vLoadTextures().
Поскольку DirectX для загрузки текстуры необходимо устройство визуализации, я добавил функцию установки устройства визуализации, которая инициализирует указатель на устройство. В единственном параметре этой функции передается указатель LPDIRECT3DDEVICE9, который сохраняется в члене данных m_pd3dDevice. Позднее он будет использован для загрузки данных текстуры.
Вот как выглядит код этой функции:
void CUnitAnimation::vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d) { m_pd3dDevice = pd3d; }
Функция загрузки текстур получает информацию, хранящуюся в относящихся к кадрам анимации членах данных класса и загружает соответствующие файлы с текстурами. Вот ее код:
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++; } }
Пожалуйста, не бейте меня! Я понимаю, что это большой фрагмент кода, но к счастью в нем много повторяющихся фрагментов. В работе кода можно выделить два основных этапа. На первом этапе осуществляется выделение памяти для объектов текстур. Здесь вычисляется количество текстур, необходимых для добавления всех кадров анимации. На втором этапе для каждой анимационной последовательности выполняется цикл в котором загружаются необходимые для нее текстуры.
Здесь начинаются хитрости анимационной графики. Функция загрузки текстур выделяет необходимую для текстур память, а затем в цикле перебирает кадры каждой анимационной последовательности, загружая данные текстур. Но для чего нужна константа 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.
После того, как название файла создано, я задаю устройство визуализации для объекта текстуры, после чего вызываю функцию загрузки объекта текстуры. Завершив эти действия я увеличиваю счетчик общего количества загруженных текстур и заканчиваю цикл.
Поскольку в класс анимации включены графические данные, функция установки начальных значений стала сложнее. Это вызвано тем, что функция должна освобождать память, выделенную для хранения текстур. Вот как выглядит код:
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. Просто, не так ли?
Как я упоминал ранее, класс текстур используется мной для хранения данных Почему я использую отдельный класс текстур? Я думаю, что такой подход упрощает переход к новым версиям 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); };
Класс не очень сложен, поскольку он всего лишь загружает и хранит данные текстуры.
Переменная m_szName хранит имя файла с текстурой, а переменная m_pTexture хранит загруженные данные. Еще раз упомяну переменную m_pd3dDevice. Она необходима для загрузки данных текстуры.
Помимо конструктора и деструктора в классе текстуры присутствуют три функции: vLoad(), vRelease() и vSetRenderDevice().
Функция загрузки пользуется весьма полезной вспомогательной библиотекой DirectX чтобы загрузить графическое изображение из файла в буфер данных текстуры. Вот как выглядит код этой функции:
void CTexture::vLoad(char *szName) { // Сохраняем имя файла strcpy(m_szName, szName); // загружаем текстуру D3DXCreateTextureFromFile(m_pd3dDevice, m_szName, &m_pTexture); }
Первая строка кода функции сохраняет переданное в параметре имя файла текстуры для последующего использования. Я в дальнейшем не использую это имя, но его наличие очень полезно, если вдруг потребуется заново загрузить данные текстуры.
Затем функция загружает текстуру с помощью вспомогательной функции DirectX. Вы уже видели аналогичный код ранее, так что здесь никаких сюрпризов возникнуть не должно.
Функция освобождения ресурсов очень проста, поскольку ей необходимо только освободить выделенную для хранения текстуры память. Вместо оператора delete используется метод Release, поскольку это требование DirectX. Код функции приведен ниже:
void CTexture::vRelease(void) { // Удаление текстуры, если она есть в памяти if(m_pTexture) { m_pTexture->Release(); m_pTexture = NULL; } }
Сначала я проверяю, была ли выделена память для объекта текстуры; если да, то я вызываю метод для освобождения памяти, занятой данными текстуры. В результате данные удаляются из памяти.
Чтобы обеспечить возможность установки внутреннего указателя на устройство визуализации, я предоставляю функцию задания устройства визуализации. Она получает указатель на основное устройство трехмерной визуализации и сохраняет его в локальной переменной объекта текстуры. Если вы хотите взглянуть на код, откройте файл UnitTemplateClasses.cpp.
Вот и все, ребята! Я стремительно пролетел сквозь класс текстуры, но он действительно очень прост и не требует особого внимания. Надеюсь, вы согласны. Если нет, загрузите Age of Mythology и пришлите мне ICQ с предложением поиграть!
Вот мы и рассмотрели все необходимые для подразделения базовые классы. У нас есть готовые к использованию данные защиты, атаки, передвижения и анимации. Отсутствует только клей, который соединит эти разрозненные компоненты вместе. По отдельности эти детали не слишком полезны, но собранные вместе они образуют подразделение. Здесь и вступает в игру класс 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); };
С точки зрения количества функций класс не выглядит слишком сложным. Большая часть кода состоит из объявлений необходимых для игры переменных состояния. Это ни в коем случае нельзя считать достаточным для завершенного класса подразделения. Перечисленных переменных состояния достаточно только для рассматриваемого примера. В реальной игре их будет гораздо больше!
Давайте начнем с указателей на базовые типы. В рассматриваемом примере есть тип защиты, три типа атаки, тип перемещения и тип анимации. Я использую три типа атаки потому что на одной боевой единице может быть установлено несколько типов оружия. Например, на танке обычно установлена пушка и пулемет. Имея три типа атаки подразделение в игре может стрелять из трех различных типов оружия. Если вам не нужна такая сложность, просто удалите лишние типы атаки. Если же вы хотите усложнить игровой процесс, добавьте их!
Переменная 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 вы можете видеть как переменные состояния связаны с соответствующими базовыми типами. Например, максимальное значение номера кадра анимации ожидания берется из находящегося в классе анимации поля с количеством кадров анимации ожидания.
В классе CUnit я реализовал сравнительно мало методов. Идея проста — вы сами добавите необходимые вам методы, базируясь на потребностях собственного проекта. Итак, вот та часть работы, которую я проделал за вас.
Функция установки начальных значений работает точно так же, как и в других, рассмотренных в этой главе классах, присваивая данным подразделения значения по умолчанию. Здесь нет ничего сложного, поэтому я пропущу описание этой функции, посмотреть на которую можно в коде проекта.
Данная функция устанавливает указатели на базовые классы для подразделения. Вы можете сделать это вручную, но наличие одной простой функкции сделает вашу жизнь чуть легче. Вот как выглядит код функции:
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; }
В коде я присваиваю внутренним указателям на базовые типы переданные функции параметры. Параметров всего шесть: один для защиты, один для передвижения, один для анимации и три для атаки.
Функция задания местоположения позволяет установить координаты X и Y подразделения с помощью одного вызова. Она получает данные о новом местоположении подразделения и сохраняет их во внутренних переменных. Вот как выглядит код:
void CUnit::vSetPosition(float fX, float fY) { m_fXPos = fX; m_fYPos = fY; }
Вот так, красиво и просто!
Теперь у вас есть класс атаки, класс защиты, класс передвижения, класс анимации и даже класс подразделения, чтобы объединить все предыдущие классы в единое целое. Чего же не хватает? Класса для управления всей этой информацией! Все эти классы великолепны, но ручное управление ими подобно камешку в ботинке. Класс диспетчера выполняет эту работу за вас, объединяя различные строительные блоки из которых состоят подразделения в одном месте. Класс диспетчера решает за вас следующие задачи:
У вас есть базовые классы для хранения данных подразделения, но как загрузить в них информацию? Один из способов — жестко задать все значения параметров подразделений в коде программы. Подобное сляпанное наспех решение не позволит создать гибкую систему. Я предпочитаю использовать конфигурационные файлы, которые загружаются во время работы программы. Вы можете редактировать конфигурационные файлы и снова запускать игру без повторной компиляции. Это неоценимое преимущество, поскольку вы наверняка будете менять параметры вашей игры во время разработки. Это также позволяет легко создавать расширения для игры, поскольку для создания новых типов подразделений достаточно изменить значения нескольких параметров в конфигурационных файлах.
В классе есть функция 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 показаны базовые типы, содержащиеся в классе диспетчера подразделений. Слева указаны типы, а в центре — названия реальных полей. Изображения хранилищ данных справа на рисунке представляют выделенную для хранения базовых типов память.
Вы узнали, качие элементы класса хранят информацию базовых типов, но как загружаются данные? Здесь вступает в игру функция 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
Данная функция получает в своем единственном параметре строку и ищет тип защиты с указанным именем. Если такой тип найден, функция возвращает указатель на него. Вот как выглядит код этого бриллианта:
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(). После этого базовый тип подразделения готов к использованию.
Вот и все, что я хотел рассказать о коде, импортирующем данные базовых типов в диспетчер подразделений. Я знаю, что материал достаточно сложен и вам возможно придется несколько раз прочитать его, прежде чем все станет понятно.
Теперь, после того как базовая информация о подразделених загружена, вы можете создавать подразделения, которые будут использоваться в игре. Вы не можете модифицировать базовые типы, так что следует создавать новые объекты подразделений. Здесь в игру вступает член данных диспетчера подразделений с именем m_UnitObjs. Данный массив хранит модифицируемые объекты подразделений, использующиеся в игре. Для управления этими объектами применяются две функции: iAddUnit() и vRemoveUnit().
Когда вы хотите ввести в игру новое подразделение, следует вызвать функцию добавления подразделения. Она находит неактивное подразделение и инициализирует его данные для использования в игре. Вот как выглядит код этой функции:
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 используют в качестве основы данные, хранящиеся в базовых типах.
Я уже показывал вам управление текстурами ранее, в разделе посвященном импорту данных подразделений. Поскольку функция 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 | < Назад | Оглавление | Далее > |