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

         

Добавляем к игре звук


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



Краткий обзор


Качественное звуковое сопровождение абсолютно необходимо для полного погружения в игру. Многие согласятся, что качество звукового сопровождения игры очень сильно влияет на впечатление от игры в целом. Достаточно взглянуть на современные игровые консоли. Корпорация Microsoft оснастила свою первую игровую приставку Xbox настоящей поддержкой звукового сопровождения формата 5.1 Dolby Digital. Другие компании выпускают звуковые системы формата 6.1 для PC и целые домашние кинотеатры, которые можно использовать совместно с компьютером. Мы уже далеко ушли от компьютеров с единственным внутренним динамиком, издававшим простое бибиканье. Задача с которой сталкиваетесь вы, разработчик игр, заключается в необходимости включить в свою игру высококачественное звуковое сопровождение. Конечно, вы можете пропустить этот материал и использовать низкокачественный звук, но люди заметят это и не оценят.

Играли ли вы когда нибудь в интересную игру с ужасным звуковым оформлением? Такую, где выстрелы из автомата похожи на выхлопы, а взрывы на фейерверк на ветру? И уж конечно вы не хотите, чтобы люди ответили на эти вопросы «да», подразумевая при этом вашу игру. В этой главе я поговорю о следующих темах, которые помогут вам добавить звук в ваши игры:

Звуковые API Архитектура DirectMusic Как воспроизвести файл WAV Как воспроизвести файл MIDI Как воспроизвести файл MP3 Реализация класса

За кулисами клавиатуры


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



Краткий обзор


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

Обзор DirectInput. Ввод с клавиатуры. Ввод текста в игре.



Редактируем карту мира


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



Краткий обзор


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

Основы редактирования карт. Просмотр карты. Редактирование карты. Сохранение. Загрузка. Отображение мини-карты. Алгоритмы генерации карт. Слои карты.

Поиск пути


Краткий обзор
Вы когда нибудь думали о том, что требуется чтобы из пункта А попасть в пункт В? Просыпались ли вы когда-нибудь в 6 утра, с ужасной головной болью, думая о том, как вы попали в пункт С? Решение эт...



Краткий обзор


Вы когда нибудь думали о том, что требуется чтобы из пункта А попасть в пункт В? Просыпались ли вы когда-нибудь в 6 утра, с ужасной головной болью, думая о том, как вы попали в пункт С? Решение этой, кажущейся на первый взгляд элементарной, задачи достаточно сложно. Хотя существует много способов снять шкуру с кошки, не все они одинаковы. Одни методы требуют больших вычислительных мощностей, а другие дольше выполняются. Ключ здесь — соблюдение баланса между скоростью и использованием процессора. Чтобы помочь вам принять обоснованное решение, относительно того, какой метод использовать, я рассмотрю в этой главе следующие темы:

Задача поиска пути. Простое решение. Поиск пути по алгоритму A*. Реализация в коде.

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


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



Краткий обзор


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

Основные сведения о частицах. Характеристики частиц. Структура класса частицы. Реализация частиц. Реализация частиц.

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


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

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



Члены данных


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

Вектор 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.



Члены данных класса 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.



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


Вернемся к функции 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





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

// Тип защиты 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





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

Вот так, красиво и просто!