Работа с пакетом D3DFrame

         

Изменения в функции vCreateToolbar()


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

hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, hinst, NULL); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, hinst, NULL); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, hinst, NULL); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, hinst, NULL);

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



Изменения в функции vRender()


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

// Слои for(iLayer = 0; iLayer < 4; iLayer++) { // Вычисляем смещение в буфере iBufferPos = iX+g_iXPos+((iY+g_iYPos)*g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos][iLayer]; // Отображаем блок if(iCurTile != 0 || iLayer == 0) { vDrawInterfaceObject((iX * g_iTileSize), (iY * g_iTileSize), (float)g_iTileSize, (float)g_iTileSize, iCurTile); } }

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

На Рисунок 10.17 показано совмещение слоев в действии. Там изображены четыре слоя с блоками. Первый слой заполнен блоками с номером 1. Большая часть второго слоя заполнена блоками с номером 0, но кроме этого там есть несколько блоков с номером 2. Большая часть третьего слоя также заполнена блоками с номером 0, но на нем есть и несколько блоков с номером 3. Аналогичным образом устроен и четвертый слой. Когда слои совмещаются вместе, блок с номером 0 работает как цветовой ключ для размещения второго, третьего и четвертого слоев поверх первого. Результат виден в нижней части иллюстрации. Рассмотрев изображенные в левой части рисунка отдельные блоки вы поймете, как он получен.



Изменения в заголовочном файле


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

int g_iTileSize = 32; int g_iTilesWide = 20; int g_iTilesHigh = 15; int g_iMapWidth = 100; int g_iMapHeight = 100; int g_iXPos = 0; int g_iYPos = 0; int g_iTileMap[10000][4]; int g_iCurTile = 0; int g_iCurTileSet = 0; int g_iMaxTileSet = 3; int g_iTotalTiles = 18; int g_iCurLayer = 0;

Изображение частиц


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

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



Кадры анимации ожидания для танка





Обратите внимание, что для танка, находящегося в состоянии ожидания, достаточно одного кадра. Это вызвано тем, что в состоянии ожидания танк ничего не делает!





Кадры анимации передвижения танка





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



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





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



Кадры гибели танка





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

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



Кадры с цветами владельца для вертолета 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.



Класс CParticle


Класс CParticle предназначен для хранения всей информации, необходимой системе частиц для управления отдельной частицей. Он не предназначен для управления набором частиц. Для создания системы частиц вам потребуется написать диспетчер частиц.



Класс 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.



Класс 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); };

Класс CVector


В начале заголовочного файла расположена реализация очень простого класса вектора. Я мог бы воспользоваться для представления векторов вспомогательным классом DirectX, но предпочел создать собственный класс, чтобы обеспечить переносимость кода. Мой класс вектора используется для хранения значений X, Y и Z таких параметров частиц, как местоположение и скорость. Как видно из кода, класс является только хранилищем данных и ничем более.



Коэффициенты поражения


На рис 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 означает, что для данного способа атаки подразделение практически неуязвимо.



Компоненты редактора карт


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

Область редактирования. Область выбора блоков. Мини-карта. Область вывода информации.

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





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



Методы генерации карт


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



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


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

Функция CParticle() является конструктором класса и ее основная задача — очистить все члены данных, присвоив им значения по умолчанию.

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

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

Функция bIsAlive() сообщает вам жива еще частица или нет. Если она возвращает 0, значит частица уже уничтожена. Если она возвращает 1 — частица еще жива. Чтобы определить, какое значение возвращать, функция проверяет значение члена данных m_iLife.

Функция vSetTextures() устанавливает информацию об анимации текстур, которая будет использоваться частицей.

Функция vSetPos() устанавливает начальное местоположение частицы.

Функция vSetAcceleration() устанавливает начальное ускорение частицы.

Функция vSetGravity() задает гравитационное воздействие на частицу.

Функция vSetSpeed() задает начальную скорость частицы.

Функция vSetLife() устанавливает период жизни частицы.



Методы класса 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(), точно так же как это делалось в классе типов защиты. Вот и все, что можно сказать о работе класса типов атаки.



Миникарта


Область мини-карты показывает вам, как редактируемый мир выглядит с большой высоты. На Рисунок 10.1 и Рисунок 10.2 мини-карта расположена в левом верхнем углу интерфейса. Хороший метод для применения в мини-картах — назначить различные цвета различным типам блоков. Например, вы можете выбрать зеленый цвет для блоков изображающих землю и синий цвет — для блоков изображающих воду.



