Обработка перемещающихся подразделений
Последний тип анимации относится к перемещающимся подразделениям. Код, который я поместил в пример выглядит очень похоже на обработку анимации других действий, но в нем отсутствует код для передвижения подразделения по экрану. Я удалил его чтобы максимально упростить пример, но абсолютно ничего не препятствует вам перемещать подразделения. Вот как выглядит код:
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
Обратите внимание на уже ставшие привычными элементы рисунка. Здесь есть панель инструментов, область редактирования и мини-карта. Новым элементом является появившаяся на панели инструментов кнопка Generate. Она очищает всю карту, заполняя ее водой, и создает случайно расположенные участки суши. Кроме того, вы можете заметить, что я увеличил размер мини-карты. Я сделал это для того, чтобы вы более ясно увидели эффект генерации случайной карты. Есть и еще одно изменение, которое нельзя заметить на Рисунок 10.13. Если вы запустите программу D3D_MapEditorGeneration, то увидите красный прямоугольник на мини-карте. Он показывает, какой именно фрагмент большой карты отображается в окне просмотра и помогает понять что именно вы сейчас редактируете.
Теперь загрузите проект D3D_MapEditorGeneration и следуйте за мной, а я буду рассказывать вам о коде программы.
Окно программы D3D_MapEditorLayers
На Рисунок 10.16 показано окно программы D3D_MapEditorLayers. На панели инструментов появились четыре новые кнопки, отмеченные цифрами от 1 до 4. Они позволяют установить активный слой, который будет редактироваться. Например, чтобы редактировать базовый слой, щелкните по кнопке 1. После того, как вы выбрали первый слой, все щелчки по области редактирования будут менять текстуры, выводящиеся в первом слое блоков. Кнопки, отвечающие за другие слои работают аналогично. В изображенном на иллюстрации окне редактирования вы видите маленький песчанный остров с травой в центре. Изображение песка находится в слое 1, а изображение травы — в слое 2. Благодаря этому достигается плавный переход от травы к песку без необходимости вводить специальные переходные блоки с изображением травы и песка. Хватит обсуждать иллюстрацию. Загрузите программу D3D_MapEditorLayers и следуйте вперед.
Окно программы D3D_MapEditorLite
На Рисунок 10.7 изображено окно программы D3D_MapEditorLite. Оно выглядит очень похоже на окно программы просмотра карт, о которой вы уже читали. Главным отличием данной программы является добавление области выбора блоков и возможность редактировать карту в реальном времени.
Теперь загрузите проект D3D_MapEditorLite и следуйте дальше вместе со мной.
Окно программы D3D_MapEditorPlus
На рис 10.10 видно, что я добавил на панель инструментов кнопки Load и Save. Их функции должны быть ясны, поскольку кнопка Load загружает данные карты из указанного файла, а кнопка Save сохраняет данные карты в указанном файле.
Окно программы D3D_MapEditorPlusGold
Правильно, — это старый редактор карт, но теперь — золотое издание! (Я знаю, что название программы становится все более причуддливым, но по крайней мере я не называю ее MapEditor 2700+ или как-нибудь еще в этом роде!)
На Рисунок 10.11 вы видите редактор карт, но теперь в нижнем левом углу есть небольшое окно в котором отображается мини-карта. В действительности это большая карта мира, на которой каждый блок представляется одной точкой. Это позволяет мне отобразить полную карту мира, размером 100 x 100 блоков в окне размером 100 x 100 точек. Загрузите проект D3D_MapEditorPlusGold и я покажу вам изменения, необходимые для реализации отображения мини-карты.
Ход исполнения программы изменился не слишком сильно, но внесенные изменения надо проиллюстрировать. Пожалуйста, посмотрите на Рисунок 10.12, чтобы увидеть ход выполнения программы.
Окно программы D3D_Particles
Обратите внимание на разбросанные по экрану разноцветные частицы. Программа показывает как создать случайный набор частиц и подбрасывать их в воздух. Вы можете назвать это демонстрацией попкорна, я оставляю выбор за вами. Кажется я знаю, что вы думаете сейчас: «Какое это имеет отношение к программированию стратегических игр?». Ответ прост — предполагается, что программа покажет вам как можно быстро начать работать с частицами. Более сложные формирования, такие как взрывы и ударные волны, придут позднее, когда вы освоитесь с основными приемами использования частиц.
Проект состоит из четырех файлов: main.cpp, main.h, CParticle.cpp и CParticle.h. Кроме того, для компиляции потребуются следующие библиотеки: d3d9.lib, dxguid.lib, d3dx9dt.lib и d3dxof.lib.
Окно программы D3D_PathFinding
Запустите программу и щелкните по расположенной на панели команд кнопке Go. В результате будет запущен алгоритм поиска пути. Как видно на Рисунок 12.5 программа ищет путь из начальной точки в конечную и отображает решение в виде стрелок. Вы можете загружать различные ландшафты, находящиеся в сопроводительных файлах, и смотреть, как алгоритм справляется с ними. Самое лучшее, что алгоритм A* всегда находит лучший путь с учетом имеющегося времени и ресурсов.
Как работает программа D3D_PathFinding? Не беспокойтесь; на этот раз я не буду сразу переходить к описанию исходного кода. Вместо этого я сначала приведу теоретическое описание работы алгоритма A*.
Окно программы D3DFrame_UnitTemplate
На Рисунок 8.7 показано окно программы D3DFrame_UnitTemplate, входящей в сопроводительные файлы к книге. На рисунке видны четыре вертолета летящих над травяным полем. В верхнем левом углу окна выводится отладочная информация. Возможно, все это выглядит не слишком впечатляюще, но лежащая в основе программы система управления подразделениями весьма сложна.
Загрузите с компакт-диска проект с именем D3DFrame_UnitTemplate и следуйте за мной дальше. Чтобы создать полнофункциональный шаблон подразделения, вам потребуются следующие классы:
Класс типа защиты Класс типа атаки Класс типа передвижения Класс анимации подразделения Класс текстур подразделения Класс подразделения Класс диспетчера подразделенийОкно программы 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(). Она получает раскладку подключенной к системе клавиатуры для дальнейшего использования.
ПРИМЕЧАНИЕЭтапы, необходимые для инициализации клавиатуры, показаны на Рисунок 9.6.
Реализация системы частиц
Теперь загрузите проект D3D_Particles, если вы еще не сделали этого, и скомпилируйте его. Запустите программу и вы увидите сцену, похожую на ту, что показана на Рисунок 13.3.