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

Активация ввода текста

Функция установки активных зон отвечает за размещение в интерфейсе игры горячих точек, реагирующих на щелчки кнопок мыши. Помимо этого она применяется для активации ввода текста в экране начала новой игры. Приведенный ниже код покажет вам, как я выполняю эти действия:
// Экран начала новой игры else if(iMenu == 4) { MZones.vFreeZones(); MZones.vInitialize(1); MZones.iAddZone("EXIT_BUTTON", 587, 0, 53, 24, 0); // // Установка поля ввода текста // // Установка позиции курсора g_shTextInputXPos = 200; g_shTextInputYPos = 196; // Очистка текста memset(g_szTextInputBuffer, 0x00, 64); // Установка позиции ввода данных g_shTextInputPosition = 0; // Установка активного поля данных g_iTextInputFieldID = GAMEINPUT_NAME; // Установка флага активности поля ввода g_bTextInputActive = 1; // Установка таймера мерцания курсора g_dwTextInputTimer = 0; // Установка состояния мерцания курсора g_bTextInputCursorFlash = 0; // Установка максимальной длинны текста: 20 символов g_shTextMaxSize = 20; } Приведенный выше код — всего лишь фрагмент функции vSetupMouseZones(), но он показывает вам как я сообщаю программе о необходимости принять вводимое с клавиатуры имя игрока. Код не только устанавливает активные зоны для экрана начала новой игры, но и присваивает значения нескольким глобальным переменным, которые сообщают программе, как обрабатывать вводимый текст. Эти переменные и их назначение перечислены в таблице 9.2.
Таблица 9.2. Глобальные переменные, управляющие вводом текста
Переменная Описание
g_shTextInputXPos Координата X текстового поля ввода.
g_shTextInputYPos Координата Y текстового поля ввода.
g_szTextInputBuffer Хранит содержимое текстового поля.
g_shTextInputPosition Активная позиция в текстовом поле.
g_iTextInputFieldID Следит, какое текстовое поле активно.
g_bTextInputActive Сообщает системе, что текстовый ввод включен.
g_dwTextInputTimer Таймер для анимации курсора в активном текстовом поле.
g_bTextInputCursorFlash Определяет включен курсор или выключен во время мерцания.
g_shTextMaxSize Максимальное количество символов в буфере.

В приведенном выше коде я устанавливаю координаты текстового поля таким образом, чтобы они указывали на верхний левый угол первого символа в поле с именем игрока. Благодаря этому система визуализации будет знать где отображать текст с именем, когда он будет введен. Кроме того, эти значения сообщают системе визуализации где отображать курсор.
Затем устанавливается позиция ввода текста. Я присваиваю этой переменной 0, чтобы игрок вводил текст с начала буфера имени.
Далее идентификатору поля присваивается значение GAMEINPUT_NAME. В заголовочном файле main.h у меня есть ряд констант, соответствующих присутствующим в игре текстовым полям. Вы не обязаны использовать константы, но мне они помогают следить за тем, что происходит в программе.
Потом я присваиваю полю g_bTextInputActive значение 1. Оно сообщает программе, что текстовое поле активно и ожидает ввод. Это важно знать, так как программа должна добавлять текст в поле и отображать его.
После того, как текстовое поле активизировано, я присваиваю 0 переменной g_dwTextInputTimer. Данный таймер отвечает за анимацию курсора. Следующая переменная, g_bTextInputCursorFlash, определяет включен курсор или выключен. Когда таймер курсора заканчивает отсчет она меняет свое состояние.
Последнее, что требуется сделать для инициализации текстового ввода — задать максимальное количество символов в имени игрока. Я делаю это присваивая переменной g_shTextMaxSize значение 20.

Алгоритмы генерации карт

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


Анимация атаки

Переменная, m_iNumAttackFrames, указывает, сколько кадров присутствует в анимационной последовательности, показываемой когда подразделение кого-нибудь атакует. Этот момент иллюстрирует Рисунок 8.15.


Анимация частиц

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


Анимация гибели

Переменная m_iNumDieFrames сообщает вам сколько кадров содержится в анимационной последовательности, показываемой при гибели боевой единицы. Пример показан на Рисунок 8.16.


Анимация ожидания

Переменная m_iNumStillFrames сообщает сколько кадров используется в анимационной последовательности, изображающей боевую единицу в состоянии ожидания. Множество подразделений в состоянии ожидания ничего не делают, но поведение некоторых боевых единиц может быть очень сложным. Например, у радиолокационной станции скорее всего будет вращаться антена, что потребует нескольких кадров анимации. Танк, с другой стороны, в состоянии ожидания не выполняет никаких видимых действий. Это показано на Рисунок 8.13.


Анимация передвижения

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


Анимационная последовательность


Анимационная последовательность


На Рисунок 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.
После того, как название файла создано, я задаю устройство визуализации для объекта текстуры, после чего вызываю функцию загрузки объекта текстуры. Завершив эти действия я увеличиваю счетчик общего количества загруженных текстур и заканчиваю цикл.

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

Базовой стоимостью узла называется стоимость передвижения через данный узел. В простейшем случае базовая стоимость всех доступных узлов одна и та же. Однако, если вы хотите усложнить игру, можно назначить различным узлам различную стоимость, в зависимости от их типа ландшафта. Взгляните, например, на следующий список узлов с их стоимостью:
Таблица 12.1. Базовая стоимость узлов
Тип узла Стоимость
Трава 1
Грязь 2
Песок 3
Скалы 4
Болото 5

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

Базовые типы в классе диспетчера подразделений


Базовые типы в классе диспетчера подразделений


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

Блоксхема ввода данных с клавиатуры


Блоксхема ввода данных с клавиатуры


На Рисунок 9.10 показана логика, необходимая для получения данных от клавиатуры и их помещения в текстовое поле. Начнем сверху: программа вызывает функцию чтения с клавиатуры, чтобы проверить есть ли какие-либо ожидающие обработки данные. Если есть, система в цикле перебирает полученные данные и выполняет ряд проверок. Сперва проверяется не была ли нажата клавиша Esc. Если да, программа помещает в очередь сообщение о выходе и завершает работу. Если нет, работа продолжается и выполняется проверка активности текстового поля. Если текстовое поле активно, система проверяет осталось ли в текстовом поле свободное место для ввода очередного символа. Если свободное место обнаружено, программа в цикле перебирает все клавиши клавиатуры и проверяет состояние каждой из них. Если проверяемая на данной итерации цикла клавиша является алфавитно-цифровой или пробелом, программа проверяет, была ли данная клавиша отпущена. Если клавиша была отпущена, проверяется нажата ли клавиша Shift. Если да, программа помещает в буфер имени игрока символ данной клавиши в верхнем регистре. Если клавиша Shift не нажата, в буфер помещается полученный по умолчанию символ. Данный процесс повторяется, пока не будут обработаны все состояния клавиш, находящиеся в буфере DirectInput.
Кроме того, на Рисунок 9.10 изображены проверки нажатия клавиш Backspace и Enter. Если игрок нажимает клавишу Backspace, программа удаляет последний символ в буфере имени игрока и передвигает курсор на одну позицию назад. Если нажата клавиша Enter, программа переходик к экрану новой игры и деактивирует текстовый ввод.

Более сложный путь


Более сложный путь


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


Буферизованный ввод с клавиатуры

Следующая часть функции может показаться вам странной, поскольку пока я еще не объяснил ее назначение. Дело в том, что для клавиатуры имеется два способа получения входных данных: непосредственный и буферизованный. Непосредственный ввод позволяет получить состояние клавиш на момент опроса. Если пользователь нажал клавишу хотя бы на 1/100 секунды раньше, это событие будет пропущено, поскольку оно не произошло именно в тот момент, когда выполнялась проверка. В игре это представляет серьезную проблему, поскольку циклы визуализации и обработки данных отнимают много времени, что может привести к частой потере вводимых данных. Данный момент проиллюстрирован на Рисунок 9.4.

Буферизованный ввод с клавиатуры


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

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

Переменная m_szName хранит имя файла с текстурой, а переменная m_pTexture хранит загруженные данные. Еще раз упомяну переменную m_pd3dDevice. Она необходима для загрузки данных текстуры.

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

Давайте начнем с указателей на базовые типы. В рассматриваемом примере есть тип защиты, три типа атаки, тип перемещения и тип анимации. Я использую три типа атаки потому что на одной боевой единице может быть установлено несколько типов оружия. Например, на танке обычно установлена пушка и пулемет. Имея три типа атаки подразделение в игре может стрелять из трех различных типов оружия. Если вам не нужна такая сложность, просто удалите лишние типы атаки. Если же вы хотите усложнить игровой процесс, добавьте их!
Переменная m_iCurHitPoints хранит текущий показатель здоровья подразделения. Когда ее значение достигает 0, подразделение погибает. Максимально возможное значение этого поля хранится в переменной m_iHitPoints класса защиты.
Переменная m_fCurSpeed показывает текущую скорость подразделения. Чтобы вычислить куда переместилось подразделение следует умножить текущую скорость на вектор направления. Когда движение подразделения замедляется, выполняется вычитание из этого значения, а чтобы подразделение двигалось быстрее, увеличьте значение данного поля. Максимальное значение данного поля хранится в переменной базового класса типа перемещения с именем m_fMovementSpeed.
Переменные m_fXPos и m_fYPos хранят местоположение подразделения на карте. В рассматриваемом примере используется двухмерная графика и поэтому координат требуется тоже две — X и Y.
Переменная m_fRot указывает угол поворота боевой единицы в градусах. Это значение используется, когда необходимо развернуть подразделение по направлению к противнику или определить направление перемещения. Поскольку значение изменяется в градусах, допустимый диапазон значений — от 0.0 до 359.0.
Поле m_fScale задает текущий размер подразделения. Оно применяется для того, чтобы в двухмерной графике создать эффект приближения подразделения к камере. Обычно значение данной переменной равно 1.0, чтобы подразделение выглядело таким же, как и при разработке.
Переменная m_iUnitID хранит уникальный идентификатор подразделения. Он необходим для реализации многопользовательской игры. Очень трудно приказать другому компьютеру уничтожить подразделение, если вы не можете сообщить его идентификатор.
Поле m_iParentID указывает какое подразделение является владельцем данного. Переменная используется для транспортных средств, таких как десантные самолеты и авианосцы. Если значение переменной отличается от –1, значит данное подразделение перевозится другой боевой единицей. Если же значение равно –1, — подразделение не имеет назначенного родителя.
Массив символов m_szName хранит название подразделения. Оно используется для отображения в интерфейсе пользователя и других информационных целей.
Поле m_bActive сообщает вам, является ли подразделение активным в данный момент. Поскольку в игре для каждого игрока выделяется лишь ограниченное количество боевых единиц, погибшие подразделения должны отмечаться как неактивные, чтобы освободившиеся записи можно было использовать для других подразделений. Когда подразделение отмечено как активное, оно используется в игре и его запись не может использоваться для других целей.
Поле m_iOwner сообщает кто является владельцем данного подразделения. Одно из его применений — назначение цветов владельца при отображении графики.
Поле m_iCurAnimFrame указывает, какой именно кадр анимационной последовательности отображается в данное время.
Поле m_iCurAttackFrame следит за анимацией подразделения во время воспроизведения анимационной последовательности атаки. Это необходимо потому что у вас может быть несколько кадров для каждого типа анимации.
Поле m_iCurStillFrame работает так же как и предыдущее, но следит за анимацией ожидания, а не атаки. Оно используется в те моменты, когда подразделение ничем не занято.
Поле m_iCurMoveFrame похоже на остальные счетчики кадров анимации, но используется когда подразделение перемещается.
Переменная m_iCurDieFrame работает также как и предыдущие счетчики кадров анимации и используется только при гибели подразделения. О-ох, взгляните на эти взрывы!
Чтобы увидеть как переменные состояния связаны с базовыми типами, взгляните на Рисунок 8.21.


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

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

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

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


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

Класс содержит переменные, аналогичные тем, которые находятся в классе атаки, за исключением того, что их значения относятся к перемещению, а не атаке. Члены данных класса показаны на Рисунок 8.11.


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

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


Члены данных

В классе частицы я объявляю несколько переменных, предназначенных для описания характеристик частицы. Вот они:
Вектор m_vecPos определяет местоположение частицы в трехмерном пространстве. Функция визуализации использует эту информацию, чтобы разместить частицу в требуемом месте экрана.
Вектор m_vecCurSpeed определяет насколько быстро передвигается частица относительно каждой из трех осей координат. При каждом обновлении игрового экрана будет браться скороть частицы и прибавляться к ее текущему местоположению. В результате частица с течением времени будет перемещаться в заданном темпе.
Вектор m_vecAcceleration определяет как будет изменяться скорость частицы в каждом такте игры. Это позволяет вам получать частицы, которые со временем будут замедляться или ускоряться.
Вектор m_vecGravity позволяет указать, как на частицу влияет гравитация. На каждом такте игры влияние гравитации суммируется со скоростью частицы. Это позволяет реализовать вызываемые гравитацией эффекты без модификации базовой скорости.
Элемент m_iLife сообщает сколько игровых тактов должно пройти, прежде чем частица станет неактивной и будет уничтожена. На каждом игровом такте значение этой переменной уменьшается на единицу. Это очень полезно для таких частиц, продолжительность жизни которых ограничена. Некоторые частицы, например, вода, могут постоянно присутствовать в игре, в то время как частицы дыма и огня обычно исчезают через какой-то период времени.
Элементы m_iTextureStart и m_iTextureEnd задают диапазон используемых для анимации частиц текстур. Класс частицы поддерживает анимацию текстур в форме задания начальной и конечной текстуры. Вы можете установить эти параметры в тех случаях, когда за время жизни частицы ее изображение изменяется. В результате можно реализовать такие замечательные спецэффекты, как дымовые следы, о которых я говорил ранее.
Элемент m_iTextureType сообщает как именно должна анимироваться текстура. Можно использовать единственную текстуру (т.е. анимация отсутствует), либо можно в цикле перебрать текстуры от начальной до конечной и остановиться. Также можно перебрать текстуры от начальной до конечной (или в обратном порядке), а затем повторять цикл анимации снова. Основное назначение этого параметра — предоставить возможность контроллировать стиль анимации текстур.
Элемент m_iTextureCur сообщает частице, какая именно текстура используется в данный момент времени.
Элемент m_iTextureSteps сообщает сколько тактов игры следует ожидать, прежде чем сменить одну текстуру на другую. Это позволяет задавать паузу в анимации текстуры на несколько тактов игры.
Элемент m_iTextureCurStep сообщает сколько еще тактов игры должно проити до смены текстуры. Отсчет начинается с 0 и идет до значения, заданного переменной m_iTextureSteps. Как только счетчик достигнет максимального значения, будет изменено значение переменной m_iTextureCur а отсчет в переменной m_iTextureCurStep начнется снова с 0.

Чтение данных клавиатуры