Многомерный массив


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

Следующее изменение — добавление переменной g_iCurLayer. Она отслеживает с каким именно слоем карты ведется работа. Это очень важно знать, когда вы размещаете новый блок в окне редактирования. Программа должна знать куда его поместить!



Многослойные карты


Вы помните многослойные блоки, о которых рассказывалось в главе 5? Если нет, вам лучше сейчас вернуться назад и повторить изложенный там материал. Слои позволяют вам отображать несколько блоков один поверх другого. Например, вы можете вывести блок с изображением травы, а затем добавить поверх него блок с изображением деревьев. Вы можете даже добавить поверх блока с изображением деревьев блок с изображением огня, чтобы показать лесной пожар. Открывающиеся возможности безграничны. В связи с этим возникает вопрос: как реализовать редактирование нескольких слоев в редакторе карт? Подумайте об этом, поскольку я собираюсь показать вам подобную возможность! Взгляните на Рисунок 10.16, где изображен редактор карт с поддержкой слоев.



Начальный кадр анимации


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



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


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

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

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

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



Найденный путь





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



Навигация по карте


На блок-схеме программы, изображенной на Рисунок 10.5, присутствует вызов функции vCheckInput(). Навигация по карте осуществляется путем нажатия на клавиши, так что это очень важная функция. Следуйте далее и взгляните на приведенный ниже код:

void vCheckInput(void) { // Чтение из буфера клавиатуры int iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Перебираем в цикле полученные данные for(int i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } // Вверх if(diks[DIK_UP][i]) { g_iYPos--; } // Вниз if(diks[DIK_DOWN][i]) { g_iYPos++; } // Влево if(diks[DIK_LEFT][i]) { g_iXPos--; } // Вправо if(diks[DIK_RIGHT][i]) { g_iXPos++; } // Проверяем, не вышли ли за границы if(g_iYPos < 0) g_iYPos = 0; else if (g_iYPos >= (g_iMapHeight - g_iTilesHigh)) g_iYPos = (g_iMapHeight - g_iTilesHigh); if(g_iXPos < 0) g_iXPos = 0; else if (g_iXPos >= (g_iMapWidth - g_iTilesWide)) g_iXPos = (g_iMapWidth - g_iTilesWide); } } }

Иллюстрации всегда хорошо дополняют слова, так что взгляните на Рисунок 10.6, показывающий работу кода.



Навигация по меню


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

if(g_iCurrentScreen == 0) { // Переход к главному меню if(!stricmp(szZoneHit, "TITLE_SCREEN")) { // Делаем главное меню активным g_iCurrentScreen = 1; // Устанавливаем активные зоны vSetupMouseZones(1); } // Переход к экрану завершения игры else if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем экран завершения текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } }

Приведенный код выполняется когда активен титульный экран. Если пользователь выбирает активную зону TITLE_SCREEN, программа делает активным экран главного меню и устанавливает активные зоны для него. Если выбрана кнопка Exit, код активирует экран выхода из игры и устанавливает активные зоны для него. Такие же действия выполняются в коде для каждого доступного пункта меню. Ход выполнения функции проверки ввода показан на Рисунок 9.9.



Название типа атаки


Переменная m_szName хранит название типа атаки в виде последовательности символов. Это поле действует аналогично полю с названием типа защиты.



Название защиты


Переменная m_szName хранит название защиты в виде строки символов. Я использую ее чтобы было проще узнать тип защиты подразделения без необходимости запоминать соответствующие числовые значения. Это поле добавлено лишь для удобства.



Непосредственное чтение данных клавиатуры





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

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



Объекты DirectInput





На Рисунок 9.1 изображен главный объект DirectInput с двумя объектами устройств. Левый объект является мышью, а правый — клавиатурой. Под мышью изображен экземпляр объекта устройства, представляющий кнопки мыши. Под клавиатурой находится экземпляр объекта устройства, представляющий клавиши клавиатуры.



Область редактирования


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

Хотя область редактирования во многом похожа, на то что видит игрок, обычно существует ряд отличий. Первое из них — добавление сетки блоков. Вы должны включать возможность вывода сетки в области редактирования, чтобы облегчить создателю карт выравнивание блоков. Сетка показывает где начинается и где заканчивается каждый блок. Это также полезно, чтобы показать размер редактируемых блоков. Пример сетки редактирования показан на Рисунок 10.2.



Область выбора блоков


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



Область вывода информации


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



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


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

// Обновление подразделений 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));

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

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

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