- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF85:Ogre
Материал из Linuxformat.
Разработка 3D-игры |
---|
Видеокарта Nvidia GeForce 7800, используемая для разработки этого руководства, была любезно предоставлена MSI. Спасибо, ребята!
Содержание |
Ogre: Добавим вражьих роботов-умников
ЧАСТЬ 4: Наскучило бродить одному по бескрайним просторам? Пол Хадсон подскажет вам, как добавить плохих ребят…
Солнце встает, освещая старый бревенчатый домик, а вода плещется у краев покатого ландшафта — наступает новый день в игре Висельник Чед. За последние три урока мы создали нашему игроку живописный остров, где он может порезвиться. Логичный следующий шаг — населить остров роботами-убийцами. Как, вы в этом не уверены? Тогда пришлите нам запрос на создание учебника по игре «Сажаем цветочки», и мы посмотрим, что можно сделать, а на данном уроке займемся роботами. Сотней здоровенных, вооруженных до зубов, злобных роботов. Да-с.
Наши роботы будут первым шажком на пути к искусственному интеллекту (ИИ), именно здесь и начинается настоящая потеха: неважно, как Ogre работает с 3D-графикой, сам по себе он нечеловечески скучен. Все, что выходит за рамки влияния Ogre, придется писать самим. Поверьте, это забавно!
На сей раз мы создадим новый класс, отслеживающий перемещение роботов. Я не большой любитель объектно-ориентированного программирования — обычно мои классы превращаются в штуки типа struct; и я, скорее всего, не один такой. Кому не по душе моя манера делать все переменные публичными, пишите письма на имя /dev/null.
Рысканье, тангаж и крен (РТК) – известные как угловое вращение Эйлера – так люди представляют себе перемещение в пространстве. Если у вас есть игрушечный самолетик и пара шампуров, то вот как понять эти характеристики:
- Рысканье – глядя на самолет сверху вниз, проткните его через крышу и пол: он сможет вращаться только влево и вправо.
- Тангаж – глядя на самолет сбоку, проткните его с одного борта до другого: он сможет вращаться только вверх и вниз.
- Крен – глядя на самолет спереди, проткните его от носа до хвоста: он сможет вращаться только вправо или влево; учтите, это не то вращение, что при рысканье!
Проблема традиционного РТК-метода работы с 3D-пространством (кроме того, что жалко дырявить самолетик) – здесь иногда случается складывание плоскостей. Такое происходит, когда какой-либо поворот кратен 90 градусам: в результате на вид две степени свободы превращаются в одну. Вам будет легче осознать, в чем дело, если вы взглянете на рисунок справа: внешняя рамка представляет тангаж, средняя – крен, а внутренняя – рысканье. Если средняя рамка провернется еще влево, то есть попадет в одну плоскость с внешней, то внутренняя рамка будет функционально аналогична внешней рамке, и нам покажется, что степень свободы потеряна.
Кватернионы решают эту проблему, определяя вектор (значения X,Y,Z) плюс угол поворота вокруг этого вектора. Поэтому вместо того, чтобы протыкать наш самолет в трех местах, вращая по отдельности, мы протыкаем его произвольным образом. Такой способ позволяет избегать слипания рамок и проводить гладкую интерполяцию между точками – идеально при полетах с кинокамерой.
В процессе создания роботов вы познакомитесь с кватернионами (которые, наряду с ‘shazbat’, являются моим любимым словом), анимацией и Стандартной библиотекой шаблонов С++ (Standard Template Library или STL). Если вы раньше не использовали STL, вам вполне простительно решить, что это плод труда двух комитетов из разных стран, говорящих на разных языках — но, отстранившись от мелочей, вы поймете, как все просто.
Наши враги будут управляться специальным классом, который я назову CChadEnemy, так что создайте файлы chademeny.h и chadenemy. cpp (да позаботьтесь о включении файла chadenemy.cpp в Makefile). Нам требуется, чтобы у робота была своя сущность (каркас робота) и узел сцены, чтобы мы могли помещать его в сцену. Вспомните, как мы передвигали игрока (LXF83): используя луч для нахождения высоты игрока над ландшафтом. Надо придумать что-то подобное для каждого из наших врагов, поэтому вместо указания позиций в chad.cpp применим метод SetPos().
Вот файл chad.h:
#include "Ogre." #include "OgreFrameListener.h" #include "OgreEventListeners.h" #include "OgreKeyEvent.h" using namespace Ogre; class CChadEnemy { public: SceneManager* m_SceneMgr; Entity* m_Entity; SceneNode* m_Node; int m_Speed; CChadEnemy(SceneManager* scenemgr); void SetPos(Vector3 dest); };
Каждый враг создается своим конструктором, CChadEnemy(). В нем будет происходить загрузка каркаса, создание узла сцены, затем добавление в менеджер сцены (вы заметили указатель на менеджер сцены в chad.h?). Каркас робота предоставляется Ogre и должен находится в вашем каталоге с медиа-ресурсами, но мы уменьшим размер робота вчетверо, вот этот код:
CChadEnemy::CChadEnemy(SceneManager* scenemgr) { m_SceneMgr = scenemgr; static int EnemyNum = 0; char enemyname[32]; sprintf (enemyname, "Robot %d", ++EnemyNum); m_Entity = m_SceneMgr->createEntity(enemyname,"robot.mesh"); m_Node = m_SceneMgr->getRootSceneNode()->createChildSceneNode(); m_Node->attachObject(m_Entity); m_Node->scale(0.25,0.25,0.25); m_Speed = 5; }
Строки с EnemyNum, enemyname и sprintf() раздражают своим уродством, но для Ogre это в порядке вещей. Проблема здесь в том, что каждой загружаемой сущности требуется дать уникальное имя, а достичь этого можно, отслеживая, сколько врагов уже создано с помощью переменной EnemyNum, и добавляя это значение к слову Robot. EnemyNum объявлена как статическая переменная, то есть ее разделяют все экземпляры класса.
Я не собираюсь печатать метод SetPos(), потому что по большей части он идентичен методу SetPos() для игрока; вы можете просто его скачать из исходного кода c web-сайта Linux Format[1].
Стандартная библиотека шаблонов
Теперь у нас хватает кода, чтобы населить остров врагами-роботами, но сначала надо придумать, как их хранить. Метод должен быть гибким: незачем объявлять переменную для каждого робота или заводить статические массивы, потому что желательно создавать роботов по запросу. Решение состоит в использовании STL: это коллекция стандартных абстрактных типов данных для C++. Пусть слово «абстрактные» вас не пугает: STL чрезвычайно полезна, а ее типы данных обычно легко представить.
Для управления врагами нам потребуется тип данных «вектор» - массив некоторых величин, в котором нас интересует только их значение. Добавление «вражеского» вектора потребует сделать два изменения в chad.h. Вставьте следующие две строчки после директив #include:
#include "chadenemy.h" #include <vector>
Теперь вставьте эту строку в конце определения класса CChadGame:
std::vector<CChadEnemy*> Enemies;
Далее припишите к методу createOutdoorScene() следующий код:
for (int i = 0; i < 100; ++i) { CChadEnemy* enemy = new CChadEnemy(m_SceneMgr); enemy->SetPos(Vector3(rand() % 1000,0, rand() % 1000)); Enemies.push_back(enemy); }
Этот код создаст 100 новых врагов, задаст им произвольные позиции на карте (Y установлен равным 0: метод SetPos() потом переопределит это значение с учетом высоты местности), затем добавляет врагов в вектор Enemies. Если вашей системе больше трех лет от роду, врагов лучше взять поменьше. Вот и все, что потребовалось для добавления роботов — теперь скомпилируйте игру и запустите ее!
Двигаемся дальше
Мы привыкли перемещать камеру, но в этот раз я прикрепил ее к собственному узлу сцены и передвигал его. Такой ход позволит в будущем проделывать интересные вещи, например, вызывать метод lookAt() для других узлов, чтобы они взглянули в камеру.
Наши роботы стоят столбом, напоминая батарейки из рекламы Duracell, которые демонстрируют, что другие уже рухнули. Попробуем их оживить: пусть выберут какой-то пункт назначения на карте и отправятся туда, обуреваемые злыми намерениями. Для этого нам потребуется внести следующие изменения:
- Назначить каждому роботу точку назначения и направление.
- Сказать каждому роботу, сколько времени прошло с его последнего перемещения (поэтому они двигаются с постоянной скоростью, независимо от скорости вашего компьютера).
- Велеть роботам перемещаться при начале каждого кадра.
Мы можем разрешить первые два пункта, отредактировав файл chadenemy.h. Добавьте две строки после определения m_Speed:
Vector3 m_Direction; Vector3 m_Destination;
Также вам понадобится дописать определение следующего метода в определении класса:
void Update(Real time);
Если вы обнаружили, что анимация не работает, проверьте, вызывается ли метод addTime() для каждого робота при вызове его метода Update(). Без этого анимация обновляться не будет: будет проигрываться один и тот же кадр.
Мы будем вызывать метод Update() для каждого робота каждый раз, как только начнется кадр, поэтому робот сможет перемещать сам себя. Временной параметр сообщает роботам, сколько времени прошло с момента отрисовки последнего кадра. Ogre об этом позаботился: взгляните на метод CChadGame::frameStarted() в файле chad.cpp, и увидите следующее:
bool CChadGame::frameStarted(const FrameEvent& evt) {
Параметр FrameEvent сообщает множество характеристик текущего кадра, включая время, прошедшее между кадрами. Проблема состоит в том, что мы затем вызываем frameStartedOutside() или frameStartredInside() и не передаем этот параметр. Поэтому откройте файл chad.h и измените их, чтобы они соответствовали прототипам:
bool frameStartedOutside(const FrameEvent& evt); bool frameStartedInside(const FrameEvent& evt);
Вам также понадобится отредактировать файл chad.cpp. Метод frameStarted() необходимо подправить, чтобы он передавал параметр требуемому методу, например:
bool CChadGame::frameStarted(const FrameEvent& evt) { switch (scenemanager) { case ST_EXTERIOR_CLOSE: return frameStartedOutside(evt); break; case ST_INTERIOR: return frameStartedInside(evt); break; } return true; }
Каждый раз, когда кадр начинается вне дома, необходимо обновить всех наших роботов с помощью метода Update(). В части кода, получается, надо пройти в цикле по вектору Enemies() и передать данные о кадре, предоставляемые Ogre. Поэтому в конце метода frameStartedOutside() (перед строкой ‘return line’) добавьте небольшой цикл:
for (std::vector<CChadEnemy*>::iterator iter = Enemies.begin(); iter != Enemies.end(); ++iter) { CChadEnemy* enemy = (*iter); enemy->Update(evt.timeSinceLastFrame); }
Здесь STL применяется в большей степени, и наш код напоминает синтаксическую окрошку. На самом деле этот цикл проходит от начала до конца вектора, используя STL-итератор. Можно, конечно, получить прямой доступ к элементам вектора через индексы, но итераторы более предпочтительны. Для каждого врага в нашем векторе мы вызываем метод Update() и посылаем отрезок времени, прошедший с прошлого кадра. Именно тут наши роботы должны проделать всё, что им полагается — сейчас мы просто заставим их двигаться.
Пора написать сам метод Update(). Вставьте следующий код в конец файла chadenemy.cpp:
void CChadEnemy::Update(Real time) { if (m_Destination == Vector3::ZERO) { m_Destination = Vector3(rand() % 1000,0, rand() % 1000); m_Direction = m_Destination - m_Node->getPosition(); } else { Vector3 ourpos = m_Node->getPosition(); ourpos.y = 0.0f; m_Direction = m_Destination - ourpos; Real dist = m_Direction.normalise(); Real movespeed = m_Speed * time; if (dist < movespeed) { m_Destination = Vector3::ZERO; } else { // необходимо переместиться ближе к точке назначения Vector3 newpos = m_Direction * movespeed; SetPos(newpos); } } }
Код включает две ветви: если врагу уже задано направление движения, то он по нему и движется, если же нет, направление выбирается произвольно. Ветвь определяется в самом начале: если вектор m_Destination равен Vector::ZERO (по умолчанию, пустой вектор), то необходим выбор точки назначения. Зная эту точку, мы можем вычислить направление простым вычитанием координат текущей позиции (все векторные операции Ogre берет на себя).
Пункт назначения: Чед
Если роботу задан путь, то мы берем его текущую позицию и точку назначения, затем пересчитываем направление. Это не обязательно, потому что направление использует числа с плавающей запятой: достаточно посчитать его один раз и забыть о нем. Но и вреда в этом нет: робот будет корректировать свою позицию при каждом кадре, и придет точно в заданный пункт независимо от расстояния до него.
Мы можем рассчитать длину вектора, вызвав метод normalise(), который вернет нормализованный вектор — имеющий то же направление, но длина его равна 1. Штука хорошая, но на самом-то деле нас интересует значение длины исходного вектора, которое также возвращается методом normalise и в нашем коде является расстоянием от робота до точки назначения. Сохраним это значение в переменной dest.
Теперь — важный момент: если расстояние до точки назначения короче, чем расстояние, на которое собирается переместиться робот (его скорость, помноженная на время, прошедшее с прошлого кадра), значит, он подобрался очень близко, и нам необходимо выбрать новое направление. Это можно сделать, установив вектор m_Destination равным Vector3::Zero, тогда при следующем вызове метода робот случайным образом выберет другую точку назначения.
Если расстояние до пункта назначения больше, чем величина перемещения робота, помножим направление на скорость робота и передвинем его на следующую позицию. Вектор направления уже был нормализован, поэтому все будет работать.
Вот и все — запустите новый код и наслаждайтесь реалистичностью перемещений ваших роботов по ландшафту!
Идем верным путем
Ну, пожалуй, «реалистичность» — слишком сильно сказано: на самом деле роботы просто скользят, по причине отсутствия кода для анимации. Да и вообще, нет ничего, обуславливающего должное направление их перемещения. Давайте это исправим!
В файле chadenemy.h добавьте две строки, перед вектором m_Direction:
AnimationState* m_AnimationState;
bool WatchingPlayer;
AnimationState представляет собой тип данных Ogre, ответственный за анимацию сущности: проигрывать ли ее бесконечно, и так далее. Мы хотим, чтобы наши роботы по умолчанию использовали анимацию ‘Idle’, поэтому скопируйте следующий код в конструктор класса CChadEnemy:
m_AnimationState = m_Entity->getAnimationState("Idle"); m_AnimationState->setLoop(true); m_AnimationState->setEnabled(true); WatchingPlayer = true;
Переменная WatchingPlayer будет первым кусочком искусственного интеллекта: если робот приблизится к игроку на определенное расстояние, пусть остановится и посмотрит на игрока. По умолчанию такая опция установлена для каждого робота; вы скоро узнаете, зачем.
У нашего робота имеются и другие анимации, например, ‘Shoot’ [Стрелять] и ‘Die’ [Умирать]. Их можно использовать аналогично анимациям ‘Idle’ [Безделье] и ‘Walk’ [Ходьба], вот и попробуйте их!
Осталось изменить метод Update(). Первое, что необходимо сделать — обновить анимацию робота, этим займется метод addTime() нашей переменной m_AnimationState(). Он принимает в качестве параметра время, прошедшее с прошлого кадра, которое мы уже знаем, поэтому поместите следующую строку в начале метода Update():
m_AnimationState->addTime(time);
Далее нам необходимо поместить код, вычисляющий расстояние между текущим роботом и игроком и заставляющий робота предпринять соответствующее действие, если поблизости находится игрок: бросить вышеупомянутый злобный взгляд. Добавьте следующий код (после проверки Vector3::ZERO) внутри другого блока if:
Vector3 campos = m_SceneMgr->getSceneNode("camnode")->getPosition(); Vector3 ourpos = m_Node->getPosition(); Vector3 playerdist = ourpos - campos; if (playerdist.normalise() < 100) { if (!WatchingPlayer) { m_AnimationState = m_Entity->getAnimationState("Idle"); m_AnimationState->setLoop(true); m_AnimationState->setEnabled(true); WatchingPlayer = true; } Vector3 orient = m_Node->getOrientation() * Vector3::UNIT_X; Vector3 dir = campos - m_Node->getPosition(); dir.y = 0; Ogre::Quaternion quat = orient.getRotationTo(dir); m_Node->rotate(quat); } else { if (m_Destination == Vector3::ZERO) { // здесь почти ничего не изменилось SetPos(newpos); } // а это новый код if (WatchingPlayer) { m_AnimationState = m_Entity->getAnimationState("Walk"); m_AnimationState->setLoop(true); m_AnimationState->setEnabled(true); WatchingPlayer = false; } }
- Назначьте каждому роботу персональную скорость.
- Ваши роботы идут в некотором направлении и смотрят на игрока, когда он в зоне досягаемости. Теперь заставьте их ходить за игроком по пятам!
- Более опытные программисты могут попробовать выставить направление следующего робота по предыдущему, а первый робот выбирает произвольное направление.
Итак, если робот невдалеке от игрока, мы заставляем его взглянуть, если только он уже не глядит (чтобы избежать лишних вызовов функции). Задача заставить робота смотреть на игрока представляет собой лишь вопрос получения ориентации робота, нахождению направляющего вектора между нами и роботом, а затем преобразованию его в кватернион, используя метод getRotationTo() вектора. Это позволит нам развернуть робота лицом к игроку, передав кватернион в метод rotate().
Если робот находится далеко, выполним проверку с вектором Vector3::ZERO, за которым следует много кода, заканчивающегося вызовом SetPos(). Тут все осталось без изменений, но после этого нам надо проверить, смотрел ли робот на предыдущем шаге на игрока, и если да, то заставить робота снова шагать. Теперь вы понимаете, почему все роботы по умолчанию видят игрока: первый раз при проходе по этому методу, его позиция будет установлена правильно, а анимация работать.
Лимит на код в этом уроке исчерпан. Прокрутите свою игру: мы добавили врагов, передвижение, анимацию и простенький искусственный интеллект — не так уж плохо для одного урока!
- ↑ Код этого урока можно найти на: http://www.linuxformat.co.uk/mag/hangingchad_lxf85.tar.gz
Категории: Учебники | Игрострой | Ogre | Пол Хадсон