Вернемся к функции WinMain() и рассмотрим следующий фрагмент кода:
while(msg.message != WM_QUIT) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { // Чтение из буфера клавиатуры iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Цикл обработки полученных данных for(i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } else if (ascKeys[13][i]) { PostQuitMessage(0); } } } } } Представленный код является стандартным циклом обработки сообщений Windows. Его ключевой особенностью является вызов функции iReadKeyboard(). Обращение к ней происходит каждый раз, когда в очереди нет системных сообщений для обработки. Функция возвращает количество зафиксированных изменений состояний клавиш и сохраняет их в глобальных массивах diks и ascKeys. Если функция возвратила какие-нибудь данные, программа в цикле перебирает полученные изменения состояний клавиш и проверяет не была ли нажата клавиша Esc. Если клавиша была нажата, выполнение программы завершается.

Цвета владельца

Здесь начинаются хитрости анимационной графики. Функция загрузки текстур выделяет необходимую для текстур память, а затем в цикле перебирает кадры каждой анимационной последовательности, загружая данные текстур. Но для чего нужна константа 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.


Дальнобойность

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

Данные атаки хранящиеся в электронной таблице Excel


Данные атаки хранящиеся в электронной таблице 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.


Данные текстуры

Указатель m_Textures применяется для хранения кадров анимации подразделения. Он указывает на массив объектов CTexture и замечательно справляется с задачей хранения информации.
Переменная m_iTotalTextures сообщает вам, сколько всего кадров анимации требуется для данного подразделения. Она, помимо всего прочего, полезна для контроля за расходованием памяти.
Последний относящийся к текстурам член данных — m_pd3dDevice. Он содержит указатель на графическую систему Direct3D используемый при загрузке текстур. Поскольку этот указатель необходим функциям загрузки текстур в DirectX, я включил его в класс текстуры.

Данные защиты в электронной таблице Excel


Данные защиты в электронной таблице 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 показаны данные для типов атаки.


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


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


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

Два квадрата с различными базовыми точками


Два квадрата с различными базовыми точками


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


Два подразделения с разной скоростью поворота


Два подразделения с разной скоростью поворота


На Рисунок 8.12 скорость поворота левого танка равна 45. Скорость поворота правого подразделения равна 22.5. За два раунда левый танк повернется вправо. И у него останется еще два раунда, прежде чем правый танк сможет повернуться к нему. Если эти два танка сражаются, левый танк сможет несколько раз выстрелить, прежде чем правый развернет свою пушку в его направлении! Вот почему скорость поворота так важна в сражениях.
Некоторые игры не беспокоятся о скорости поворота. Они просто считают, что подразделение может сразу передвигаться в заданном направлении не тратя времени на поворот. Это помогает сохранить быстрый темп игры, но отнимает у нее значительную долю реализма.

Два текстурированных квадрата с различными базовыми точками


Два текстурированных квадрата с различными базовыми точками


На Рисунок 8.29 показаны те же два квадрата, что и ранее, но на них нанесена текстура с изображением танка. Танк слева поворачивается очень странно, поскольку его базовая точка расположена неверно. Танк справа поворачивается правильно, потому что его базовая точка расположена в центре квадрата.
Итак, какое отношение это имеет к моему примеру? Основной момент заключается в том, что вам необходимо иметь два буффера для геометрии: один для блоков игрового поля и один для подразделений. Чтобы создать квадрат, базовая точка которого находится в центре, вы должны создать вершины вокруг центра. Вот как выглядит код для этого:
// Создание вершин pVertices[0].position = D3DXVECTOR3(-0.5f, -0.5f, 0.0f); pVertices[0].tu = 0.0f; pVertices[0].tv = 1.0f; pVertices[0].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[1].position = D3DXVECTOR3(-0.5f, 0.5f, 0.0f); pVertices[1].tu = 0.0f; pVertices[1].tv = 0.0f; pVertices[1].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[2].position = D3DXVECTOR3(0.5f, -0.5f, 0.0f); pVertices[2].tu = 1.0f; pVertices[2].tv = 1.0f; pVertices[2].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[3].position = D3DXVECTOR3(0.5f, 0.5f, 0.0f); pVertices[3].tu = 1.0f; pVertices[3].tv = 0.0f; pVertices[3].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); Код создает четыре вершины — по одной для каждого из углов квадрата. Их расположение показано на Рисунок 8.30.


Движение частиц

Чтобы частицы произвели какой-нибудь эффект, они должны двигаться. Возьмем для примера фейерверк; когда заряд разрывается на сотни частиц, именно их движение определяет, насколько красивым будет салют. Одни заряды образуют сферы, другие — огенные полосы. Так же работают и системы частиц в играх. Вы, как разработчик, должны написать алгоритм движения, которому будут следовать частицы. Два примера показаны на Рисунок 13.1.


Функция CParticle vUpdate()

Код реализации методов класса находится в файле CParticle.cpp. В нем достаточно много функций, но сравнительно сложной является только функция обновления данных. Цель этой функции — вносить требуемые изменения в состояние частицы на каждом такте игры. Это включает изменение скорости, местоположения и состояния анимации текстуры частицы. Вот как выглядит выполняющий эти действия код:
// Изменяем скорость с учетом ускорения m_vecCurSpeed.fX += m_vecAcceleration.fX; m_vecCurSpeed.fY += m_vecAcceleration.fY; m_vecCurSpeed.fZ += m_vecAcceleration.fZ; // Изменяем скорость с учетом гравитации m_vecCurSpeed.fX += m_vecGravity.fX; m_vecCurSpeed.fY += m_vecGravity.fY; m_vecCurSpeed.fZ += m_vecGravity.fZ; // Обновляем местоположение m_vecPos.fX += m_vecCurSpeed.fX; m_vecPos.fY += m_vecCurSpeed.fY; m_vecPos.fZ += m_vecCurSpeed.fZ; // // Обновление текстуры // // Статическая структура if(m_iTextureType == 0) { m_iTextureCur = m_iTextureStart; } // Покадровая анимация else { m_iTextureCurStep++; if(m_iTextureCurStep >= m_iTextureSteps) { // Линейная if(m_iTextureType == 1) { if(m_iTextureCur != m_iTextureEnd) { m_iTextureCur++; } } // Циклическая прямая else if(m_iTextureType == 2) { m_iTextureCur++; if(m_iTextureCur > m_iTextureEnd) { m_iTextureCur = m_iTextureStart; } } // Циклическая обратная else if(m_iTextureType == 3) { m_iTextureCur--; if(m_iTextureCur < m_iTextureStart) { m_iTextureCur = m_iTextureEnd; } } // Сброс счетчика текстур m_iTextureCurStep = 0; } } // Уменьшение счетчика времени жизни m_iLife--; Код начинается с прибавления текущего значения ускорения частицы к ее текущей скорости. Вы должны быть осторожны, поскольку слишком большое ускорение приведет к тому, что частица очень быстро скроется из поля зрения игрока.
Следующая часть кода учитывает значение гравитации и также прибавляет его к скорости. Благодаря ей у вас есть два способа управления скоростью частицы: ускорение и гравитация. Как только вычислено результирующее значение скорости, оно прибавляется к текущему местоположению частицы. В результате частица переместится на новое место.
Следующий блок кода проверяет используемый метод анимации текстуры и соответствующим образом меняет текущую текстуру.
В коде реализованы четыре типа анимации. Первый тип — использование статической текстуры. Это означает, что в цикле анимации используется только одна, стартовая текстура и никаких изменений с ней в ходе игры не происходит.
Второй тип — линейная анимация. В этом случае текстуры постепенно меняются, пока не будет достигнута конечная текстура. После этого конечная текстура остается неизменной и никаких дальнейших действий не требуется.
Третий тип — циклическая анимация с изменением текстур от начальной до конечной. Когда будет достигнута конечная текстура, цикл анимации вновь начинается с начальной текстуры, и это продолжается до тех пор, пока существует частица.
Четвертый тип анимации — обратная циклическая анимация. Она похожа на предыдущий тип, но выполняется в обратном направлении — от конечной текстуры к начальной. Процесс повторяется в течение всей жизни частицы.
Последняя вешь, которую выполняет функция обновления, — уменьшение на единицу счетчика времени жизни частицы.


Функция CPathFinder bFindPath()

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


Функция CTexture vLoad()

Функция загрузки пользуется весьма полезной вспомогательной библиотекой DirectX чтобы загрузить графическое изображение из файла в буфер данных текстуры. Вот как выглядит код этой функции:
void CTexture::vLoad(char *szName) { // Сохраняем имя файла strcpy(m_szName, szName); // загружаем текстуру D3DXCreateTextureFromFile(m_pd3dDevice, m_szName, &m_pTexture); } Первая строка кода функции сохраняет переданное в параметре имя файла текстуры для последующего использования. Я в дальнейшем не использую это имя, но его наличие очень полезно, если вдруг потребуется заново загрузить данные текстуры.
Затем функция загружает текстуру с помощью вспомогательной функции DirectX. Вы уже видели аналогичный код ранее, так что здесь никаких сюрпризов возникнуть не должно.

Функция CTexture vRelease()

Функция освобождения ресурсов очень проста, поскольку ей необходимо только освободить выделенную для хранения текстуры память. Вместо оператора delete используется метод Release, поскольку это требование DirectX. Код функции приведен ниже:
void CTexture::vRelease(void) { // Удаление текстуры, если она есть в памяти if(m_pTexture) { m_pTexture->Release(); m_pTexture = NULL; } } Сначала я проверяю, была ли выделена память для объекта текстуры; если да, то я вызываю метод для освобождения памяти, занятой данными текстуры. В результате данные удаляются из памяти.

Функция CTexture vSetRenderDevice()

Чтобы обеспечить возможность установки внутреннего указателя на устройство визуализации, я предоставляю функцию задания устройства визуализации. Она получает указатель на основное устройство трехмерной визуализации и сохраняет его в локальной переменной объекта текстуры. Если вы хотите взглянуть на код, откройте файл UnitTemplateClasses.cpp.
Вот и все, ребята! Я стремительно пролетел сквозь класс текстуры, но он действительно очень прост и не требует особого внимания. Надеюсь, вы согласны. Если нет, загрузите Age of Mythology и пришлите мне ICQ с предложением поиграть!

Функция CUnit vReset()

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

Функция CUnit vSetBaseValues()

Данная функция устанавливает указатели на базовые классы для подразделения. Вы можете сделать это вручную, но наличие одной простой функкции сделает вашу жизнь чуть легче. Вот как выглядит код функции:
void CUnit::vSetBaseValues(CUnitDefense* ptrDef, CUnitOffense* ptrOff1, CUnitOffense* ptrOff2, CUnitOffense* ptrOff3, CUnitMovement* ptrMove, CUnitAnimation* ptrAnimation) { // Указатели на переданные классу объекты m_Defense = ptrDef; m_Offense1 = ptrOff1; m_Offense2 = ptrOff2; m_Offense3 = ptrOff3; m_Movement = ptrMove; m_Animation = ptrAnimation; } В коде я присваиваю внутренним указателям на базовые типы переданные функции параметры. Параметров всего шесть: один для защиты, один для передвижения, один для анимации и три для атаки.

Функция CUnit vSetPosition()

Функция задания местоположения позволяет установить координаты X и Y подразделения с помощью одного вызова. Она получает данные о новом местоположении подразделения и сохраняет их во внутренних переменных. Вот как выглядит код:
void CUnit::vSetPosition(float fX, float fY) { m_fXPos = fX; m_fYPos = fY; } Вот так, красиво и просто!

Функция CUnitAnimation vLoadTextures()

Функция загрузки текстур получает информацию, хранящуюся в относящихся к кадрам анимации членах данных класса и загружает соответствующие файлы с текстурами. Вот ее код:
void CUnitAnimation::vLoadTextures(void) { // Загрузка анимаций int i, j; int iLocalCount = 0; char szBitmapFileName[128]; // Выделение памяти для текстур m_Textures = new CTexture[ (m_iNumStillFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumMoveFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumAttackFrames * (UNITMANAGER_MAXOWNERS + 1))+ (m_iNumDieFrames * (UNITMANAGER_MAXOWNERS + 1))]; // Графика для ожидания (покоя) m_iStartStillFrames = 0; for(i = 0; i < m_iNumStillFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для перемещения m_iStartMoveFrames = m_iTotalTextures; for(i = 0; i < m_iNumMoveFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для атаки m_iStartAttackFrames = m_iTotalTextures; for(i = 0; i < m_iNumAttackFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для гибели m_iStartDieFrames = m_iTotalTextures; for(i = 0; i < m_iNumDieFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } } Пожалуйста, не бейте меня! Я понимаю, что это большой фрагмент кода, но к счастью в нем много повторяющихся фрагментов. В работе кода можно выделить два основных этапа. На первом этапе осуществляется выделение памяти для объектов текстур. Здесь вычисляется количество текстур, необходимых для добавления всех кадров анимации. На втором этапе для каждой анимационной последовательности выполняется цикл в котором загружаются необходимые для нее текстуры.

Функция CUnitAnimation vReset()

Поскольку в класс анимации включены графические данные, функция установки начальных значений стала сложнее. Это вызвано тем, что функция должна освобождать память, выделенную для хранения текстур. Вот как выглядит код:
void CUnitAnimation::vReset(void) { memset(m_szName, 0x00, 64); memset(m_szBitmapPrefix, 0x00, 64); // Освобождаем память текстур if(m_iTotalTextures) { delete [] m_Textures; m_Textures = NULL; m_iTotalTextures = 0; } m_iNumStillFrames = 0; m_iNumMoveFrames = 0; m_iNumAttackFrames = 0; m_iNumDieFrames = 0; m_iType = 0; m_iStartStillFrames = 0; m_iStartMoveFrames = 0; m_iStartAttackFrames = 0; m_iStartDieFrames = 0; } Как видно в коде, чтобы определить наличие текстур я проверяю значение переменной m_iTotalTextures. Если какие-либо текстуры загружены, я удаляю массив m_Textures и устанавливаю количество загруженных текстур равным 0. Просто, не так ли?

Функция CUnitAnimation vSetRenderDevice()

Поскольку DirectX для загрузки текстуры необходимо устройство визуализации, я добавил функцию установки устройства визуализации, которая инициализирует указатель на устройство. В единственном параметре этой функции передается указатель LPDIRECT3DDEVICE9, который сохраняется в члене данных m_pd3dDevice. Позднее он будет использован для загрузки данных текстуры.
Вот как выглядит код этой функции:
void CUnitAnimation::vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d) { m_pd3dDevice = pd3d; }

Функция CUnitManager iAddUnit()

Когда вы хотите ввести в игру новое подразделение, следует вызвать функцию добавления подразделения. Она находит неактивное подразделение и инициализирует его данные для использования в игре. Вот как выглядит код этой функции:
int CUnitManager::iAddUnit(char *szName, int iOwner) { int i; int iFoundID = -1; // Ищем соответствующий тип for(i = 0; i < m_iTotalUnitBaseObjs; i++) { if(!stricmp(szName, m_UnitBaseObjs[i].m_szName)) { iFoundID = i; break; } } // Возвращаемся, если базовый тип не найден if(iFoundID == -1) { return(-1); } // Ищем свободный блок данных подразделения for(i = 0; i < m_iTotalUnitObjs; i++) { // Проверяем, является ли блок неактивным if(!m_UnitObjs[i].m_bActive) { // Активируем подразделение m_UnitObjs[i].m_bActive= 1; // Устанавливаем его внутренние типы m_UnitObjs[i].vSetBaseValues( m_UnitBaseObjs[iFoundID].m_Defense, m_UnitBaseObjs[iFoundID].m_Offense1, m_UnitBaseObjs[iFoundID].m_Offense2, m_UnitBaseObjs[iFoundID].m_Offense3, m_UnitBaseObjs[iFoundID].m_Movement, m_UnitBaseObjs[iFoundID].m_Animation); // Устанавливаем тип подразделения m_UnitObjs[i].m_iType = iFoundID; // Устанавливаем владельца подразделения m_UnitObjs[i].m_iOwner = iOwner; // Увеличиваем количество подразделений у владельца m_iOwnerTotal[iOwner]++; return(i); } } return(-1); } Первая часть кода функции в цикле перебирает все базовые типы подразделений и пытается найти тот из них, название которого совпадает со строкой, переданной в параметре функции. Если совпадение найдено, сохраняется идентификатор подразделения и выполнение функции продолжается.
ПРИМЕЧАНИЕ Всегда выполняйте поиск имени базового типа подразделения. Если вы не будете искать имя, соответствующее переданному из вызывающего кода, у вас может использоваться несуществующий тип подразделения! Следующая часть кода в цикле перебирает список всех подразделений игрока, ища неактивную запись. Данный этап необходим потому что активные подразделения уже присутствуют в игре и мы не можем использовать здесь относящиеся к ним записи. После обнаружения неактивного подразделения, оно делается активным и выполняется установка его базовых типов. В конце задается тип подразделения и его владелец. Я также отслеживаю сколько подразделений находится у каждого владельца, чтобы не превысить установленный лимит, если он существует.
Еще раз проясню ситуацию: массив m_UnitObjs хранит данные подразделений, которые изменяются во время игры, а массив m_UnitBaseObjs хранит шаблоны подразделений, которые никогда не меняются. Объекты m_UnitObjs меняют свои данные состояния, а объекты m_UnitBaseObjs — нет. Взаимосвязь между базовыми типами и динамическими объектами показана на Рисунок 8.27.


Функция CUnitManager iLoadBaseTypes()

Вы узнали, качие элементы класса хранят информацию базовых типов, но как загружаются данные? Здесь вступает в игру функция iLoadBaseTypes(). Она получает пять параметров, каждый из которых является именем файла, содержащего импортируемые данные. Отдельные файлы требуются для данных защиты, данных атаки, данных передвижения, данных анимации и данных подразделений. Функция загрузки базовых типов получает имена пяти файлов и импортирует данные из них в диспетчер подразделений. На Рисунок 8.23 показана взаимосвязь между классом диспетчера подразделений и импортируемыми файлами.


Функция CUnitManager ptrGetDefenseType()

Данная функция получает в своем единственном параметре строку и ищет тип защиты с указанным именем. Если такой тип найден, функция возвращает указатель на него. Вот как выглядит код этого бриллианта:
CUnitDefense* CUnitManager::ptrGetDefenseType(char *szName) { int i; CUnitDefense *ptrUnitDefense = NULL; for(i = 0; i < m_iTotalDefObjs; i++) { if(!stricmp(szName, m_DefenseObjs[i].m_szName)) { ptrUnitDefense = &m_DefenseObjs[i]; return(ptrUnitDefense); } } return(ptrUnitDefense); } Код представляет собой простой цикл, перебирающий все загруженные типы защиты. Название каждого типа защиты сравнивается с переданной функции строкой. Если строки совпадают, возвращается указатель на тип защиты. Это позволяет вызывающему коду использовать данные типа защиты без создания копии данных, благодаря чему уменьшается объем занимаемой памяти.
Функции, подобные рассмотренной выше, используются для получения указателей на типы атаки, передвижения и анимации. Я не буду приводить их здесь, поскольку они практически идентичны уже рассмотренному коду и вы можете увидеть их в файле UnitTemplateClasses.cpp.
Вернемся к нашей на время отложенной программе. Теперь, когда у нас есть указатели на различные типы, мы можем сохранить их в объекте боевой единицы с помощью функции vSetBaseValues(). После этого базовый тип подразделения готов к использованию.
Вот и все, что я хотел рассказать о коде, импортирующем данные базовых типов в диспетчер подразделений. Я знаю, что материал достаточно сложен и вам возможно придется несколько раз прочитать его, прежде чем все станет понятно.

Функция ID3DXFont DrawText()

Прототип функции DrawText() выглядит следующим образом:
INT DrawText( LPCSTR pString, INT Count, LPRECT pRect, DWORD Format, D3DCOLOR Color ); Первый параметр, pString, исключительно прост, ведь в нем передается отображаемый текст. В рассматриваемом примере я передаю в этом параметре имя игрока.
Второй параметр, Count, содержит количество отображаемых символов. Я передаю в этом параметре –1, чтобы DirectX мог сам вычислить, сколько символов отображать. Если вы будете поступать так же, убедитесь, что ваша строка завершается нулевым символом!
Третий параметр, pRect, представляет собой описание прямоугольной области, сообщающее DirectX где именно следует отображать текст. В рассматриваемом примере я создаю область визуализации внутри изображения текстового поля ввода и сохраняю ее параметры в переменной rectText.
В четвертом параметре, Format, передаются флаги форматирования, сообщающие системе как выполнять визуализацию текста. В рассматриваемой программе я использую флаг DT_LEFT, указывающий системе, что выводимый текст должен выравниваться по левому краю. Существует множество других флагов. Их описание вы найдете в документации DirectX SDK.
Пятый параметр, Color, определяет цвет, используемый при визуализации. В этом параметре я использую макрос D3DCOLOR_RGBA(), позволяющий просто указать значения RGBA для шрифта.


Функция IDirectInputDevice8 SetProperty()

Реализация буферизованного ввода достаточно проста — достаточно установить свойство устройства клавиатуры. Это осуществляется с помощью функции установки свойств. Вот как выглядит ее прототип:
HRESULT SetProperty( REFGUID rguidProp, LPCDIPROPHEADER pdiph ); Первый параметр, rguidProp, является GUID того свойства устройства, которое вы хотите установить. Чтобы установить размер буфера устройства используйте значение DIPROP_BUFFERSIZE.
Второй параметр, pdiph, является структурой данных, содержащей информацию о создаваемом буфере. Тип этой структуры данных — DIPROPDWORD. В коде я заполняю эту структуру данных нулями и устанавливаю параметр, определяющий размер создаваемого буфера клавиатуры. Количество сохраняемых в буфере событий клавиатуры задает следующая строка кода:
dipdw.dwData = KEYBOARD_BUFFERSIZE; Поле dwData определяет максимальное количество сохраняемых в буфере событий клавиатуры. В рассматриваемом примере я использую значение 10. Вы можете поиграться с этой константой, чтобы подобрать более подходящее для вашей игры значение.

Функция iInitDirectInput()

Функция iInitDirectInput() — это мое собственное творение и я использую ее для создания главного объекта DirectInput. Код, используемый мной для создания упомянутого объекта должен выглядеть для вас очень знакомым, поскольку я уже описывал его в предыдущем разделе главы. Здесь я привожу полный код функции:
int iInitDirectInput(void) { HRESULT hReturn; // Не пытаться создать Direct Input, если он уже создан if(!pDI) { // Создаем объект DInput if(FAILED(hReturn = DirectInput8Create( g_hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL))) { return(INPUTERROR_NODI); } } else { return(INPUTERROR_DI_EXISTS); } return(INPUTERROR_SUCCESS); } В приведенном выше коде я сперва проверяю, существует ли объект DirectInput. Если да, мне не надо создавать еще один объект. В этом случае функция возвращает код ошибки, говорящий вызывающей программе, что объект уже создан.
В следующем блоке кода выполняется вызов функции DirectInput8Create() для создания объекта DirectInput. Как только он будет успешно выполнен, моя функция возвращает WinMain() код успешного завершения. В результате этих действий глобальный указатель pDI будет содержать ссылку на созданный при вызове функции объект DirectInput.

Функция iInitKeyboard()

Теперь, когда у нас есть действующий объект ввода в форме глобального указателя pDI, можно создать интерфейс объекта клавиатуры. Здесь выходит на сцену моя функция iInitKeyboard(). В ней я создаю устройство клавиатуры, устанавливаю буфер клавиатуры, задаю режим доступа, захватываю клавиатуру и получаю раскладку клавиатуры. Вот как выглядит код функции:
int iInitKeyboard(HWND hWnd) { HRESULT hReturn = 0; DIPROPDWORD dipdw; // Не пытайтесь создать клавиатуру дважды if(pKeyboard) { return(INPUTERROR_KEYBOARDEXISTS); } // Выход, если не найден интерфейс DirectInput else if (!pDI) { return(INPUTERROR_NODI); } // Получаем интерфейс устройства системной клавиатуры if(FAILED(hReturn = pDI->CreateDevice( GUID_SysKeyboard, &pKeyboard, NULL))) { return(INPUTERROR_NOKEYBOARD); } // Создаем буфер для хранения данных клавиатуры ZeroMemory(&dipdw, sizeof(DIPROPDWORD)); dipdw.diph.dwSize = sizeof(DIPROPDWORD); dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); dipdw.diph.dwObj = 0; dipdw.diph.dwHow = DIPH_DEVICE; dipdw.dwData = KEYBOARD_BUFFERSIZE; // Устанавливаем размер буфера if(FAILED(hReturn = pKeyboard->SetProperty( DIPROP_BUFFERSIZE, &dipdw.diph))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем формат данных клавиатуры if(FAILED(hReturn = pKeyboard->SetDataFormat( &c_dfDIKeyboard))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем уровень кооперации для монопольного доступа if(FAILED(hReturn = pKeyboard->SetCooperativeLevel( hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND ))) { return(INPUTERROR_NOKEYBOARD); } // Захватываем устройство клавиатуры pKeyboard->Acquire(); // Получаем раскладку клавиатуры g_Layout = GetKeyboardLayout(0); return(INPUTERROR_SUCCESS); } Гм-м — многовато кода для простой инициализации клавиатуры, не так ли? В действительности все не так уж и плохо, если учесть чего мы с помощью этого кода достигнем.
Первая часть кода проверяет не проинициализирован ли уже указатель pKeyboard. Если да, объект клавиатуры уже создан ранее и функция возвращает код ошибки, извещающий нас об этом. В следующей проверке мы убеждаемся, что существует объект ввода pDI. Если инициализация DirectInput не выполнена, нет смысла пытаться создать объект клавиатуры!
Как только необходимые проверки успешно пройдены, я вызываю функцию CreateDevice() для создания устройства клавиатуры. Ранее я уже описывал эту функцию, так что код должен выглядеть для вас очень знакомо.

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

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


Функция iReadKeyboard()

Вместо того, чтобы одним махом показать вам весь код функции, я разделил его на небольшие кусочки. Вот первый фрагмент функции iReadKeyboard():
if(!pKeyboard || !pDI) { return(INPUTERROR_NOKEYBOARD); } Этот маленький фрагмент кода проверяет существуют ли объекты клавиатуры и DirectInput. Если какого-нибудь из них нет, функция возвращает код ошибки. Пришло время следующего фрагмента:
hr = pKeyboard->GetDeviceData( sizeof(DIDEVICEOBJECTDATA), didKeyboardBuffer, &dwItems, 0); Вызов функции получения данных от устройства возвращает любые данные, находящиеся в буфере устройства ввода. В данном случае возвращается буфер клавиатуры. Переменная dwItems будет содержать количество возвращенных элементов, а сами они будут помещены в буфер didKeyboardBuffer. Переменная hr сохраняет код завершения, возвращаемый функцией получения данных от устройства. Логика проверки кода завершения выглядит следующим образом:
// Клавиатуа может быть потеряна, захватить устройство снова if(FAILED(hr)) { pKeyboard->Acquire(); return(INPUTERROR_SUCCESS); } Если переменная hr содержит код ошибки, это может быть вызвано тем, что клавиатура потеряна из-за сворачивания окна или каких-нибудь других действий. В этом случае нужно повторно захватить клавиатуру с помощью функции захвата устройства.
Если мы без ошибок прошли все предыдущие этапы, настало время в цикле получить данные от устройства и заполнить ими глобальный буфер клавиатуры. Соответствующий код представлен ниже:
// Если есть данные, обработаем их if (dwItems) { // Обработка данных for(dwCurBuffer = 0; dwCurBuffer < dwItems; dwCurBuffer++) { // Преобразование скан-кода в код ASCII byteASCII = Scan2Ascii( didKeyboardBuffer[dwCurBuffer].dwOfs); // Указываем, что клавиша нажата if(didKeyboardBuffer[dwCurBuffer].dwData & 0x80) { ascKeys[byteASCII][dwCurBuffer] = 1; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 1; } // Указываем, что клавиша отпущена else { ascKeys[byteASCII][dwCurBuffer] = 0; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 0; } } } Код проверяет были ли возвращены какие-нибудь данные функцией получения данных от устройства. Если да, код в цикле перебирает возвращенные элементы в буфере и сохраняет результаты в глобальных массивах diks и ascKeys.

Функция LoadMap()

Функция загрузки карты работает во многом так же, как и функция сохранения карты. Тем не менее, в ней есть ряд ключевых отличий. Во-первых, функция GetSaveFileName() заменена на функцию GetOpenFileName(). Я не знаю, почему есть две различных функции, выполняющих одинаковые действия, но кто я такой, чтобы подвергать сомнению установленный порядок вещей? Так или иначе, но функция получает имя открываемого файла и заносит его в указанную строку. Убедившись, что указанный файл существует, код открывает его, и загружает содержимое в глобальный массив карты. После завершения работы воспроизводится звуковой сигнал, оповещающий об успешной загрузке файла.
Если вы этого еще не сделали, запустите программу D3D_MapEditorPlus и щелкните по кнопке Load. Загрузите файл с именем TileMap.dat и вы увидите рисунок, выполненный моей рукой из песка.
Это все основные сведения о загрузке и сохранении блочных карт. С демонстрацией!


Функция SaveMap()

Чтобы сохранить карту вам необходимо сохранить все содержимое массива блоков g_iTileMap. Поскольку массив представляет собой одну непрерывную область памяти, операция достаточно проста. Фактически, вы должны создать файл для сохранения данных, записать в этот файл содержимое массива блочной карты, после чего закрыть файл. Для большего удобства я добавил функциональность, предоставляющую пользователю диалоговое окно выбора файла. Оно позволяет выбрать имя файла данных с помощью простого и интуитивно понятного интерфейса. Вот как выглядит код, который я использую для сохранения карты:
void vSaveMap(void) { FILE *fp; int iRet; OPENFILENAME fileStruct; char szFileName[512]; char szFilter[32]; char szExtension[32]; // Очищаем буфер для получения имени файла memset(szFileName, 0x00, 512); // Создаем фильтр имен файлов memset(szFilter, 0x00, 32); strcpy(szFilter, "*.dat"); // Указываем расширение имени файла memset(szExtension, 0x00, 32); strcpy(szExtension, "dat"); // Создаем структуру диалога выбора файла memset(&fileStruct, 0x00, sizeof(OPENFILENAME)); // Инициализируем структуру fileStruct.hInstance = g_hInstance; fileStruct.hwndOwner = g_hWnd; fileStruct.lpstrDefExt = szExtension; fileStruct.lpstrFileTitle = szFileName; fileStruct.lpstrFilter = szFilter; fileStruct.nMaxFileTitle = 512; fileStruct.lStructSize = sizeof(OPENFILENAME); // Получаем имя файла iRet = GetSaveFileName(&fileStruct); // Выходим в случае ошибки if(!iRet) { return; } // Открываем файл fp = fopen(szFileName, "wb"); // Возвращаемся, если не можем открыть файл if(fp == NULL) { return; } // Сохраняем буфер блочной карты fwrite(g_iTileMap, 10000, sizeof(int), fp); // Закрываем файл fclose(fp); // Воспроизводим звук, сообщающий о завершении действия PlaySound("bleep.wav",NULL,SND_FILENAME|SND_ASYNC); } В первой части кода выполняется инициализация структуры данных OPENFILENAME, необходимой для функции GetSaveFileName(). Функция GetSaveFileName() является частью Microsoft Visual C++ SDK и предоставляет все необходимое для создания диалогового окна сохранения файла.
СОВЕТ Предоставляемое Microsoft диалоговое окно делает получение имени файла очень простой задачей. Я рекомендую вам применять его в своих программах. После того, как вызванная функция возвратила имя файла, откройте файл и сохраните в нем содержимое массива карты. Здесь можно воспользоваться функцией fwrite(), которая замечательно справится с этой работой.
Поднимающийся вверх должен будет спуститься вниз и точно так же открытое должно быть закрыто. В согласии с этой политикой я вызываю функцию fclose() чтобы закрыть открытый файл с данными карты. Вы можете спросить, зачем выполнив все действия я выполняю воспроизведение звука. Это поможет пользователю понять, что сохранение карты успешно завершено. Данное действие не является необходимым, но это признак хорошего стиля.
ПРИМЕЧАНИЕ Если вы введете имя файла в поле Save As и не выберете расширение, файл будет сохранен без расширения. Убедитесь, что вы сохраняете карты в файлы с расширением .dat, если не хотите столкнуться с проблемами во время их загрузки. Загрузите редактор карт и поиграйте с ним, редактируя и сохраняя карты. Это предоставит вам материал для экспериментов с загрузкой карт.

Функция смены слоя

Для поддержки переключения слоев я также добавил в редактор новую функцию. Вот ее прототип:
void vChangeLayer(int iLayer); Функция переключения слоя получает в своем параметре новый номер слоя и устанавливает переменные программы таким образом, чтобы этот слой стал активным. Кроме того функция реализует эффекты для графического интерфейса пользователя, отражающие переключение слоев блоков.

Функция vChangeLayer()

Когда нажимается любая кнопка выбора слоя, осуществляется вызов функции vChangeLayer(). Она уничтожает все кнопки слоев, создает их заново, устанавливая каждую в состояние по умолчанию, и затем создает кнопку активного слоя с черной рамкой вокруг. Вот как выглядит код, выполняющий эти действия:
void vChangeLayer(int iLayer) { // Уничтожение кнопок слоев DestroyWindow(hBUTTON_LAYER1); DestroyWindow(hBUTTON_LAYER2); DestroyWindow(hBUTTON_LAYER3); DestroyWindow(hBUTTON_LAYER4); // Установка кнопок в состояние по умолчанию hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, g_hInstance, NULL); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, g_hInstance, NULL); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, g_hInstance, NULL); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, g_hInstance, NULL); // Активация требуемой кнопки if(iLayer == 1) { DestroyWindow(hBUTTON_LAYER1); hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, g_hInstance, NULL); } else if(iLayer == 2) { DestroyWindow(hBUTTON_LAYER2); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, g_hInstance, NULL); } else if(iLayer == 3) { DestroyWindow(hBUTTON_LAYER3); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, g_hInstance, NULL); } else if(iLayer == 4) { DestroyWindow(hBUTTON_LAYER4); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, g_hInstance, NULL); } // Установка текущего слоя g_iCurLayer = (iLayer - 1); PlaySound("button.wav", NULL, SND_FILENAME|SND_ASYNC); } Возьмем к примеру кнопку слоя с номером 2. Когда вы щелкаете по ней, выполняется вызов функции и значение ее параметра iLayer равно 2. Функция уничтожает кнопки слоев, а затем создает снова без черной рамки вокруг. Затем функция проверяет, на какой слой указывает параметр iLayer. Она доходит до второй проверки и вновь уничтожает кнопку второго слоя. Затем кнопка создается вновь, но уже с черной рамкой вокруг, показывающей, что данный слой активен. В самом конце кода функции переменной g_iCurLayer также присваивается значение, соответствующее активному слою.

Функция vCreateMinimap()

Функция создания мини-карты вызывается приложением только один раз во время инициализации. Функция создает окно для мини-карты и размещает его в нижнем левом углу главного окна редактора. При вычислении координат этого окна я учитываю местоположение и размер главного окна редактора и размер мини-карты. Вот код, который создает окно:
// Создание окна мини-карты hWndMinimap = CreateWindowEx( WS_EX_LEFT|WS_EX_TOPMOST|WS_EX_TOOLWINDOW, "Minimap", "Minimap", WS_BORDER | WS_VISIBLE | WS_MINIMIZEBOX, rcWindow.left + 10, rcWindow.bottom + g_iYOffset - 140, 100, 100, hwnd, NULL, hinst, NULL); Приведенный выше код не слишком гибок, поскольку я жестко запрограммировал его для мини-карты размером 100 х 100 точек. Впрочем, ничто не мешает вам изменить эти значения, чтобы приспособить окно для отображения мини-карты другого размера.
Теперь, когда у вас есть окно мини-карты, необходим код, который будет отображать саму мини-карту. Этим занимается функция vRenderMinimap().

Функция vDrawUnit()

Теперь, когда у вас есть буфер вершин для подразделения, необходимо место для выполнения визуализации. Здесь выходит на сцену функция рисования подразделения. Она работает точно так же, как функция рисования блока игрового поля, которую я показывал в главе 5, за исключением некоторых добавленных функций. Вот ее код:
void CD3DFramework::vDrawUnit( float fXPos, float fYPos, float fXSize, float fYSize, float fRot, CUnitAnimation *animObj, int iTexture, int iOwner) { D3DXMATRIX matWorld; D3DXMATRIX matRotation; D3DXMATRIX matTranslation; D3DXMATRIX matScale; // Установка значений по умолчанию для // местоположения, масштабирования и вращения D3DXMatrixIdentity(&matTranslation); // Масштабирование блока D3DXMatrixScaling(&matScale, fXSize, fYSize, 1.0f); D3DXMatrixMultiply(&matTranslation,&matTranslation,&matScale); // Вращение блока D3DXMatrixRotationZ(&matRotation, (float)DegToRad(-fRot)); D3DXMatrixMultiply(&matWorld, &matTranslation, &matRotation); // Перемещение блока matWorld._41 = fXPos - 0.5f; // X-Pos matWorld._42 = fYPos + 0.5f; // Y-Pos // Установка матрицы m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); // Используем буфер вершин блока m_pd3dDevice->SetStreamSource( 0, m_pVBUnit, 0, sizeof(TILEVERTEX)); // Используем фрмат вершин блока m_pd3dDevice->SetFVF(D3DFVF_TILEVERTEX); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture + iOwner + 1].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Разыменовываем текстуру m_pd3dDevice->SetTexture(0, NULL); } Первое отличие данной функции от ранее рассмотренной vDrawTile() — добавление параметра вращения. Он позволяет вам развернуть двухмерное изображение на любой необходимый угол. Само вращение реализуется перемножением матриц поворота и преобразования. Матрица поворота создается вспомогательной функцией DirectX D3DXMatrixRotationZ().
СОВЕТ В DirectX угол поворота всегда вычисляется в радианах. Для преобразования угловых величин в радианы я использую макрос DegToRad(). Вам в ваших программах также следует использовать аналогичную функцию или вращение графики и трехмерных объектов будет выполняться неправильно. Следующее отличие рассматриваемой функции заключается в том, что я использую буфер вершин m_pVBUnit вместо буфера m_pVBTile. Это сделано для того, чтобы базовая точка располагалась в центре, о чем я говорил ранее.
Самое большое отличие данной функции — добавление параметра CUnitAnimation. Он сообщает функции откуда она должна брать текстуры. Указатель на класс анимации необходим потому, что именно в нем хранятся используемые для визуализации текстуры.
Я задаю используемую для визуализации текстуру путем передачи в функцию параметра, указывающего ее местоположение во внутреннем массиве класса анимации. В результате отображается базовое изображение подразделения. В следующием вызове функции визуализации изменена позиция в массиве для того, чтобы отображались цвета владельца. Если вы помните, раньше я говорил о том, что цвета владельца хранятся следом за кадром анимации. Данные о цветах накладываются на базовое изображение подразделения, чтобы в результате отображалось изображение подразделения, раскрашенного в цвета его владельца.

Функция vGenerateMap()

Структура этой программы редактирования карт практически полностью повторяет структуру предыдущего примера. Главные отличия сконцентрированы в функции генерации карты. Ее цель — генерировать случайно расположенные участки суши, базируясь на передаваемом в качестве параметра номере используемого алгоритма. В рассматриваемом примере я реализовал только один тип алгоритма генерации карты, но ничто не мешает вам добавить и другие! Вот как выглядит код функции:
void vGenerateMap(int iType) { int iRandDirection; int iSeedPos[32]; int i, j; int iNumSeeds = 32; int iNumUpdates = 800; // -- ТИП 0 -- Случайные семена if(iType == 0) { // Очиска карты vInitMap(); // Создание случайно расположенных начальных семян for(i = 0; i < iNumSeeds; i++) { // Установка начальной позиции семени iSeedPos[i] = rand() % (g_iMapHeight * g_iMapWidth); // Помещаем в позицию семени блок с изображением травы g_iTileMap[iSeedPos[i]] = 17; } // Перемещение семени for(i = 0; i < iNumUpdates; i++) { for(j = 0; j < iNumSeeds; j++) { iRandDirection = rand()%4; // перемещаем семя вверх if(iRandDirection == 0) { iSeedPos[j] -= g_iMapWidth; } // Перемещаем семя вправо else if(iRandDirection == 1) { iSeedPos[j]++; } // Перемещаем семя вниз else if(iRandDirection == 2) { iSeedPos[j] += g_iMapWidth; } // Перемещаем семя влево else if(iRandDirection == 3) { iSeedPos[j]--; } // Если семя вышло за пределы карты, // помещаем его в случайную позицию if(iSeedPos[j] < 0 || iSeedPos[j] >= (g_iMapHeight * g_iMapWidth)) { iSeedPos[j] = rand() % (g_iMapHeight * g_iMapWidth); } // Помещаем в позицию семени блок с изображением травы g_iTileMap[iSeedPos[j]] = 17; } } } // Отображение мини-карты vRenderMinimap(); // Воспроизведение звука, сообщающего о завершении операции PlaySound("bleep.wav",NULL,SND_FILENAME|SND_ASYNC); } Код может выглядеть устрашающе, так что сперва взгляните на Рисунок 10.14, где показан ход выполнения функции.


Функция vRender()

Сейчас у вас есть буфер вершин для подразделения и функция, помогающая при визуализации. Белым пятном остается место, где создается изображение каждого кадра. Встречайте старого знакомого — функцию vRender().
Функция визуализации в программе D3DFrame_UnitTemplate работает во многом так же, как и одноименная функция из программы D3DFrame_2DTiles. В первой части выполняется визуализация карты посредством цикла, в котором перебираются и отображаются отдельные блоки карты. На этом подобие заканчивается.

Функция vRenderMinimap()

Функция визуализации мини-карты работает точно так же как основная функция визуализации, перебирая в цикле блоки карты и выводя для каждого из них соответствующее изображение. Главное отличие заключается в том, что функция визуализации мини-карты проходит в цикле по всей карте, а не только по небольшой ее области, и, кроме того, при визуализации масштабирует изображение каждого блока до размера 1 х 1 пиксел. Вот код, выполняющий эту, достойную Геракла работу:
void vRenderMinimap(void) { RECT rectSrc; RECT rectDest; int iX; int iY; int iCurTile; int iBufferPos; // Очистить вторичный буфер, заполнив его синим цветом g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); // Начинаем отображение сцены g_pd3dDevice->BeginScene(); // Визуализация мини-карты // Сверху вниз for(iY = 0; iY < g_iMapHeight; iY++) { // Справа налево for(iX = 0; iX < g_iMapWidth; iX++) { // Вычисление смещения в буфере iBufferPos = iX + (iY * g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos]; // Отображаем блок vDrawInterfaceObject((iX), (iY), (float)1, (float)1, iCurTile); } } // Завершаем сцену g_pd3dDevice->EndScene(); // Исходный прямоугольник rectSrc.top = 0; rectSrc.bottom = g_iMapHeight; rectSrc.left = 0; rectSrc.right = g_iMapWidth; // Прямоугольник места назначения rectDest.top = 0; rectDest.bottom = g_iMapHeight; rectDest.left = 0; rectDest.right = g_iMapWidth; // Представляем результат g_pd3dDevice->Present(&rectSrc, &rectDest, hWndMinimap, NULL); } Я отметил наиболее интересные части функции полужирным курсивом. Обратите внимание, что при выводе я устанавливаю размер каждого блока равным 1 х 1 пикселу. Благодаря этому при отображении мини-карты происходит масштабирование каждого блока до размеров единственной точки. Важно заметить, что при этом точка представляет общий цвет блока, поскольку это масштабированное представление, а не замещающая текстура. Что такое замещающая текстура? Один из методов рисования мини-карты заключается в том, что каждому типу блоков назначается представляющий его цвет. Например, вода может изображаться квадратами синего цвета, земля — квадратами зеленого цветаа постройки — черными квадратами. В этом случае не надо выполнять масштабирование блока, а достаточно заменить блок на точку с цветом, соттветствующим функциональному назначению данного блока в игре. Лично я предпочитаю метод масштабирования, поскольку он позволяет получить более точное представление карты и обходится без дополнительного кода для замещения текстур.
СОВЕТ Если вы хотите получить мини-карту более крупного масштаба, надо только изменить размер окна мини-карты и размер блоков в цикле визуализации. Побробуйте сделать это!

Генерация случайной карты

Функция vInitMap() отвечает за создание случайной карты. Взгляните как выглядит код, выполняющий эти действия:
void vInitMap(void) { int i; // Заполнение карты случайными блоками for(i = 0; i < g_iMapWidth * g_iMapHeight; i++) { g_iTileMap[i] = rand()%3; } } Возможно вы думаете «Что мне может дать случайное заполнение карты?». Гораздо больше, чем вы могли предположить. Хотя вы будуте редактировать почти каждый блок на игровом поле, случайный набор блоков является хорошей отправной точкой, благодаря которой карта будет выглядеть естественно. Скорее всего, вы не захотите вручную размещать каждый камень, куст или ягоду на нескольких десятках карт. Сначала это может быть забавно, но очень быстро вы устанете.
В коде видно, как я просматриваю весь буфер карты и присваиваю каждому блоку случайное значение в диапазоне от 0 до 2. В результате карта будет похожа на мешанину. Но есть несколько вещей, которые вы можете реализовать здесь. Например, вы можете задать параметры распределения случайных блоков. Пусть 10 процентов карты беспорядочно заполняются камнями, а 60 процентов — водой. Тпкие игры, как SimCity 4 и Civilization используют подобный метод при создании собственных карт. Позднее я подробнее рассмотрю автоматическую генерацию карт, так что пока прекратим вешать лапшу на уши.

Глобальные переменные для просмотра карты


Глобальные переменные для просмотра карты


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

Глобальные переменные карты

В заголовочном файле 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]; Первая переменная, g_iTileSize, сообщает программе просмотра карты сколько точек в ширину и в высоту занимают используемые блоки. Я присваиваю ей значение 32, следовательно ширина и высота моих блоков будут равны 32 точкам.
Вторая переменная, g_iTilesWide, сообшает программе сколько блоков должно помещаться в окне просмотра по горизонтали. Поскольку ширина используемого мной окна равна 640 точкам, а ширина блока равна 32 точкам, я присваиваю этой переменной значение 20, чтобы карта занимала все окно.
Третья переменная, g_iTilesHigh, работает точно так же, как g_iTilesWide, за исключением того, что задает количество блоков в окне по вертикали. Высота области просмотра равна 480 точкам, так что 15 блоков замечательно заполнят ее.
Четвертая переменная, g_iMapWidth, сообщает программе сколько блоков в карте по оси X. Поскольку программа просмотра может прокручивать карту, последняя может быть больше, чем область просмотра. Я задаю здесь значение 100, чего должно быть вполне достаточно для демонстрации прокрутки.
Пятая переменная, g_iMapHeight, работает точно так же как и предыдущее поле, за исключением того, что задает количество блоков в карте по оси Y. Ей я также присваиваю значение 100, чтобы карта была квадратной.
Шестая переменная, g_iXPos, сообщает программе просмотра в каком месте по оси X расположена область просмотра. Поскольку карта по размерам больше чем окно просмотра, программа должна отслеживать положение окна просмотра на карте. Это число не может быть отрицательным, поскольку отрицательные координаты располагаются за пределами карты.
Седьмая переменная, g_iYPos, задает вторую координату местоположения окна просмотра на карте.
Восьмая переменная, g_iTileMap, представляет собой массив целых чисел, описывающий блочную карту. В нем хранятся номера всех блоков, отображаемых на карте. Поскольку ширина и высота нашей карты равны 100 блокам, я создаю массив размером 10 000 элементов.
Назначение этих переменных и их значения представлены на Рисунок 10.4.


В заголовочном файле проекта main.h появилось несколько новых членов данных, необходмых для редактирования. Вот они в порядке их появления:
int g_iCurTile = 0; int g_iCurTileSet = 0; int g_iMaxTileSet = 3; int g_iTotalTiles = 18; Первая переменная, g_iCurTile, сообщает редактору какой именно блок выбран пользователем для рисования в данный момент. Когда пользователь редактирует карту, на нее будет помещаться именно этот блок.
Следующая переменная, g_iCurTileSet, сообщает редактору какая страница набора блоков отображается на панели инструментов. Данная переменная необходима для навигации по страницам набора блоков, поскольку у вас может быть больше блоков, чем одновременно можно отобразить на панели инструментов. В рассматриваемом редакторе карт есть только одна страница блоков, но в нем реализована поддержка нескольких страниц на тот случай, если вы захотите увеличить количество блоков.
Далее идет переменная g_iMaxTileSet. Она сообщает системе сколько страниц может быть в наборе блоков. Фактически вы можете указать здесь сколь угодно большое число. Я использую его лишь для того, чтобы уберечь пользователя от погони за горизонтом.
Последний элемент, g_iTotalTiles, сообщает программе сколько блоков загружено в память. Это очень важная информация, поскольку она позволяет предотвратить выбор пользователем отсутствующих блоков, что может привести к краху программы. Я загружаю 18 блоков, но вы можете увеличить количество загружаемых блоков в соответствии с вашими потребностями, увеличив значение переменной.

Характеристики частиц

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


  • Ход исполнения программы D3D_MapEditorPlusGold


    Ход исполнения программы D3D_MapEditorPlusGold


    На Рисунок 10.12 видно, как программа инициализирует DirectInput, клавиатуру, DirectGraphics, объекты интерфейса, блочную карту, панель инструментов и, наконец, окно мини-карты. Поскольку очень удобно, когда мини-карта размещается в собственном перемещаемом окне, я создаю отдельное окно специально для этой цели. Это делает функция vCreateMinimap().

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

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


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

    Ход выполнения функции проверки входных данных


    Ход выполнения функции проверки входных данных


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

    Ход выполнения функции vGenerateMap()


    Ход выполнения функции vGenerateMap()


    В самом начале код равномерно разбрасывает по карте семена будущего ландшафта. Эти семена являются стартовыми точками будущих континентов, которые создаст код. В данном алгоритме не используется никаких шаблонов — семена размещаются абсолютно случайным образом. Справа от процедуры размещения семян показано, как будет выглядеть получившаяся в результате карта. Это не слишком впечатляюще, поскольку код лишь поместил блоки с изображением травы в стартовые позиции каждого семени.
    Как только семена размещены на карте, код в цикле указанное число раз перебирает каждое из семян и случайным образом смещает его на один квадрат влево, вправо, вверх или вниз. В новую позицию семени также помещается блок с изображением травы. Вот так, медленно но верно, данный метод заполняет карту случайно расположенными блоками с изображением травы. На Рисунок 10.14 справа от процедур перемещения семени изображено, как карта постепенно обретает форму.
    Есть еще пара вещей, отсутствующих на иллюстрации. Во-первых, перед началом создания ландшафта я вызываю функцию vInitMap() для очистки карты. Это требуется для того, чтобы перед созданием случайно расположенных блоков с изображением травы массив карты был приведен в исходное состояние. Во-вторых, на иллюстрации отсутствует проверка, гарантирующая, что семя не будет блуждать за пределами карты. Если семя выходит за границы карты, оно помещается в случайную позицию внутри карты и продолжает формировать изображение суши в новом месте. И последняя отсутствующая вещь — вызов функции отображения миникарты, показывающей обновленный ландшафт.

    Ход выполнения функции визуализации


    Ход выполнения функции визуализации


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

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


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


    Обратите внимание, как функция vInitPathing() использует при вычислении пути объект класса CPathFinder. Кроме того, на рисунке изображена функция iGetMapCost(), которая вычисляет базовую стоимость для данного узла карты. Вот ее код:
    int iGetMapCost(int iX, int iY) { // Узел непроходим, если находится вне горизонтальных границ карты if(iX < 0 || iX >= g_iTilesWide) return(-1); // Узел непроходим, если находится вне вертикальных границ карты if(iY < 0 || iY >= g_iTilesHigh) return(-1); // Узел непроходим, если номер блока карты отличается от 0 if(g_iTileMap[iX + (iY * g_iMapWidth)][1] != 0) { return(-1); } // Для всех остальных случаев возвращаем стоимость блока else { return(g_iTileMap[iX + (iY * g_iMapWidth)][1]); } } Функция получения стоимости узла карты принимает в параметрах пару координат и возвращает значение, зависящее от того, какой блок находится в точке с указанными координатами. Если точка с заданными координатами находится вне карты, функция возвращает –1. Если в указанной точке находится блок, через который нельзя пройти, функция также возвратит –1. И только когда точка с заданными координатами находится в пределах карты и через соответствующий блок можно передвигаться, функция вернет его стоимость.
    Как я говорил раньше, функция vInitPathing() использует функцию получения стоимости узла карты при обращении к объекту поиска пути. Вот код функции инициализации пути:
    void vInitPathing(void) { bool bRet; int iTempX; int iTempY; int iDir; // Начальная и конечная позиции на карте int iNodeStartX; int iNodeStartY; int iNodeEndX; int iNodeEndY; // Таймеры DWORD dwStartTime; DWORD dwTotalTime; // Объект класса пути CPathFinder pathMyPath; // Очистить карту со стрелками // Она используется в дальнейшем для отображения пути for(int i = 0; i < g_iMapWidth * g_iMapHeight; i++) { g_iArrowMap[i] = -1; } // Ищем на карте исходный пункт for(int y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 19) { g_iRabbitXPos = x; g_iRabbitYPos = y; // Сохраняем исходное состояние iNodeStartX = g_iRabbitXPos; iNodeStartY = g_iRabbitYPos; break; } } } // Ищем на карте конечный пункт for(y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 20) { iNodeEndX = x; iNodeEndY = y; break; } } } // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "CALCULATING PATH"); vRender(); // Задаем функцию получения стоимости pathMyPath.vSetCostFunction(iGetMapCost); // Запуск таймера dwStartTime = timeGetTime(); // Задаем начальную и конечную позиции pathMyPath.vSetStartState(iNodeStartX, iNodeStartY, iNodeEndX, iNodeEndY); // Ищем путь - максимальная длина 300 узлов bRet = pathMyPath.bFindPath(300); // Остановка таймера dwTotalTime = timeGetTime() - dwStartTime; // Выход в случае сбоя if(!bRet) { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "FAILED, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); return; } else { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "COMPLETE, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); } // Теперь следуем по пути CPathNode *GoalNode = pathMyPath.m_CompletePath->m_Path[0]; int iTotalNodes = 0; // Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY; // Старт из позиции 1, а не 0 iTotalNodes++; GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes]; // Перебираем в цикле составляющие путь узлы // Для каждого шага рисуем стрелку while(iTotalNodes < pathMyPath.m_CompletePath->m_iNumNodes) { // Определяем направление стрелки iDir = vFindDirection(iTempX, iTempY, GoalNode->m_iX, GoalNode->m_iY); // Сохраняем стрелку в карте стрелок g_iArrowMap[GoalNode->m_iX + (GoalNode->m_iY * g_iMapWidth)] = iDir; // Визуализируем сцену vRender(); // Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY; // Увеличиваем счетчик узлов iTotalNodes++; // Получаем следующий узел GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes]; }; } Гм-м — придется просмотреть весьма много кода. Я знаю, что код выглядит сложно, но большая его часть предназначена для отображения стрелок, представляющих найденный путь. В первой части кода определяется где на карте кролик находится сначала и где он должен оказаться в конце. Поскольку начало и конец пути представлены на карте специальными блоками, код просто ищет их и сохраняет координаты их местоположения.
    После того, как начальная и конечная точки маршрута обнаружены код передает в класс поска пути указатель на функцию получения стоимости узла карты. Это делается для того, чтобы класс поиска пути знал как вычислить наилучший из возможных путей, основываясь на базовой стоимости блоков ландшафта. После того, как задана функция получения стоимости узла карты, код устанавливает начальный и конечный пункты маршрута в объекте поиска пути. Это действие сообщает объекту между какими двумя пунктами ему следует проложить маршрут.
    Все самое интересное происходит когда программа вызывает принадлежащую объекту поиска пути функцию bFindPath(). Именно она выполняет работу по поиску наиболее эффективного пути на карте от начального до конечного пункта. Если путь найден, функция возвращает 1; если путь найти не удалось, функция возвращает 0.
    Чтобы отобразить найденный путь на экране программа перебирает в цикле все входящие в путь узлы карты, начиная с первого, пока не доберется до цели. Проходя по пути она отображает стрелки, чтобы показать путь по которому кролик добирается от одного узла к другому. Направление вычисляется на основании данных о местоположении предыдущего узла пути относительно текущего. Здесь вступает в игру функция vFindDirection(). Она очень простая, поскольку лишь вычисляет какую именно стрелку необходимо отображать.

    Ход выполнения процедуры отображения текста


    Ход выполнения процедуры отображения текста


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

    Ход выполнения программы D3D_Particles


    Ход выполнения программы D3D_Particles


    На Рисунок 13.4 видно, как функция WinMain() выполняет инициализацию системы, последовательно обращаясь к функциям InitD3D(), vInitInterfaceObjects() и vInitParticles(). С первыми двумя функциями мы уже встречались в предыдущих примерах, а вот функция инициазизации частиц новая и появляется в этом примере впервые. Ее цель — создание частиц для сцены и установка их атрибутов для анимации.

    Ход выполнения программы просмотра карт


    Ход выполнения программы просмотра карт


    На Рисунок 10.5 появилась только одна новая функция — vInitMap().

    Ход выполнения программы

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


    Ход выполнения программы

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


    Импорт данных из пяти различных


    Импорт данных из пяти различных


    На Рисунок 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.


    Инициализация частиц

    Код инициализации частиц выглядит следующим образом:
    void vInitParticles(void) { // Инициализация каждой частицы for(int i = 0; i < TOTAL_PARTICLES; i++) { // Установка последовательности анимации текстур g_partExplosion[i].vSetTextures( rand() % 3, // Тип анимации 0, // Начальная текстура 5, // Конечная текстура 10); // Пауза между сменой текстур // Установка начального местоположения g_partExplosion[i].vSetPos( 0.0f + (rand() % g_iWindowWidth), // X 0.0f + (rand() % g_iWindowHeight), // Y 0.0f); // Z // Установка начальной скорости g_partExplosion[i].vSetSpeed( -1.0f + rand() % 2, // X -8.0f + rand() % 4, // Y 0.0f); // Z // Установка гравитационного воздействия g_partExplosion[i].vSetGravity( 0.0f, // X 0.1f, // Y 0.0f); // Z // Установка длительности жизни частицы g_partExplosion[i].vSetLife(200); } } Функция в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы задается случайное местоположение и скорость. Затем задается гравитационное воздействие и, в самом конце, продолжительность жизни частицы устанавливается равной 200. Это сообщает системе, что частица будет существовать в течение 200 игровых тактов.
    При случайном размещении я помещаю частицы где-нибудь в пределах экрана. При генерации случайной скорости используется небольшой диапазон значений для скорости перемещения по горизонтали и больший диапазон — для перемещения по вертикали. В результате создается впечатление, что частицы летят по экрану снизу вверх.
    Тип анимации выбирается случайным образом, а диапазон используемых при анимации текстур для всех частиц будет от 0 до 5. Кроме того, я задаю паузу между сменой текстур, равной 10 для того, чтобы кадры анимации не сменяли друг друга слишком быстро.

    Инициализация DirectInput

    Откройте файл main.cpp и найдите код функции WinMain(). В ней вы найдете обычный код создания объектов Windows, за которым следует код инициализации DirectInput и устройства клавиатуры, выглядящий так:
    // Инициализация DirectInput iResult = iInitDirectInput(); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Direct Input.", MB_ICONERROR); vCleanup(); exit(1); } // Инициализация клавиатуры DI iResult = iInitKeyboard(hWnd); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Keyboard.", MB_ICONERROR); vCleanup(); exit(1); } В приведенном выше коде вызываются две функции: iInitDirectInput() и iInitKeyboard(). Вызов первой из них инициализирует главный объект DirectInput, а вызов второй создает устройство клавиатуры. Увидеть ход выполнения программы можно на Рисунок 9.3.


    Рабочей лошадкой DirectInput является интерфейс IDirectInput8. Это COM-объект, отвечающий за настройку среды ввода. После того, как вы создали объект DirectInput можно создавать устройства для объекта. Как же можно создать этот объект? С помощью следующего кода:

    if(FAILED(hReturn = DirectInput8Create( g_hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL))) { return(INPUTERROR_NODI); } Из приведенного кода видно, что объект DirectInput создает функция DirectInput8Create(). Вот как выглядит прототип этой функции:

    HRESULT WINAPI DirectInput8Create( HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID *ppvOut, LPUNKNOWN punkOuter ); В первом параметре, hinst, должен передаваться дескриптор текущего экземпляра вызывающего приложения. В приведенном выше фрагменте кода я передаю глобальный указатель экземпляра с именем g_hInstance. Он содержит экземпляр приложения и инициализируется в главной функции окна.

    Следующий параметр, dwVersion, содержит номер версии DirectInput, которую вы намереваетесь использовать. В приведенном выше примере я использую глобальную константу с именем DIRECTINPUT_VERSION. Ее значение равно 0x0800, и это значит, что мы намереваемся использовать DirectInput версии 8.

    ПРИМЕЧАНИЕ Хотя эта книга посвящена использованию DirectX 9, компонент DirectInput не изменялся с версии 8. Третий параметр, riidltf, передает уникальный идентификатор интерфейса. Для DirectX 8 и 9 вы должны использовать идентификатор IID_IDirectInput8.

    Четвертый параметр, ppvOut, содержит адрес указателя в котором будет сохранена ссылка на объект DirectInput. Для этого параметра я использую глобальный указатель с именем pDI. Тип указателя pDI — LPDIRECTINPUT8.

    Последний параметр, punkOuter, используется для указания на интерфейс IUnknown COM-объекта. Я всегда передаю в этом параметре значение NULL, и вы можете поступать так же.

    Если работа функции завершена успешно, она возвращает значение DI_OK.


    Объект DirectInput создает устройства в виде объектов интерфейса IDirectInputDevice8. Интерфейс IDirectInputDevice8 выполняет большую часть работы по поддержке конкретного устройства. Чтобы создать интерфейс устройства вы должны вызвать метод CreateDevice() главного объекта DirectInput. Вот как выглядит его прототип:

    HRESULT CreateDevice( REFGUID rguid, LPDIRECTINPUTDEVICE *lplpDirectInputDevice, LPUNKNOWN pUnkOuter ); В первом параметре, rguid, передается GUID создаваемого устройства. Для этой цели каждый тип устройств в DirectX имеет свой собственный GUID. Если вы хотите создать интерфейс клавиатуры, передайте в этом параметре идентификатор GUID_SysKeyboard. Чтобы создать интерфейс мыши, передайте идентификатор GUID_SysMouse.

    Второй параметр, lplpDirectInputDevice, представляет собой указатель на указатель на создаваемое новое устройство. В своих примерах я передаю указатель с именем pKeyboard типа LPDIRECTINPUTDEVICE8.

    Последний параметр применяется для COM, и большинство людей просто передают здесь NULL.

    В случае успешного завершения функция возвращает значение DI_OK.

    Интерфейс шрифта

    Возможно, рассматривая код визуализации вы заметили, что для отображения текста на экране я использую объект с именем pD3DXFont. Это экземпляр предоставляемого DirectX интерфейса ID3DXFont. Данный интерфейс очень полезен, так как выполняет все необходимое для отображения шрифтов в Direct3D. Вам надо лишь указать дескриптор шрифта и выводимый текст. Это действительно просто! Если вы взглянете на функцию инициализации объектов интерфейсов, то увидите следующий код:
    // Шрифт текста hFont = CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, PROOF_QUALITY, 0, "fixedsys"); D3DXCreateFont(g_pd3dDevice, hFont, &pD3DXFont); В первой строке вызывается системная функция CreateFont(). Она является частью системы GDI Windows и создаеет дескриптор шрифта, получая в качестве параметров имя шрифта, его размер и ряд других атрибутов. Подробные сведения об этой функции вы найдете в справочнике MSDN.
    После того, как вы получили дескриптор шрифта, остается только вызвать функцию D3DXCreateFont(). Эта функция получает указатель на устройство Direct3D, дескриптор шрифта и адрес указателя на объект ID3DXFont. В результате выполнения функции в указатель на объект ID3DXFont помещается ссылка на созданный интерфейс шрифта, который будет использоваться в дальнейшем при визуализации.
    В рассматриваемом примере я создаю моноширинный шрифт. Я люблю моноширинные шрифты, поскольку при их использовании очень просто вычислить длину создаваемой строки. В пропорциональных шрифтах ширина символов неодинакова. Это создает кучу проблем, особенно когда требуется отобразить курсор в конце текста!
    Вернемся к коду визуализации. Для отображения только что созданного шрифта я обращаюсь к функции интерфейса шрифта DrawText().

    Использование альфаканала

    Первые отличия кода функции проявляются в том месте, где я включаю альфа-смешивание. Это действие позволяет видеть блоки карты сквозь текстуры подразделений. Сам процесс достаточно прямолинеен и осуществляется путем изменения нескольких состояний визуализации. Вот как выглядит код:
    // Включение прозрачности m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE); m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); Первый вызов функции установки состояния визуализации сообщает системе визуализации DirectX о необходимости включить альфа-смешивание. Второй вызов сообщает системе о том, что будет выполняться смешивание текстуры подразделения с ее альфа-каналом. Последний вызов функции установки состояния визуализации сообщает системе что будет выполняться смешивание изображения-приемника с инвертированным альфа-каналом текстуры подразделения.

    Использование шаблонов для генерации случайного ландшафта


    Использование шаблонов для генерации случайного ландшафта


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


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

    Существует множество других вещей, которые следует учесть в вашем коде поиска пути. Поработайте над упомянутыми ниже моментами, чтобы превратить заготовку аггоритма поиска пути в готовое к выпуску изделие.
  • Выполняя поиск среди открытых узлов, начинайте его с тех узлов, которые расположены ближе к цели. Во многих случаях это поможет сократить количество узлов, которые требуется просмотреть при поиске пути.
  • Не помещайте другие подразделения на карту препятствий. Если вы поступите так, перемещения подразделений могут вызвать проблемы. Одно из возможных решений состоит в том, чтобы при поиске пути не учитывать другие подразделения и в случае столкновения с другим подразделением заново рассчитывать путь. Если подразделения не блокируют друг друга, это не требуется.
  • Используйте диспетчер путей для управления фиксированным пулом путей. Если вы используете пул путей, это поможет вам сократить нагрузку на процессор, вызванную необходимостью динамического формирования каждого пути.
  • Используйте многоуровневый поиск пути. Для этого применяйте крупномасштабную карту препятствий, каждый узел которой соответствует нескольким блокам обычной карты, что позволит маневрировать среди больших ландшафтных препятствий. Как только подразделение достигнет более сложной области, переключитесь на карту препятствий более мелкого масштаба с большим количеством узлов. Многоуровневый поиск пути может значительно ускорить работу кода.
  • Никогда не вычисляйте перемещение всех подразделений сразу. Создайте очередь путей и вычисляйте на каждом такте игры пути для небольшого числа подразделений.
  • Если в вашей игре разворот подразделений занимает какое-то время, добавьте к вычислениям общей стоимости каждого узла карты стоимость смены направления движения. Это позволит выбирать для подразделений наиболее эффективный путь с наименьшим количеством поворотов. Кроме того, вы можете сделать ряд других подобных дополнений к стоимости узла карты. Например, вы можете увеличить стоимость узлов, расположенных рядом со вражескими позициями, чтобы предотвратить случайное вступление ваших подразделений в бой!
  • Алгоритм А* работает на картах различных типов, в том числе и на тех, где форма блоков отличается от квадратной. Испытайте его на картах с шестиугольными блоками или даже на маршрутных картах.
  • собран специально для http://www.natahaus.ru/

    Мы уже достигли конца очередной

    Мы уже достигли конца очередной главы? Это произошло так быстро! Вот несколько секретов, которые мы узнали в этой главе:
  • Вот четыре блока, которые являются фунндаментом разработки подразделений: атака, защита, передвижение и анимация.
  • Базовые типы подразделений помогают сократить объем используемой памяти.
  • Базовые типы подразделений помогают организовывать подразделения в группы.
  • Диспетчер подразделений упрощает контроль за подразделениями.
  • Использование текстовых файлов для хранения данных подразделений поможет сделать вашу игру более гибкой.
  • Использование текстовых файлов для хранения данных подразделений поможет сделать вашу игру более гибкой.
  • исходный текст перевода взят с сайта netlib.narod.ru
    и собран специально для
    http://www.natahaus.ru/

    В этой главе я показал вам как реализовать в ваших играх ввод данных с клавиатуры. Есть множество доступных методов ввода, и я надеюсь, что вы возьмете показанное вам в этой главе за основу более гибких и функциональных систем. Перед тем, как продолжить чтение, обратите внимание на следующие моменты:
  • DirectInput предоставляет все необходимое для работы с клавиатурой в вашей игре. Он также предоставляет возможность работать с другими типами устройств ввода, необходимыми для игры.
  • Коды DIK — это внутренние коды клавиатуры, назначенные DirectInput.
  • С помощью DirectInput можно получить коды ASCII, но для этого вы должны сами преобразовывать коды DIK.
  • Буферизованный ввод необходим. Никогда не реализуйте методы ввода без буферизации.
  • Интерфейс ID3DXFont предоставляет вам мощные средства для отображения текста.
  • предоставляет вам мощные средства для отображения текста. исходный текст перевода взят с сайта netlib.narod.ru
    и собран специально для
    http://www.natahaus.ru/

    Эта глава познакомила вас с основами редактирования, просмотра и генерации карт, а также с методами отображения мини-карты. Вспомните следующие ключевые моменты:
  • Редактор карт является одной из наиболее важных частей вашего проекта. Если с вашим редактором карт сложно работать, создание карт для игры будет занимать очень много времени и игроки не захотят создавать свои собственные карты.
  • Кнопки интерфейса редактора карт хорошо размещать на панели инструментов. Сделайте панель инструментов перемещаемой, чтобы она не занимала ценное пространство области редактирования.
  • Мини-карта выглядит более реалистично, если при ее отображении вы масштабируете исходные блоки, а не используете цветовое представление блоков. Учтите, что это влияет на скорость, поскольку для масштабирования требуются дополнительные ресурсы системы визуализации.
  • Существует множество методов алгоритмической генерации карт. Попробуйте сперва продемонстрированный мной простейший метод, а затем усовершенствуйте его, пока не получите мощный генератор случайных ландшафтов.
  • Слои предоставляют вам большую свободу творчества при создании карт. Благодаря им вы сможете использовать перекрытия, полупрозрачные блоки и многие другие эффекты.


  • Я предоставил вам краткий обзор частиц с точки зрения их использования в разработке игр. Взяв его за основу вы можете создавать собственные реализации систем частиц. Есть сотни вещей, которые можно сделать при помощи частиц, и ограничивает вас в этом только собственное воображение. Если вы создадите замечательный пример использования частиц, сообщите мне об этом по электронной почте и я размещу его на своем сайте. Вот несколько моментов, заслуживающих вашего внимания:
  • Частицы — это мелкие фрагметы крупных объектов.
  • Хотя частицы небольшие, вы можете использовать для их анимации любые графические изображения.
  • Анимация текстур улучшит внешний вид ваших частиц и обеспечит дополнительную гибкость проекту.
  • Для управления частицами можно использовать класс системы частиц.
  • собран специально для http://www.natahaus.ru/

    Изменение частиц с течением времени


    Изменение частиц с течением времени


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


    Изменение процедур сохранения и загрузки

    Функции vSaveMap() и vLoadMap() модифицированы для включения в каждую карту информации о дополнительных слоях. Поскольку в примере поддерживается четыре слоя, будет сохраняться и записываться в четыре раза больше данных. Необходимые для этого изменения кода минимальны. Приведенная ниже строка показывает изменения, необходимые для функции vLoadMap():
    fread(g_iTileMap, 40000, sizeof(int), fp); Обратите внимание, что функция fread() считывает 40 000 целых чисел, а не 10 000, как раньше. Аналогичные изменения вносятся и в функцию vSaveMap():
    fwrite(g_iTileMap, 40000, sizeof(int), fp); В функции сохранения карты количество записываемых чисел также изменено с 10 000 на 40 000. Это единственное изменение, которое необходимо сделать в функции записи.
    ВНИМАНИЕ! Не пвтайтесь загружать карты, созданные одной версией редактора в другую версию. Это может привести к краху программы.

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

    Поскольку вам необходима возможность редактировать различные слои карты, требуется внести изменения в функцию vCheckMouse(). Вот как выглядит измененный код:
    g_iTileMap[iTileX+g_iXPos+ ((iTileY + g_iYPos) * g_iMapWidth)][g_iCurLayer] = g_iCurTile; В коде видно, что теперь номер блока помещается в многомерный массив g_iTileMap. Поскольку карта теперь состоит из нескольких слоев, для того, чтобы определить, в какой именно слой должен быть помещен блок, я использую переменную g_iCurLayer.
    В программе редактора карт есть и еще несколько изменений, но здесь я показал самые важные. Если вы этого еще не сделали, запустите программу и поиграйте с редактированием слоев, чтобы лучше разобраться с ним.


    Изменения в функции 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


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


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

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

    Методы класса используют только что описанные члены данных для того, чтобы устанавливать, перемещать и анимировать частицы во время их жизни в игре. Вот список методов с их кратким описанием:
    Функция CParticle() является конструктором класса и ее основная задача — очистить все члены данных, присвоив им значения по умолчанию.
    Функция ~CParticle() — это деструктор класса, и она освобожает занятую память, когда объект класса уничтожается.
    Функция vUpdate() вызывается на каждом такте игры и обновляет местоположение, скорость и состояние текстур частицы.
    Функция bIsAlive() сообщает вам жива еще частица или нет. Если она возвращает 0, значит частица уже уничтожена. Если она возвращает 1 — частица еще жива. Чтобы определить, какое значение возвращать, функция проверяет значение члена данных m_iLife.
    Функция vSetTextures() устанавливает информацию об анимации текстур, которая будет использоваться частицей.
    Функция vSetPos() устанавливает начальное местоположение частицы.
    Функция vSetAcceleration() устанавливает начальное ускорение частицы.
    Функция vSetGravity() задает гравитационное воздействие на частицу.
    Функция vSetSpeed() задает начальную скорость частицы.
    Функция vSetLife() устанавливает период жизни частицы.

    Миникарта

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


    Объекты 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)); Сперва в коде увеличивается номер текущего кадра ожидания. Это продвигает анимационную последовательность ожидания.
    Следом идет проверка, которая должна гарантировать что полученный номер кадра не превышает количество доступных кадров в анимационной последовательности ожидания. Вы же наверняка не хотите, чтобы изображение подразделения, на которое ссылается указатель на кадр анимации оказалось отсутствующим!
    И наконец я устанавливаю текущий кадр анимации для чего беру номер начального кадра анимации ожидания и прибавляю к нему текущее значение счетчика кадров ожидания, умноженное на количество поддерживаемых цветов владельца плюс единица. Гмм! Поскольку все данные анимационной графики подразделений хранятся в одном большом массиве, эти вычисления необходимы, чтобы получить требуемый кадр последовательности.

    Обработка перемещающихся подразделений

    Последний тип анимации относится к перемещающимся подразделениям. Код, который я поместил в пример выглядит очень похоже на обработку анимации других действий, но в нем отсутствует код для передвижения подразделения по экрану. Я удалил его чтобы максимально упростить пример, но абсолютно ничего не препятствует вам перемещать подразделения. Вот как выглядит код:
    ptrUnit->m_iCurMoveFrame++; if(ptrUnit->m_iCurMoveFrame >= ptrUnit->m_Animation->m_iNumMoveFrames) { ptrUnit->m_iCurMoveFrame = 0; } ptrUnit->m_iCurAnimFrame = ptrUnit->m_Animation->m_iStartMoveFrames + (ptrUnit->m_iCurMoveFrame * (UNITMANAGER_MAXOWNERS + 1)); Если вы хотите добавить код для передвижения, попытайтесь увеличивать координату Y подразделения, пока изображение не выйдет за границу экрана, а затем повторить цикл с низу экрана. Мне кажется, вы разочарованы? Вот код с добавленной логикой для передвижения:
    // Передвижение подразделения ptrUnit->m_fYPos += ptrUnit->m_Movement->m_fMovementSpeed; // Если вышли за верхнюю границу экрана, начинаем заново снизу if(ptrUnit->m_fYPos > 360.0f) ptrUnit->m_fYPos = -360.0f; Приведенный выше код увеличивает координату Y подразделения, пока оно не скроется из виду, а затем устанавливает координаты подразделения, чтобы оно появилось в нижней части экрана. Это очень простой вариант перемещения снизу вверх, но он открывает перед вами много возможностей.

    Обработка поворачивающих подразделений

    Второе действие представляет собой разворот подразделения. Вот предназначенный для этого код:
    // Поворот ptrUnit->m_fRot += ptrUnit->m_Movement->m_fTurnSpeed; // Сброс, если завершен полный разворот if(ptrUnit->m_fRot > 360.0f) ptrUnit->m_fRot -= 360.0f; В первой строке угол поворота подразделения увеличивается на величину, заданную в переменной, определяющей скорость поворота подрзделения. В результате изображение подразделения вращается в соответствии с заданными параметрами. Самое лучшее здесь то, что вы можете увеличивать или уменьшать частоту вращения просто регулируя скорость поворота.
    Следующая строка кода проверяет не превысил ли угол поворота величину 360 градусов. Если да, то из величины угла поворота вычитается 360 градусов. Благодаря этому значение угла никогда не станет слишком большим.
    Поскольку вращение происходит без изменения текстуры, текущий кадр анимации в этом коде не устанавливается.

    Обработка текстового ввода

    Теперь, когда система текстового ввода активизирована, вам необходимо обработать поступающие от клавиатуры данные и сохранить результат в буфере символов. Этим занимается функция vCheckInput(). Раньше я уже показывал вам часть функции обработки ввода, которая обрабатывает поступающие от мыши данные. Сейчас пришло время взглянуть на ту часть функции, которая обрабатывает данные клавиатуры. Вот ее код:
    // ВВОД С КЛАВИАТУРЫ // Чтение из буфера клавиатуры int iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Цикл перебора полученных данных for(int i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } // ЛОГИКА РАБОТЫ ТЕКСТОВОГО ВВОДА if(g_bTextInputActive) { // Не сохранять текст, если для него нет места в буфере if(g_shTextInputPosition < g_shTextMaxSize) { // Сохраняем отпущенные клавиши for(int j = 32; j < 123; j++) { // Проверяем, что введен допустимый символ if((j > 96) || (j == 32) || (j > 47 && j < 58)) { // Проверяем, что клавиша отпущена if(ascKeys[j][i]) { if(g_bShift) { g_szTextInputBuffer[ g_shTextInputPosition] = toupper(j); } else { g_szTextInputBuffer[g_shTextInputPosition] = j; } g_shTextInputPosition++; } } } } // Проверяем не нажата ли клавиша удаления символа if(diks[DIK_BACK][i]) { // Проверяем введен ли какой-нибудь текст if(g_shTextInputPosition) { // Удаляем последний символ g_szTextInputBuffer[g_shTextInputPosition - 1] = '\0'; // Сдвигаем курсор назад g_shTextInputPosition--; } } // Проверяем не нажата ли клавиша ENTER if(diks[DIK_RETURN][i]) { // Завершаем ввод имени g_bTextInputActive = 0; // АКТИВАЦИЯ НОВОЙ ИГРЫ if(g_iTextInputFieldID == GAMEINPUT_NAME) { // Делаем текущим основной игровой экран g_iCurrentScreen = 5; // Устанавливаем активные зоны vSetupMouseZones(5); } break; } } } } Это достаточно большой фрагмент кода, так что взгляните на Рисунок 9.10.


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

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


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

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


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


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


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


    Обзор DirectInput

    DirectInput — это часть DirectX, которая обрабатывает все формы ввода от игрока. Вы можете управлять мышью, клавиатурой, джойстиками, устройствами с обратной связью и многими другими типами устройств ввода. Для каждого типа контроллера имеется связанный с ним объект устройства. Для каждого объекта устройства вы создаете его экземпляр. Все это показано на Рисунок 9.1.


    Очки повреждений

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

    Окно программы D3D_MapEditorGeneration


    Окно программы D3D_MapEditorGeneration


    Обратите внимание на уже ставшие привычными элементы рисунка. Здесь есть панель инструментов, область редактирования и мини-карта. Новым элементом является появившаяся на панели инструментов кнопка Generate. Она очищает всю карту, заполняя ее водой, и создает случайно расположенные участки суши. Кроме того, вы можете заметить, что я увеличил размер мини-карты. Я сделал это для того, чтобы вы более ясно увидели эффект генерации случайной карты. Есть и еще одно изменение, которое нельзя заметить на Рисунок 10.13. Если вы запустите программу D3D_MapEditorGeneration, то увидите красный прямоугольник на мини-карте. Он показывает, какой именно фрагмент большой карты отображается в окне просмотра и помогает понять что именно вы сейчас редактируете.
    Теперь загрузите проект D3D_MapEditorGeneration и следуйте за мной, а я буду рассказывать вам о коде программы.

    Окно программы D3D_MapEditorLayers


    Окно программы D3D_MapEditorLayers


    На Рисунок 10.16 показано окно программы D3D_MapEditorLayers. На панели инструментов появились четыре новые кнопки, отмеченные цифрами от 1 до 4. Они позволяют установить активный слой, который будет редактироваться. Например, чтобы редактировать базовый слой, щелкните по кнопке 1. После того, как вы выбрали первый слой, все щелчки по области редактирования будут менять текстуры, выводящиеся в первом слое блоков. Кнопки, отвечающие за другие слои работают аналогично. В изображенном на иллюстрации окне редактирования вы видите маленький песчанный остров с травой в центре. Изображение песка находится в слое 1, а изображение травы — в слое 2. Благодаря этому достигается плавный переход от травы к песку без необходимости вводить специальные переходные блоки с изображением травы и песка. Хватит обсуждать иллюстрацию. Загрузите программу D3D_MapEditorLayers и следуйте вперед.

    Окно программы D3D_MapEditorLite


    Окно программы D3D_MapEditorLite


    На Рисунок 10.7 изображено окно программы D3D_MapEditorLite. Оно выглядит очень похоже на окно программы просмотра карт, о которой вы уже читали. Главным отличием данной программы является добавление области выбора блоков и возможность редактировать карту в реальном времени.
    Теперь загрузите проект D3D_MapEditorLite и следуйте дальше вместе со мной.

    Окно программы D3D_MapEditorPlus


    Окно программы D3D_MapEditorPlus


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

    Окно программы D3D_MapEditorPlusGold


    Окно программы D3D_MapEditorPlusGold


    Правильно, — это старый редактор карт, но теперь — золотое издание! (Я знаю, что название программы становится все более причуддливым, но по крайней мере я не называю ее MapEditor 2700+ или как-нибудь еще в этом роде!)
    На Рисунок 10.11 вы видите редактор карт, но теперь в нижнем левом углу есть небольшое окно в котором отображается мини-карта. В действительности это большая карта мира, на которой каждый блок представляется одной точкой. Это позволяет мне отобразить полную карту мира, размером 100 x 100 блоков в окне размером 100 x 100 точек. Загрузите проект D3D_MapEditorPlusGold и я покажу вам изменения, необходимые для реализации отображения мини-карты.
    Ход исполнения программы изменился не слишком сильно, но внесенные изменения надо проиллюстрировать. Пожалуйста, посмотрите на Рисунок 10.12, чтобы увидеть ход выполнения программы.


    Окно программы D3D_Particles


    Окно программы D3D_Particles


    Обратите внимание на разбросанные по экрану разноцветные частицы. Программа показывает как создать случайный набор частиц и подбрасывать их в воздух. Вы можете назвать это демонстрацией попкорна, я оставляю выбор за вами. Кажется я знаю, что вы думаете сейчас: «Какое это имеет отношение к программированию стратегических игр?». Ответ прост — предполагается, что программа покажет вам как можно быстро начать работать с частицами. Более сложные формирования, такие как взрывы и ударные волны, придут позднее, когда вы освоитесь с основными приемами использования частиц.
    Проект состоит из четырех файлов: main.cpp, main.h, CParticle.cpp и CParticle.h. Кроме того, для компиляции потребуются следующие библиотеки: d3d9.lib, dxguid.lib, d3dx9dt.lib и d3dxof.lib.

    Окно программы D3D_PathFinding


    Окно программы D3D_PathFinding


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

    Окно программы D3DFrame_UnitTemplate


    Окно программы D3DFrame_UnitTemplate


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


  • Окно программы DInput_Simple


    Окно программы DInput_Simple


    На Рисунок 9.2 изображено обычное окно с текстом, сообщающим, что для выхода из программы следует нажать на клавишу Esc. Вместо того, чтобы использовать для перехвата нажатия на клавишу Esc сообщения Windows, мы воспользуемся DirectInput и устройством клавиатуры. Теперь загрузите проект и следуйте за мной далее.
    Проект содержит два файла: main.cpp и main.h. В файле main.cpp находится код реализации функций, а в заголовочном файле main.h сосредоточена вся заголовочная информация. Проекту необходимы две библиотеки: dxguid.lib и dinput8.lib. Библиотека dxguid.lib содержит уникальные GUID для устройств DirectInput. В библиотеке dinput8.lib находятся сами функции DirectInput.

    Окно программы просмотра карты


    Окно программы просмотра карты


    На Рисунок 10.3 вы видите выглядящий знакомым набор блоков, отображенный в окне несколько большего размера. Отличие этой блочной карты от примеров из главы 5 заключается в том, что вы можете передвигать карту с помощью клавиш управления курсором. Стрелки вверх и вниз вызывают перемещение вдоль оси Y, а стрелки влево и вправо — вдоль оси X. Запустите программу и проверьте это самостоятельно.
    Вы можете обратить внимание на отладочную информацию, отображаемую в левом верхнем углу окна. Здесь показаны координаты блока, который в данный момент отображается в левом верхнем углу окна. Когда вы перемещаетесь по карте, эти координаты изменяются, отражая вашу глобальную позицию. Помните, что значение координат по любой из осей не может быть меньше нуля.

    Определение состояния DIK

    Массив didKeyboardBuffer хранит данные возвращенные DirectInput. Чтобы сделать их читаемыми, необходимо проверить значение каждого элемента массива. Если результат поразрядной логической операции И над возвращенным значением и константой 0x80 не равен нулю, значит клавиша была нажата; в ином случае клавиша была отпущена. Я знаю, это выглядит причудливо, но именно так работает DirectInput!

    Основные сведения о частицах

    Сейчас вы в лагере работающих с частицами новобранцев. Первый вопрос повестки дня — что такое частицы? Если вы откроете корпус своего компьютера и дунете внутрь, то, скорее всего, увидите летающие по комнате частицы пыли. Если вы изучали электромагнитные явления, то наверняка использовали железные опилки. Запустив фейерверк вы увидите летящие в разные стороны искры. Фактически, частица это очень маленькая часть чего-нибудь. Слово «чего-нибудь» допускает многочисленные толклвания. У вас могут быть частицы древесины, песка, грязи, воды и т.д. Вот несколько примеров частиц, которые часто встречаются в стратегических играх:
  • Частицы огня для взрывов.
  • Частицы дыма для следов ракет.
  • Частицы пыли при передвижении подразделений.
  • Частицы воды для дождя.
  • Частицы льда для снега.
  • Частицы энергии для силовых лучей.
  • Частицы энергии для силовых лучей.


  • Основы A*

    Давайте познакомимся с терминами, которые используются при описании алгоритма A*.
    Узел — Позиция на карте.
    Открытый список — Список узлов в которые может переместиться игрок и которые являются смежными с закрытыми узлами.
    Закрытый список — Спиок узлов, в которые может переместиться игрок и которые уже были пройдены им.
    Чтобы понять, как эти термины применяются, взгляните на Рисунок 12.6.


    Основы редактирования карт

    Первый вопрос, который вы должны задать: «Что такое редактор карт?». Редактор карт помогает вам собирать вместе графические блоки карты в формате, пригодном для использования в вашей игре. Он очень похож на программу для рисования, где в качестве холста выступает карта, а в качестве кистей — графические блоки.
    Следующий вопрос: «Зачем создают редакторы карт?». Если вы не хотите вручную печатать тысячи номеров графических блоков, редактор карт — единственный способ создавать карты и уровни для вашей игры. Продолжая аналогию с программой для рисования можно сказать, что создание карт без редактора подобно рисованию картины путем ввода шестнадцатеричных кодов, задающих цвета точек. Это подходит для мечты идиота, но в реальном мире не слишком практично.
    И, наконец, последний вопрос: «На что похож редактор карт?». Если вы когда-нибудь использовали редактор уровней в игре, то уже знаете ответ на этот вопрос. Если нет, взгляните на Рисунок 10.1.


    Отображение активных подразделений

    Осталось только выполнить цикл, который будет перебирать все подразделения и отображать те из них, которые в данный момент активны. Выполняющий эту задачу фрагмент кода приведен ниже:
    // Цикл перебирающий подразделения for(int i = 0; i < m_UnitManager.m_iTotalUnitObjs; i++) { // Устанавливаем указатель на подразделение ptrUnit = &m_UnitManager.m_UnitObjs[i]; // Проверяем, активно ли подразделение if(ptrUnit->m_bActive) { // Рисуем подразделение vDrawUnit( ptrUnit->m_fXPos, ptrUnit->m_fYPos, ptrUnit->m_fScale * 128.0f, ptrUnit->m_fScale * 128.0f, ptrUnit->m_fRot, ptrUnit->m_Animation, ptrUnit->m_iCurAnimFrame, ptrUnit->m_iOwner ); } } В приведенном выше коде я в цикле перебираю все подразделения, созданные в диспетчере подразделений. Если подразделение активно я вызываю функцию рисования и передаю ей параметры подразделения. Местоположение подразделения определяет в каком месте экрана оно будет находиться. Параметр, задающий поворот определяет ориентацию подразделения. Указатель анимации сообщает функции визуализации подразделения откуда ей брать данные текстуры. Номер текущего кадра анимации сообщает функции визуализации подразделения, какую именно текстуру ей следует выводить. Код владельца определяет, какая именно текстура с цветами владельца будет наложена поверх изображения подразделения. Хм-м-м... Мне кажется, я что-то забыл. Ах, да! Как вычисляется текущий кадр анимации? С помощью функции vUpdateUnits(). Взгляните на Рисунок 8.31, чтобы увидеть ход выполнения функции визуализации до данного момента.


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

    Панель инструментов сама по себе выглядит великолепно, но окончательный блеск ей придают отображаемые на ней блоки. В области выбора блоков одновременно может отображаться до 21 блока. Если у вас больше 21 блока, для доступа к дополнительным блокам используются кнопки навигации. Чтобы отобразить 21 блок я визуализирую их в пустом буфере, а затем копирую полученный результат на панель инструментов. Код для этого содержится в следующей функции:
    void vRenderTileSet(void) { RECT rectDest; RECT rectSrc; int iX; int iY; int iTile; // Включаем рассеянное освещение g_pd3dDevice->SetRenderState(D3DRS_AMBIENT, 0x00606060); // Очищаем вторичный буфер и z-буфер g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); // Начинаем визуализацию g_pd3dDevice->BeginScene(); // Задаем состояние альфа-смешивания // Это необходимо для реализации прозрачности/полупрозрачности g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); // Отображение активных блоков for(iY = 0; iY < 7; iY++) { for(iX = 0; iX < 3; iX++) { // Вычисляем отображаемый блок iTile = (g_iCurTileSet * 21) + (iX + (iY * 3)); // Отображаем, если это существующий блок if(iTile < g_iTotalTiles) { vDrawInterfaceObject( iX * g_iTileSize, iY * g_iTileSize, (float)g_iTileSize, (float)g_iTileSize, iTile); } // Рисуем рамку поверх текущего блока if(iTile == g_iCurTile) { vDrawInterfaceObject( iX * g_iTileSize, iY * g_iTileSize, (float)g_iTileSize, (float)g_iTileSize, 18); } } } // Отображаем текущий блок vDrawInterfaceObject( 32, 32 * 7, (float)g_iTileSize, (float)g_iTileSize, g_iCurTile); // Завершаем визуализацию g_pd3dDevice->EndScene(); // Исходный прямоугольник rectSrc.top = 0; rectSrc.bottom = g_iTileSize * 8; rectSrc.left = 0; rectSrc.right = g_iTileSize * 3; // Целевой прямоугольник rectDest.top = 2; rectDest.bottom = (g_iTileSize * 8) + 2; rectDest.left = 0; rectDest.right = (g_iTileSize * 3); g_pd3dDevice->Present(&rectSrc, &rectDest, hWndToolBar, NULL); } Первая часть логики визуализации содержит код, который очищает буфер, включает рассеянное освещение и активизирует альфа-смешивание. Главное удовольствие начинается в идущих следом циклах визуализации. В основном цикле программа перебирает в цикле ряды из трех блоков и визуализирует каждый блок в экранном буфере. Код продолжает работать таким образом, пока не будут отображены все семь рядов блоков.
    Чтобы помочь пользователю понять, какой именно блок активен в данный момент времени, код изображает вокруг выбранного блока красный квадрат. В цикле визуализации для каждого блока проверяется не является ли он активным в данный момент, и, если да, то к его изображению добавляется красный квадрат.
    После того, как визуализация набора блоков завершена, копия выбранного в данный момент блока отображается в нижней части экрана. Это еще один полезный индикатор текущего блока. Взгляните на Рисунок 10.9, чтобы увидеть структуру панели инструментов.


    Отображение миникарты

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


    Отображение введенного текста

    Вы видели как инициализируется текстовый ввод и как происходит обработка полученных от клавиатуры данных, но где же отображение текста? Эта сложная задача возложена на функцию визуализации, которая и отображает введенный текст даже не поморщившись. Как выполняется отображение введенного текста показано в следующие фрагменте кода:
    // Отображение экрана новой игры vDrawInterfaceObject(0, 0, 256.0f, 256.0f, 0); vDrawInterfaceObject(256, 0, 256.0f, 256.0f, 1); vDrawInterfaceObject(512, 0, 256.0f, 256.0f, 2); vDrawInterfaceObject(0, 256, 256.0f, 256.0f, 3); vDrawInterfaceObject(256, 256, 256.0f, 256.0f, 4); vDrawInterfaceObject(512, 256, 256.0f, 256.0f, 5); // Поле ввода vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 14); // Отображаем курсор, если ввод активен if(g_bTextInputActive) { // Обновление состояния мерцания курсора if(timeGetTime() > g_dwTextInputTimer) { if(g_bTextInputCursorFlash) { g_bTextInputCursorFlash = 0; g_dwTextInputTimer = timeGetTime() + 250; } else { g_bTextInputCursorFlash = 1; g_dwTextInputTimer = timeGetTime() + 250; } } // Рисуем курсор, если он не скрыт if(g_bTextInputCursorFlash) { vDrawInterfaceObject(g_shTextInputXPos + g_shTextInputPosition * 8, g_shTextInputYPos, 4.0f, 16.0f, 15); } } // Отображение текста // Создаем прямоугольник для текста RECT rectText = { g_shTextInputXPos, g_shTextInputYPos, g_shTextInputXPos + (g_shTextMaxSize * 8), g_shTextInputYPos + 20 }; // Выводим текст pD3DXFont->DrawText(g_szTextInputBuffer, -1, &rectText, DT_LEFT, D3DCOLOR_RGBA(255, 255, 255, 255)); Код визуализации, отвечающий за отображение вводимого текста вступает в действие когда пользователь переходит на экран номер четыре. Логика работы данного кода изображена на Рисунок 9.11.


    Переменные для новых кнопок

    Чтобы кнопки переключения слоев правильно функционировали, каждой из них требуется дескриптор окна и уникальный идентификатор. Соответствующий код приведен ниже:
    const int ID_BUTTON_LAYER1 = 40006; const int ID_BUTTON_LAYER2 = 40007; const int ID_BUTTON_LAYER3 = 40008; const int ID_BUTTON_LAYER4 = 40009; HWND hBUTTON_LAYER1 = NULL; HWND hBUTTON_LAYER2 = NULL; HWND hBUTTON_LAYER3 = NULL; HWND hBUTTON_LAYER4 = NULL; В приведенном выше коде вы видите, как я создаю для каждой кнопки уникальное значение и дескриптор окна. Это необходимо для обработки событий нажатия на кнопку в цикле сообщений Windows. Здесь нет ничего специального — лишь обычный для оконного графического интерфейса код.

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

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


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

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


    Полная анимационная последовательность для танка


    Полная анимационная последовательность для танка


    На Рисунок 8.17 показаны сразу все кадры анимации танка. Первый кадр — это изображение ожидающего танка. Следующие три кадра содержат анимационную последовательность для движения. Следующие два кадра содержат анимационную последовательность атаки. Последние три кадра содержат анимационную последовательность гибели. Вместо того, чтобы хранить кадры анимации в различных массивах, класс анимации сохраняет их все в одном непрерывном массиве. Это означает, что все кадры будут расположены один за другим. В результате, анимация ожидания начинается с нулевого кадра, а анимационная последовательность перемещения — нет. Стартовый кадр каждой последовательности зависит от того, сколько кадров находится перед ним. Рассмотрим для примера анимационную последовательность атаки. Она начинается с четвертого кадра в цепочке, поскольку перед ней расположены кадр для состояния ожидания и анимационная последовательность передвижения. Помните, что номер первого кадра в цепочке — 0, а не 1. Взглянув еще раз на рисунок, вы заметите, что под каждым кадром приведен связанный с ним порядковый номер. В данном примере анимационная последовательность ожидания начинается с кадра 0, анимационная последовательность передвижения — с кадра 1, анимационная последовательность атаки — с кадра 4 и анимационная последовательность гибели — с кадра 6. Если вы добавите кадры в середину набора, номера начальных кадров расположенных правее анимационных последовательностей должны быть увеличены.

    Преобразование кода DIK в код ASCII

    Для преобразования кодов DIK в коды ASCII я написал следующую функцию:
    BYTE Scan2Ascii(DWORD scancode) { UINT vk; // Преобразование скан-кода в код ASCII vk = MapVirtualKeyEx(scancode, 1, g_Layout); // Возвращаем код ASCII return(vk); } Функция получает код клавиши DirectInput и вызывает функцию MapVirtualKeyEx() для преобразования его в ASCII. Для работы функции отображения кодов необходимы данные о раскладке клавиатуры, которые мы получили на этапе инициализации.


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


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


    На Рисунок 9.7 вы видите уже ставший знакомым интерфейс игры Battle Armor с полем ввода текста в центре экрана. В это поле вводится имя игрока. Обратите внимание, что я ввел в это поле строку «Lost Logic» и курсор находится в конце введенного текста. Все графические элементы должны быть вручную обработаны в вашей игре, так что читайте дальше, чтобы выяснить как это происходит.
    Откройте проект с именем D3D_InputBox чтобы увидеть код, создающий окно, изображенное на Рисунок 9.7. Этот проект является вариантом рассмотренного ранее проекта игрового интерейса, так что большая часть кода должна выглядеть знакомо. После загрузки проекта взгляните на Рисунок 9.8, где изображен ход выполнения программы. На Рисунок 9.8 видно, что программа инициализирует DirectInput, клавиатуру, Direct3D, объекты интерфейса и активные зоны. После завершения цинициализации программа входит в цикл обработки сообщений где проверяет поступающие данные и отображает графику.


    Примеры движения частиц


    Примеры движения частиц


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

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

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

    Проект DInput_Simple

    В сопроводительные файлы книги включен проект DInput_Simple. Он строит небольшое приложение, создающее объект клавиатуры и читающее поступающие от него данные. Окно программы показано на Рисунок 9.2.


    Программирование панели инструментов

    Панель инструментов с областью выбора блоков является очень важной частью редактора карт. Без нее динамическое редактирование блочной карты было бы очень трудным. Код использует стандартные приемы программирования для Windows, чтобы создать дочернее окно главного окна программы и добавить к нему несколько элементов управления. Отображение блоков на панели инструментов выполняется посредством обращения к DirectX. Вот как выглядит код, создающий окно панели инструментов и кнопки навигации по страницам набора блоков:
    void vCreateToolbar(HWND hwnd, HINSTANCE hinst) { WNDCLASSEX wcToolBar; // Инициализация и регистрация класса окна панели инструментов wcToolBar.cbSize = sizeof(wcToolBar); wcToolBar.style = CS_HREDRAW | CS_VREDRAW; wcToolBar.lpfnWndProc = fnMessageProcessor; wcToolBar.cbClsExtra = 0; wcToolBar.cbWndExtra = 0; wcToolBar.hInstance = hinst; wcToolBar.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcToolBar.hCursor = LoadCursor(NULL, IDC_ARROW); wcToolBar.hbrBackground= (HBRUSH) GetStockObject (COLOR_BACKGROUND); wcToolBar.lpszMenuName = NULL; wcToolBar.lpszClassName= "ToolBar"; wcToolBar.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wcToolBar); // Создание окна панели инструментов hWndToolBar = CreateWindowEx( WS_EX_LEFT|WS_EX_TOPMOST|WS_EX_TOOLWINDOW, "ToolBar", "ToolBar", WS_BORDER | WS_VISIBLE | WS_CAPTION | WS_MINIMIZEBOX, g_iWindowWidth - 100, g_iYOffset, 100, g_iWindowHeight - 20, hwnd, NULL, hinst, NULL); // Кнопка выбора предыдущего блока hBUTTON_PREVTILE = CreateWindow( "BUTTON", "<", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 10, 405, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_PREVTILE, hinst, NULL); // Кнопка выбора следующего блока hBUTTON_NEXTTILE = CreateWindow( "BUTTON", ">", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 65, 405, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_NEXTTILE, hinst, NULL); // Активация области редактирования SetActiveWindow(g_hWnd); // Отображение набора блоков на панели инструментов vRenderTileSet(); } Переменная wcToolBar хранит информацию класса окна панели инструментов. Значения для класса не представляют собой ничего такого, что следовало бы описать, поскольку следуют стандартным правилам программирования для Windows.
    Функция CreateWindowEx() выполняет фактическое создание панели инструментов. Она создает окно с именем ToolBar без кнопок закрытия и свертывания. Это гарантирует, что пользователь случайно не закроет область выбора блоков. Кроме того, в функции создания окна я задаю местоположение панели инструментов таким образом, чтобы она находилась внутри границ главного окна.
    После того, как окно панели инструментов создано, я создаю пару кнопок для навигации по набору блоков. Эти кнопки называются hBUTTON_PREVTILE и hBUTTON_NEXTTILE. Когда их нажимают, программа переходит к предыдущей или следующей странице странице набора блоков.

    Программирование шаблона

    Приготовьтесь к беспощадной драке, поскольку пришло время спуститься с небес на землю и заняться шаблонами подразделений. Данный раздел книги более сложен, чем остальные, так что будьте внимательны, чтобы извлечь максимум пользы из предоставленной информации. Не прерывайтесь, чтобы поиграть в Combat Mission!
    Прежде чем погрузиться в глубины кода, взгляните на Рисунок 8.7, где изображен результат работы программы, о которой я собираюсь рассказать.


    Просмотр карты

    Хватит уже теории! Как насчет какого-нибудь кода, который покажет вам как создать свой собственный редактор карт? Загрузите проект D3D_MapViewer и следуйте вперед.
    Программа D3D_MapViewer создает случайную карту, которую вы можете прокручивать в различных направлениях. Вы не сможете редактировать эту карту, но проект покажет вам основы навигации на блочной карте. После того, как вы разберетесь с реализацией прокручиваемой в разные стороны блочной карты, я покажу вам как осуществлять редактирование карты.
    Запустите программу просмотра карт, и вы увидите окно, изображенное на Рисунок 10.3.


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

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


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


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


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


    Радиус взрыва

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

    Радиус взрыва


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

    Раскладка клавиатуры

    В рассматриваемом примере я покажу вам как считывать коды клавиш DirectInput и ASCII-коды клавиш. Чтобы получить возможность преобразования кодов DIK в коды ASCII вы должны вызвать функцию GetKeyboardLayout(). Она получает раскладку подключенной к системе клавиатуры для дальнейшего использования.
    ПРИМЕЧАНИЕ Функция GetKeyboardLayout() не является необходимой для работы кода DirectInput. Я применяю ее только для преобразования кодов DIK в коды ASCII. Этапы, необходимые для инициализации клавиатуры, показаны на Рисунок 9.6.


    Реализация системы частиц

    Теперь загрузите проект D3D_Particles, если вы еще не сделали этого, и скомпилируйте его. Запустите программу и вы увидите сцену, похожую на ту, что показана на Рисунок 13.3.


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

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

    Редактирование карты

    К данному моменту у вас появилась лишь возможность интерактивного просмотра карты. Как насчет того, чтобы действительно поредактировать ее? Заучит заманчиво, а? В этом разделе я покажу вам как написать редактор карт, который позволит вам самим размещать блоки на карте. Ушли те дни, когда вы задавали карты в виде набора значений в коде программы! Взгляните на Рисунок 10.7, где изображено окно редактора карт, о котором я буду рассказывать.


    Панель инструментов теперь видима, но как же вычислить, какой блок на ней выбран? В этом случае мышь — лучший друг человека, и именно она будет использоваться для выбора блоков. Процесс может показаться легким, если не учесть тот факт, что панель инструментов может свободно перемещаться. Поскольку вы не можете гарантировать, что панель инструментов всякий раз будет находиться в одном и том же месте, необходимо принимать во внимание ее текущее местоположение и уже основываясь на этой информации проверять координаты указателя мыши.
    В программе D3D_MapEditorLite есть два режима работы мыши. В режиме редактирования вы шелкаете левой кнопкой мыши по карте в области просмотра и редактируете выбранный блок. В режиме выбора блока вы щелкаете по изображениям блоков на панели инструментов, чтобы выбрать новый блок для использования при редактировании. Чтобы выяснить в каком режиме работает пользователь, я проверяю координаты указателя мыши и смотрю, находятся ли они внутри области панели инструментов или внутри области просмотра карты. Если координаты находятся внутри области панели инструментов, я перехожу к логике для панели инструментов, чтобы определить, какой блок находится под указателем мыши. Если координаты находятся внутри области просмотра карты, в игру вступает логика редактирования карты, вычисляющая какой именно блок карты редактируется. Вот как выглядит код, выполняющий эти задачи:
    void vCheckMouse(void) { RECT rcWindow; POINT Point; int iMouseX; int iMouseY; int iTileX; int iTileY; // Получаем координаты указателя мыши GetCursorPos(&Point); iMouseX = Point.x; iMouseY = Point.y; // Определяем местонахождение рабочей области панели инструментов GetWindowRect(hWndToolBar, &rcWindow); // Проверяем, находится ли указатель мыши внутри окна панели инструментов if(iMouseX > rcWindow.left && iMouseX < rcWindow.right && iMouseY > rcWindow.top && iMouseY < rcWindow.bottom) { // Преобразуем координаты указателя мыши // в локальные координаты панели инструментов iMouseX -= rcWindow.right; iMouseY -= rcWindow.top; // Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize; // Вычисляем, какой блок выбран g_iCurTile = (g_iCurTileSet * 21) + (iTileX + (iTileY * 3)) - 1; // Проверяем, что выбран существующий блок if(g_iCurTile < 0|| g_iCurTile >= g_iTotalTiles) { g_iCurTile = 0; } vRenderTileSet(); } // Проверяем, находится ли указатель мыши в окне редактирования else { GetWindowRect(g_hWnd, &rcWindow); if(iMouseX > rcWindow.left && iMouseX < rcWindow.right && iMouseY > rcWindow.top && iMouseY < rcWindow.bottom) { // Преобразуем координаты указателя мыши // в локальные координаты окна редактирования iMouseX -= rcWindow.left + g_iXOffset; iMouseY -= rcWindow.top + g_iYOffset; // Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize; g_iTileMap[iTileX + g_iXPos + ((iTileY + g_iYPos) * g_iMapWidth)] = g_iCurTile; } } } Чтобы определить местоположение панели инструментов и окна редактирования я воспользовался функцией GetWindowRect(). После того, как я узнал где они расположены, достаточно простой проверки, чтобы определить находится ли указатель мыши в заданной области.
    Если указатель мыши находится внутри панели инструментов, я беру координаты указателя мыши и делю их на размеры блока, чтобы увидеть, какой именно блок выбирает пользователь. Как только я узнал, какой блок выбран, я заношу в переменную g_iCurTile новое значение. Затем я вызываю функцию vRenderTileSet(), чтобы переместить красный квадрат, отмечающий выбранный блок на новое место.
    Если указатель мыши находится в окне редактирования, я корректирую координаты указателя мыши с учетом местоположения клиентской области окна. Затем я делю координаты указателя мыши на размеры блока, чтобы вычислить какой блок карты выбран для редактирования. Последний шаг к Валгалле — помещение значения из переменной g_iCurTile в соответствующий элемент массива g_iTileMap.


    Редактор уровней игры Warcraft III

    Редактор уровней игры Warcraft III


    На Рисунок 10.1 представлено окно редактора уровней игры Warcraft III, поставляемого вместе с игрой и являющегося очень мощным инструментом. Он позволяет вам редактировать карты, поставляемые с игрой, или создавать свои собственные с нуля. На рисунке вы можете видеть мини-карту, представляющую высокоуровневый взгляд на карту и крупный план редактируемой в данный момент области, занимающий большую часть окна редактора. Вокруг области редактирования есть различные панели инструментов, позволяющие выбирать текстуры и производимые с картой действия.
    В этой главе я покажу вам как создать собственный редактор карт. Он будет не таким мощным и сложным, как редактор Blizzard для Warcraft III, но, по крайней мере, предоставит вам хорошую отправную точку.
    ПРИМЕЧАНИЕ Создание редактора карт может показаться простой задачей, но процесс его разработки обычно занимает столько же (если не больше) времени, что и цикл разработки самой игры!

    Редактор уровней Warcraft III с включенной сеткой


    Редактор уровней Warcraft III с включенной сеткой


    На рисунке показана область редактирования редактора уровней игры Warcraft III с включенной сеткой редактирования. Как видите, сетка ускоряет выравнивание плиток.

    Рисование подразделений

    Все эти классы великолепны, но как насчет визуализации? Если вы откроете файл main.cpp из проекта D3DFrame_UnitTemplate, я покажу вам! Спускайтесь вниз до функции vInitTileVB().
    Вы, возможно, еще помните пример визуализации блоков, который я демонстрировал в главе 5. Там я создавал отдельный буфер вершин для хранения геометрии текстуры. Базовая точка геометрии находилась в левом нижнем углу квадрата. Это упрощает выравнивание квадратов при визуализации блоков, но не слишком подходит для поворотов. Взгляните на Рисунок 8.28, чтобы понять что я имею в виду.


    Скорость передвижения

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

    Скорость поворота

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


    Скорость снаряда

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

    Скорость восстановления

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

    Скорострельность

    Переменная m_iRateOfFire сообщает вам, сколько раундов игры должно пройти, прежде чем оружие сможет снова выстрелить. Быстродействующее оружие, такое как автомат, может стрелять залпами в каждом раунде игры. Более медленное оружие, например, катапульты, будут стрелять один раз в пять раундов, или что-то подобное. Конечно, автомат может выпускать сразу несколько пуль, и именно поэтому я использовал термин «стрелять залпами».
    Не существует однозначного ответа на вопрос сколько раундов игры должно пройти , пока оружие сново может выстрелить. Чтобы получить сбалансированный тип атаки, вам придется поиграть с разными значениями.

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

    Вау! Мы уже узнали как редактировать карту, так что не осталось никаких препятствий на пути к вершинам картографии. Редактирование карт — великолепная возможность, но она бессмысленна, если вы не можете сохранить результаты своей работы. Здесь на сцену выходит функция сохранения карты. В этом разделе я покажу вам как добавить к программе D3D_MapEditorLite полнофункциональные кнопки сохранения и загрузки. Загрузите новый улучшенный проект D3D_MapEditorPlus и пойдемте дальше. Сперва взгляните на Рисунок 10.10, где изображено окно этой программы.


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


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


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

    Создание подразделений

    Теперь, после того как базовая информация о подразделених загружена, вы можете создавать подразделения, которые будут использоваться в игре. Вы не можете модифицировать базовые типы, так что следует создавать новые объекты подразделений. Здесь в игру вступает член данных диспетчера подразделений с именем m_UnitObjs. Данный массив хранит модифицируемые объекты подразделений, использующиеся в игре. Для управления этими объектами применяются две функции: iAddUnit() и vRemoveUnit().

    Способ передвижения

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

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

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

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

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

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


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


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


    Структура функции проверки ввода


    Структура функции проверки ввода


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

    Структура класса частиц

    В качестве примера работы с классом частиц я включил в сопроводительные файлы книги проект с именем D3D_Particles. Загрузите его и следуйте вперед к созданному мной примеру класса. Код класса находится в двух файлах проекта: CParticle.cpp и CParticle.h. Вот как выглядит заголовочный файл:
    class CVector { public: float fX; float fY; float fZ; CVector() { fX=0.0f, fY=0.0f, fZ=0.0f; }; }; class CParticle { public: CVector m_vecPos; CVector m_vecCurSpeed; CVector m_vecAcceleration; CVector m_vecGravity; int m_iLife; int m_iTextureStart; int m_iTextureEnd; int m_iTextureType; int m_iTextureCur; int m_iTextureSteps; int m_iTextureCurStep; CParticle(); ~CParticle(); void vUpdate(void); bool bIsAlive(void); void vSetTextures(int iType, int iStart, int iStop, int iSteps); void vSetPos(float x, float y, float z); void vSetAcceleration(float x, float y, float z); void vSetGravity(float x, float y, float z); void vSetSpeed(float x, float y, float z); void vSetLife(int iLife); };

    Структура объекта подразделения


    Структура объекта подразделения


    На Рисунок 8.20 видно, что класс подразделения состоит из базовых классов и данных состояния. В блоке данных состояния находятся переменные для различных параметров, таких как текущее количество очков повреждений, направление поворота, местоположение и текущая скорость. Обратите внимание на пунктирную линию, соединяющую максимальное количество очков повреждений в базовом объекте защиты и текущее количество очков повреждений в данных состояния. Текущее количество очков повреждений показывает сколько еще повреждений может получить подразделение до его уничтожения. Это значение изменяется когда подразделение получает повреждения или восстанавливается. Поскольку подразделения не могут совместно использовать одно общее значение здоровья, текущее значение здоровья хранится каждым подразделением локально в его данных состояния. Базовый тип защиты вступает в игру, когда вычисляется максимально возможный для подразделения показатель здоровья. Это значение никогда не изменяется, и поэтому базовый класс — наилучшее место для него.
    Держа в уме информацию с Рисунок 8.20, взглянем на исходный код:
    class CUnit { public: CUnitDefense *m_Defense; CUnitOffense *m_Offense1; CUnitOffense *m_Offense2; CUnitOffense *m_Offense3; CUnitMovement *m_Movement; CUnitAnimation *m_Animation; int m_iType; int m_iCurHitPoints; float m_fCurSpeed; float m_fXPos; float m_fYPos; float m_fRot; float m_fScale; int m_iUnitID; int m_iParentID; char m_szName[64]; bool m_bActive; int m_iOwner; int m_iCurAnimFrame; int m_iCurAttackFrame; int m_iCurStillFrame; int m_iCurMoveFrame; int m_iCurDieFrame; public: CUnit(); ~CUnit(); virtual void vReset(void); virtual void vSetBaseValues( CUnitDefense* ptrDef, CUnitOffense* ptrOff1, CUnitOffense* ptrOff2, CUnitOffense* ptrOff3, CUnitMovement* ptrMove, CUnitAnimation* ptrAnim); virtual void vSetPosition(float fX, float fY); }; С точки зрения количества функций класс не выглядит слишком сложным. Большая часть кода состоит из объявлений необходимых для игры переменных состояния. Это ни в коем случае нельзя считать достаточным для завершенного класса подразделения. Перечисленных переменных состояния достаточно только для рассматриваемого примера. В реальной игре их будет гораздо больше!

    Структура панели инструментов


    Структура панели инструментов


    На Рисунок 10.9 видно как отображение блоков начинается в левом верхнем углу и продолжается сверху вниз. На иллюстрации текущим блоком является блок с номером 10 и поэтому именно вокруг него нарисован красный квадрат. Тот же самый блок скопирован и в нижней части области просмотра блоков, что так же указывает какой именно блок выбран. Еще ниже на панели инструментов выводятся кнопки для навигации по набору блоков.
    Теперь, когда выполнена визуализация всех блоков, надо поместить их на панель инструментов. Это выполняется путем задания исходной и целевой областей и вызова функции Present(). Как видите, в качестве параметров этой функции передаются исходная прямоугольная область и прямоугольная область места назначения. Сама функция сообщает системе визуализации, что необходимо взять изображение из одной области и скопировать его в другую. В этом случае вы можете визуализировать блоки в буфере трехмерной графики, а затем скопировать их на панель инструментов для показа. Посмотрите на код, и вы увидите как я копирую исходную область и перемещаю ее на панель инструментов.

    Структура проекта D3D_Particles

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


    Терминология в алгоритме A*


    Терминология в алгоритме A*


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


    Тип атаки

    Переменная m_iType хранит число, соответствующее данному типу атаки. Это работает точно так же, как и для типов защиты.

    Тип защиты

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

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

    Я уже показывал вам управление текстурами ранее, в разделе посвященном импорту данных подразделений. Поскольку функция iLoadBaseTypes() загружает все тербуемые текстуры, можно считать, что управление текстурами уже реализовано. Тем не менее, я добавил еще одну функцию управления, которая подсчитывает количество загруженных текстур и возвращает полученное значение. Она полезна при вычислении объема используемой для хранения текстур памяти. Функция называется iCountTotalTextures(), и вот как выглядит ее код:
    int CUnitManager::iCountTotalTextures(void) { int iCount = 0; // Цикл перебора объектов анимации и подсчета текстур for(int i = 0; i < m_iTotalAnimationObjs; i++) { iCount += m_AnimationObjs[i].m_iTotalTextures; } return(iCount); } В функции я перебираю все загруженные базовые типы анимации и суммирую количество текстур, содержащееся в каждом из них. После того, как цикл завершен я возвращаю итоговое значение вызывающей программе. Поскольку каждая текстура в данной игре имеет размер 128x128 точек и глубину цвета 32 бит, для вычисления объема занимаемой текстурами памяти вам достаточно умножить возвращаемое функцией общее количество текстур на 65536 (128 x 128 x 4).


    Ускорение и торможение

    Чтобы добавить сложности, я включил параметры, задающие ускорение и торможение. Ускорение определяет на сколько возрастает скорость подразделения за раунд игры, когда оно разгоняется. Торможение сообщает вам на сколько уменьшается подразделения за раунд игры, когда оно тормозит. Этот параметр позволяет увеличить реализм сражений. Одни подразделения могут и должны быть медленнее (или быстрее), чем другие. Рассмотрим следующий пример:
    Легкая конница: Ускорение = 0.3, Торможение = 0.5, Скорость = 0.5
    Катапульта: Ускорение = 0.1, Торможение = 0.2, Скорость = 0.3
    В данном примере легкая конница может разогнаться до максимальной скорости за два раунда. Для полной остановки этому подразделению потребуется еще меньше времени — один раунд. Катапульты двигаются медленнее. Чтобы разогнаться до полной скорости им потребуется три раунда, а чтобы остановиться — два. Это действительно имеет смысл, ведь катапульта не может двигаться так же быстро как лошадь. Вы можете не использовать параметры ускорения и торможения в ваших играх, если они показались вам слишком сложными, но помните, что они добавляют вашей игре значительную толику реализма.

    Установка формата данных клавиатуры

    Затем вы должны задать формат данных клавиатуры. Это простая формальность, для соблюдения которой достаточно вызвать функцию IDirectInputDevice8::SetDataFormat(). Функция получает один параметр, задающий формат данных устройства. Для клавиатуры используйте значение c_dfDIKeyboard. Если же вам необходимо задать формат данных для мыши, воспользуйтесь значением c_dfDIMouse.

    Установка уровня кооперации

    Поскольку DirectX предоставляет прямой доступ к аппаратуре, очень важен уровень кооперации устройства. Он определяет как программа может использовать данный ресурс совместно с другими приложениями. Если вы установите монопольный режим, больше никто не сможет воспользоваться данным ресурсом. Если вы установите совместный режим, то доступ к клавиатуре смогут получить все желающие. Уверен, вы можете вспомнить игры, которые не делят клавиатуру ни с кем. Мне на ум приходит EverQuest. Поскольку создатели игры не хотели, чтобы сторонние разработчики писали приложения для их игры, они заблокировали использование клавиатуры вне их программы. Это не слишком хорошо и может вызвать настоящие проблемы, если вы переключитесь из игры на другое приложение, чтобы проверить почту или сделать что-нибудь еще.
    Для установки уровня кооперации применяется функция IDirectInputDevice8::SetCooperativeLevel(). Вот ее прототип:
    HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwFlags ); В ее первом параметре, hwnd, передается дескриптор окна, которое будет связано с устройством. Я в этом параметре передаю дескриптор, который был возвращен мне при создании главного окна.
    Второй параметр, dwFlags, задает уровень кооперации устройства. Доступные уровни перечислены в таблице 9.1.
    Таблица 9.1. Уровни кооперации устройств
    Значение Описание
    DISCL_BACKGROUND Доступ к клавиатуре будет предоставлен даже если окно свернуто.
    DISCL_EXCLUSIVE Предоставляется монопольный доступ к клавиатуре, для всех остальных клавиатура недоступна.
    DISCL_FOREGROUND Доступ к данным клавиатуры предоставляется только когда окно активно.
    DISCL_NONEXCLUSIVE Устройство используется совместно с другими программами.
    DISCL_NOWINKEY Блокирует клавишу Windows.

    Для рассматриваемого примера я устанавливаю флаги уровня кооперации DISCL_NONEXCLUSIVE и DISCL_FOREGROUND. Благодаря этому программа использует клавиатуру совместно с другими приложениями, а сама может читать данные клавиатуры только когда ее окно активно.

    Визуализация блоков

    Функция vRender() занимается отображением блочной карты. В ней я в цикле перебираю блоки карты и отображаю текстуры, соответствующие номерам блоков. Вот код цикла визуализации:
    // Сверху вниз for(iY = 0; iY < g_iTilesHigh; iY++) { // Справа налево for(iX = 0; iX < g_iTilesWide; iX++) { // Вычисляем смещение в буфере iBufferPos = iX + g_iXPos + ((iY + g_iYPos) * g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos]; // отображаем блок vDrawInterfaceObject((iX * g_iTileSize), (iY * g_iTileSize), (float)g_iTileSize, (float)g_iTileSize, iCurTile); } } В функции визуализации присутствуют два цикла. Первый цикл осуществляет перебор блоков вдоль оси Y. Внутренний цикл перебирает блоки вдоль оси X. Таким образом я отображаю все необходимые блоки. Это тот же самый метод, который я описывал в главе 5.

    Визуализация частиц

    Теперь, когда частицы инициализированы, пришло время отображать их. Это делается в привычной функции vRender(). Вот часть ее кода, отвечающая за визуализацию частиц:
    // Цикл перебора частиц for(int i = 0; i < TOTAL_PARTICLES; i++) { // Проверяем, жива ли частица if(g_partExplosion[i].bIsAlive()) { // Визуализация частицы vDrawInterfaceObject(g_partExplosion[i].m_vecPos.fX, g_partExplosion[i].m_vecPos.fY, (float)g_iParticleSize, (float)g_iParticleSize, g_pTexture[ g_partExplosion[i].m_iTextureCur ]); // Обновление частицы g_partExplosion[i].vUpdate(); } else { // Сброс частицы, если она уничтожена vInitParticles(); } } Функция визуализации в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы функция сперва проверяет жива ли она еще. Если частица жива, функция отображает ее в текущем местоположении. Чтобы сообщить функции визуализации, какую текстуру следует использовать, применяется хранящийся в объекте частицы номер текущей текстуры. После визуализации частицы ее данные обновляются путем вызова функции vUpdate().
    СОВЕТ Если вы хотите увеличить или уменьшить количество частиц, поменяйте значение константы TOTAL_PARTICLES в заголовочном файле. Попробуйте делать его больше и больше, пока не заметите влияние на систему. Мой компьютер заметно замедляет работу, когда я создаю около 6000 частиц. Если функция визуализации обнаруживает, что частица прекратила свое существование, она вызывает функцию инициализации, чтобы создать новую частицу. Благодаря этому на экране отображается бесконечный цикл анимации частиц, продолжающийся до тех пор, пока вы не выйдите из приложения.


    Ввод с клавиатуры

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

    Ввод текста в игре

    Решение задачи ввода текста в игре может казаться лежащим на поверхности, но есть множество моментов, на которые следует обратить внимание. Как, например, вы будете обрабатывать ввод текста в разгаре игры? Стратегическая игра реального времени останавливающаяся каждый раз, когда пользователь хочет ввести текст, выглядит не слишком хорошо! Кроме того, стоит выбрать способ отображения текста. Что вы будете применять для отображения текста: двухмерные шрифты или трехмерные карты текстур? Читайте дальше и вы найдете ответы на эти и другие вопросы.
    Для начала взгляните на Рисунок 9.7, где изображен пример ввода текста в игре.


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

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


  • Взаимосвязь между переменными состояния и базовыми типами


    Взаимосвязь между переменными состояния и базовыми типами


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

    Взаимосвязь между статическими


    Взаимосвязь между статическими


    На Рисунок 8.27 видно, как динамические данные подразделений из массива m_UnitObjs используют в качестве основы данные, хранящиеся в базовых типах.

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

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

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


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

    Загрузка базовых типов

    У вас есть базовые классы для хранения данных подразделения, но как загрузить в них информацию? Один из способов — жестко задать все значения параметров подразделений в коде программы. Подобное сляпанное наспех решение не позволит создать гибкую систему. Я предпочитаю использовать конфигурационные файлы, которые загружаются во время работы программы. Вы можете редактировать конфигурационные файлы и снова запускать игру без повторной компиляции. Это неоценимое преимущество, поскольку вы наверняка будете менять параметры вашей игры во время разработки. Это также позволяет легко создавать расширения для игры, поскольку для создания новых типов подразделений достаточно изменить значения нескольких параметров в конфигурационных файлах.
    В классе есть функция iLoadBaseTypes(), которая загружает значения из конфигурационных файлов. Перед тем, как перейти к рассмотрению этой функции, взглянем на приведенный ниже код заголовка класса:
    const int UNITMANAGER_MAXBASEOBJS= 256; const int UNITMANAGER_MAXUNITS = 1024; class CUnitManager { public: CUnitDefense *m_DefenseObjs; CUnitOffense *m_OffenseObjs; CUnitMovement *m_MovementObjs; CUnitAnimation *m_AnimationObjs; CUnit *m_UnitBaseObjs; CUnit *m_UnitObjs; int m_iTotalDefObjs; int m_iTotalOffObjs; int m_iTotalMovObjs; int m_iTotalAnimationObjs; int m_iTotalUnitBaseObjs; int m_iTotalUnitObjs; int m_iOwnerTotal[UNITMANAGER_MAXOWNERS]; // Указатель Direct 3D для загрузки текстур LPDIRECT3DDEVICE9 m_pd3dDevice; CUnitManager(); ~CUnitManager(); virtual void vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d); virtual void vReset(void); virtual void vClearMem(void); virtual int iLoadBaseTypes( char *szDefFileName, char *szOffFileName, char *szMovFileName, char *szUnitFileName, char *szAnimFileName); virtual CUnitDefense* ptrGetDefenseType(char *szName); virtual CUnitOffense* ptrGetOffenseType(char *szName); virtual CUnitMovement* ptrGetMoveType(char *szName); virtual CUnitAnimation* ptrGetAnimType(char *szName); virtual int iAddUnit(char *szName, int iOwner); virtual void vRemoveUnit(int iUnitID); virtual int iCountTotalTextures(void); }; Большинство членов данных класса имеет отношение к объектам базовых типов. Поля m_DefenseObjs, m_OffenseObjs, m_MovementObjs, m_AnimationObjs и m_UnitBaseObjs используются как массивы для хранения загружаемых впоследствии базовых типов. Переменные m_iTotalDefObjs, m_iTotalOffObjs, m_iTotalMovObjs, m_iTotalAnimationObjs и m_iTotalUnitBaseObjs отслеживают кличество загруженных в память объектов каждого типа. Это показано на Рисунок 8.22.


    Загрузка и создание подразделений

    Я показал вам как написать классы подразделений, управлять подразделениями и анимировать их. Последняя тема, которую я затрону — загрузка и создание новых подразделений. В рассматриваемом примере программы есть функция с именем vInitializeUnits(). Она отвечает за загрузку информации о базовых типах и добавляет в игру несколько активных подразделений. Вот ее код:
    void CD3DFramework::vInitializeUnits(void) { int iUnit; // Инициализация диспетчера подразделений m_UnitManager.vReset(); // Установка устройства Drect3D m_UnitManager.vSetRenderDevice (m_pd3dDevice); // Импорт базовых данных подразделений m_UnitManager.iLoadBaseTypes( "UnitData\\BaseType_Defense.csv", "UnitData\\BaseType_Offense.csv", "UnitData\\BaseType_Movement.csv", "UnitData\\BaseType_Unit.csv", "UnitData\\BaseType_Animation.csv"); // Добавление нескольких подразделений к "игре" iUnit = m_UnitManager.iAddUnit("Apache Attack Helicopter", 0); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(-180.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Apache Attack Helicopter", 1); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(-70.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Spirit Scout Helicopter", 2); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(50.0f, -80.0f); iUnit = m_UnitManager.iAddUnit("Spirit Scout Helicopter", 3); m_UnitManager.m_UnitObjs[iUnit].vSetPosition(180.0f, -80.0f); } В начале кода я вызываю функцию инициализации диспетчера подразделений. В результате освобождается выделенная ранее память и диспетчер подготавливается к загрузке данных.
    Затем для диспетчера подразделений я устанавливаю указатель на устройство визуализации DirectX. Если вы помните, ранее говорилось, что это необходимо для загрузки текстур.
    Чтобы загрузить подготовленную информацию о подразделениях я вызываю функцию загрузки базовых типов диспетчера. Она получает имена файлов с подготовленными данными и загружает их в базовые типы, которыми управляет объект диспетчера подразделений.
    Пришло время для веселья! Следующая часть кода создает подразделения с помощью функции добавления подразделений. Диспетчер создает и активирует запрошенные подразделения, чтобы они появились в игровом цикле. Сразу после создания каждого подразделения я инициализирую данные о его местоположении, чтобы подразделение появилось в требуемом месте экрана. Обратите внимание, создавая подразделения я назначаю каждому из них собственный цвет владельца. Это позволит вам увидеть как различные цвета владельца отображаются во время визуализации.
    После того как подразделения созданы в диспетчере и активированы, они могут модифицироваться и отображаться согласно вашим пожеланиям. В качестве упражнения попробуйте создать на экране еще несколько сотен подразделений и посмотрите, что получится!


    Загрузка изображений блоков

    Вы уже посмотрели, как программа осуществляет навигацию на карте, но что насчет отображения графики? Программа бесполезна без визуальной обратной связи. Старая добрая функция vInitInterfaceObjects() заботится о загрузке используемых в программе изображений блоков. Вот фрагмент кода, выполняющий этот трюк:
    for(int i = 0; i < 3; i++) { // Установка имени sprintf(szTileName, "tile%d.bmp", i); // Загрузка if(FAILED(D3DXCreateTextureFromFile( g_pd3dDevice, szTileName, &g_pTexture[i]))) { return; } } Загрузка блоков довольно проста. Я просто запускаю цикл, перебирающий доступные блоки и загружающий файлы с именами от tile0.bmp до tile2.bmp. Поскольку загружаются только три блока, процесс выполняется очень быстро. Для последующего использования загруженные блоки сохраняются в массиве g_pTexture.

    Захват клавиатуры

    Последний, относящийся к DirectX этап — вызов функции IDirectInputDevice8::Acquire(). Эта функция необходима для привязки приложения к используемому им устройству ввода. Всякий раз когда окно теряет фокус клавиатура должна быть захвачена снова.

    Аппаратное обеспечение

    ATI — ATI делает самые лучшие видеокарты, и их новая серия Radeon вне конкуренции. Если вы ищете хорошую видеокарту для практики в программировании шейдеров, загляните на www.ati.com.

    Nvidia — Nvidia также выпускает замечательные видеокарты. Лично у меня нет многих из выпускаемых моделей, но они всегда получают хорошие оценки в обзорах. URL: www.nvidia.com.

    собран специально для http://www.natahaus.ru/

    Двухмерная графика

    Adobe Photoshop — это наилучший из имеющихся на рынке редакторов двухмерной графики. Огромная гибкость и возможность сделать с двухмерным изображением все, что вам может потребоваться. URL: www.adobe.com.

    Сообщества разработчиков игр

    GameDev.net — это мой любимый сайт разработчиков игр, содержащий множество статей и великолепный форум. Вы можете найти меня среди модераторов раздела Multiplayer. URL: www.gamedev.net.

    Flip Code — еще один из моих любимых сайтов разработчиков игр с большим количеством великолепных статей и активным сообществом. URL: www.flipcode.com.

    Трехмерная графика

    3ds max — эта программа, созданная компанией Discreet, мой любимый инструмент для трехмерного моделирования и анимации. Вы можете использовать его для создания используемых в игре моделей, их анимации и даже для разработки целых сцен. URL: www.discreet.com.

    LightWave — эту программу, которой я пользуюсь уже несколько лет, создала компания Newtek. Она очень популярна среди разработчиков игр, и я рекомендую вам пристально присмотреться к ней, прежде чем приобрести что-нибудь другое. URL: www.newtek.com.

    B Ресурсы для разработчика

    Существует множество ресурсов для разработчиков, так что здесь я перечислю только свои любимые.
    Двухмерная графика
    Adobe Photoshop — это наилучший из имеющихся на рынке редакторов двухмерной графики. Огромная гибкость и возможность сделать с двухмерным изображением все, что вам может потребоваться. URL: www.a...

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

    Сообщества разработчиков игр
    GameDev.net — это мой любимый сайт разработчиков игр, содержащий множество статей и великолепный форум. Вы можете найти меня среди модераторов раздела Multiplayer. URL: www.gamedev.net. Flip Code...

    Аппаратное обеспечение
    ATI — ATI делает самые лучшие видеокарты, и их новая серия Radeon вне конкуренции. Если вы ищете хорошую видеокарту для практики в программировании шейдеров, загляните на www.ati.com. Nvidia — Nv...



    

        Сайт: Аннимация - Видео - Графика