Журнал LinuxFormat - перейти на главную

LXF83:Ogre

Материал из Linuxformat
Перейти к: навигация, поиск
Разработка 3D-игры
БЛАГОДАРНОСТЬ

Видеокарта Nvidia GeForce 7800, используемая для разработки этого руководства, была любезно предоставлена MSI. Спасибо, ребята!

Содержание

Ogre. Добавь движенья и воды!

ЧАСТЬ 2 Что толку от изменяющегося ландшафта, если вы прикованы к месту? Пол Хадсон знает ответ.

В прошлый раз мы начали работать над игрой – кандидатом в самые продаваемые в 2007 году: Висельник Чед. Мы поместили нашего героя Чеда Холла на автоматически сгенерированный ландшафт под пустым небосводом. Он готов убегать от полицейских. Но из-за пространственных ограничений мы не дали ему возможности перемещаться по игровому полю – он может только осмотреться вокруг при помощи игрока, держащего мышь. Какой преступник не может убежать от полиции? Только совершенно бестолковый. Значит, надо сделать его подвижным: пусть носится по территории. Итак, ввод с клавиатуры будет преобразовываться в движение по горизонтали и по вертикали в соответствии с рельефом.

Театр Буфф(еризации)

Для начала добавим поддержку стандартных клавиш WASD. Нам самим необходимо перехватывать нажатия клавиш, а затем передавать их в Ogre, чтобы он соответственным образом переместил камеру (помните, что это стрелялка от первого лица, поэтому на самом деле мы Чеда не видим). В прошлый раз мы уже обработали клавишу Escape, поэтому вы уже примерно представляете, как написать новую часть кода. Чтобы было немного понятнее, я объединил классы CChadFrameListner и CChadGame в один класс (CChadGame). Теперь этот класс занимается и обработкой кадра, и вводом от клавиатуры, а в конце концов будет также обрабатывать механизмы игры – надеюсь, это сделает код более читабельным, ценой легкой объектной инкапсуляции!

Для начала необходимо буферизовать ввод Ogre. Это значит, что если кто-то лупит по клавишам быстрее, чем Ogre успевает обработать, то Ogre сможет отследить, сколько раз была нажата клавиша, а затем осуществить обработку нажатий как можно скорее – даже в случае, когда клавиша была позже отжата. Например, если некий особо продвинутый игрок путешествует по нашему игровому миру, совершая хитрые трюки и маневры, чтобы побить других игроков, он может превысить число вводов в единицу времени, поддающееся обработке. Если мы будем буферизовать все, то игрок, может, пальнет и на 10 миллисекунд позже, чем планировал, но все-таки пальнет!

Достигнуть этого можно всего одной строкой кода Ogre. Добавьте следующее в файл chad.h в конструкторе, после остальных строк:

m_InputDevice->setBufferedInput(true, true);

Теперь займемся нажимаемыми клавишами – для них потребуется несколько вызовов метода isKeyDown(). Создадим вектор, задающий направление перехода (просто линия, соединяющая конечную и начальную точки); мы можем сказать Ogre, что хотим прибавить или вычесть смещение по X, Y и Z. Связав эту операцию с нажатием клавиш, мы и получим движение.

Если вы боитесь, что это сложно, расслабьтесь – я представлю вам восхитительные 6 строк, которые выполняют всю работу. Вставьте их перед return true в методе frameStarted() заголовка chadframelistner.h.

Vector3 translateVector = Vector3::ZERO;
if (m_InputReader->isKeyDown(KC_W)) translateVector.z = -0.1f;
if (m_InputReader->isKeyDown(KC_S)) translateVector.z = +0.1f;
if (m_InputReader->isKeyDown(KC_A)) translateVector.x = -0.1f;
if (m_InputReader->isKeyDown(KC_D)) translateVector.x = +0.1f;
m_Camera->moveRelative(translateVector);

Сохраните, наберите make и запустите игру – наслаждайтесь новообретенной свободой!

Птица высокого полета

Почему ввод Ogre так несносен

Поэкспериментировав со вводом Ogre, вы заметите одну вещь: он капризный, непредсказуемый, простецкий и в целом невыносим. Как случилось, что лучший в мире инструментарий разработки игр имеет столь хромую систему ввода? Ответ прост: Ogre не претендует на роль инструментария для игр, занимаясь графикой, графикой и еще раз графикой – система ввода была дана разработчикам игр исключительно для создания кросс-платформенных демо. Будьте спокойны: как только понадобится более продвинутый вариант ввода, мы тут же оставим систему Ogre, но пока ее вполне достаточно для путешествий по пространству этаким одиноким облаком.

Передвигаясь, вы столкнетесь с тремя большими проблемами:

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

Первые две проблемы можно решить, отслеживая позицию игрока относительно ландшафта и автоматически сопоставляя высоту камеры и высоту ландшафта. Не самым худшим будет чтение файла ландшафта для извлечения точной высоты нашей текущей позиции. Реализация этой идеи сопряжена с трудностями, потому что ландшафт описан как набор точек и их высот, которые интерполируются для создания гладкой поверхности: пусть точка (0,0) имеет высоту 0, а точка (0,1) высоту 1 – все работает гладко, если игрок решит перейти из точки (0,0) в (0,1). Но в стрелялке от первого лица передвижение измеряется не в целочисленных единицах – игрок может перейти в точку (0,0.1239), а для этого значения в файле данных нет.

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

Это немного сложнее, но Ogre предоставляет некоторые необходимые ресурсы. Когда начинается кадр, вызывается метод frameStarted (который теперь находится в файле chad.cpp, т.к. довольно быстро разрастается!), и мы обрабатываем нажатия клавиш. Передвинув камеру, надо проверить расстояние до земли и сделать соответствующие поправки.

Ogre предоставляет класс Ray, экземпляр которого можно создать, задав начало луча и направление. Начало задается просто координатами X,Z нашей камеры (координата Y должна быть довольно большой, чтобы гарантировать, что мы находимся над землей), а направление – вниз. Затем мы создаем объект RaySceneObject, который и проведет проверку на пересечение с землей – мы просто передадим объекту луч, созданный ранее, и велим ему выполнить запрос.

По выполнении запроса мы беремся за пересечение (если оно есть) и устанавливаем высоту камеры соответственно высоте ландшафта. Наконец, надо освободить память, занимаемую запросом, с помощью метода destroyQuery(). Если вы думаете, что это не так легко, вы правы. Но по крайней мере этим не придется заниматься часто. Добавьте следующий код до выражения return true в методе frameStarted():

Vector3 campos = m_Camera->getPosition();
Ray camray(Vector3(campos.x, 1000.0f, campos.z), Vector3::NEGATIVE_UNIT_Y);
RaySceneQuery* query = m_SceneMgr->createRayQuery(Ray() );
query->setRay(camray);
RaySceneQueryResult &result = query->execute();
RaySceneQueryResult::iterator iter = result.begin();
if (iter != result.end() && iter->worldFragment) {
  float terrainheight = iter->worldFragment->singleIntersection.y;
  if ((terrainheight + 3.0f) != campos.y) m_Camera->setPosition(campos.x, terrainheight + 3.0f, campos.z);
  }
m_SceneMgr->destroyQuery(query);

Перекомпилируйте. Вы увидите, что теперь две первые проблемы решены! Ура!

Водный мир

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

Используем для нашего океана плоскость, на которую мы можем наложить любой материал (так же, как было с небом). Мы проделаем это за пять шагов, с каждым из которых наша вода будет становиться все привлекательнее.

Сначала необходимо создать плоскость, а затем наложить на нее одну из предустановленных в Ogre водных текстур. Для создания плоскости используем класс Plane и метод createPlane(), затем создадим объект Ogre и добавим плоскость к сцене. Вот этот код:

Entity* OceanEntity;
Plane OceanPlane;
OceanPlane.normal = Vector3::UNIT_Y;
MeshManager::getSingleton().createPlane(
  "OceanPlane",
      ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
      OceanPlane,
      16000, 16000,
      10, 10,
      true, 1,
      50, 50,
      Vector3::UNIT_Z);
OceanEntity = m_SceneMgr->createEntity("water", "OceanPlane");
OceanEntity->setMaterialName("Examples/TextureEffect2");
OceanNode = m_SceneMgr->getRootSceneNode()->createChildScene
Node("OceanNode");
OceanNode->attachObject(OceanEntity)

Также вам понадобится добавить одну строчку в конец CChadGame в chad.h:

SceneNode* OceanNode;

Подойдя к границе острова, вы увидите простирающийся океан, представленной простой текстурой . Для первой попытки неплохо, но пора создать нашу собственную настраиваемую текстуру воды. Создайте новый файл chad.material и введите

material Chad/Water
{
 technique
 {
  pass
  {
   ambient 0.2 0.5 0.8
   texture_unit
   {
    texture Water02.jpg
    scroll_anim 0.02 0
   }
  }
 }
}

Водная текстура та же, что и до этого, но взято меньшее значение scroll_anim, чтобы вода двигалась не так быстро. Также устанавливается синий цвет среды, чтобы вода стала более темной (Рис. 2). В resources.cfg проверьте, что за строкой [General] идет:

FileSystem=.

Это позволит нам загружать материалы и другие ресурсы из рабочего каталога нашей игры, поэтому теперь можно использовать chad.material. Вернемся к файлу chad.cpp: найдите строку

OceanEntity->setMaterialName("Examples/TextureEffect2");

и замените ее на

OceanEntity->setMaterialName("Chad/Water");

Перекомпилируйте и увидите, что океан стал малость (ну самую малость) реалистичнее.

Следующий шаг – использование материала из двух текстур, одна из которых будет двигаться по прямой, а вторая будет менять свое направление во время движения. Вместе они придают более естественный вид воде и больше реализма игре. В chad.material добавьте второй блок texture_unit сразу же после первого:

texture_unit
{
   texture Water02.jpg
   wave_xform scroll_y sine 0 0.1 0 0.1
}

Так как мы меняли только файл с материалом, запускать make необходимости нет: просто наберите chad – и увидите новую воду.

На следующем этапе, специально для любителей горных омутов, поднимем нашу воду над землей так, чтобы она затопила низины ландшафта и образовала бассейны (Рис. 3). Это очень просто – добавьте одну строчку в конец метода createScene():

OceanNode->translate(2000, 20, 0);

И последнее изменение: добавим воде немного движения, напоминающего легкий прилив/отлив. Реализацию можно осуществить множеством хороших, продвинутых и сложных способов – но мы этого делать не будем! На самом деле наша плоскость с водой будет подниматься и опускаться каждый кадр. Некоторые норовят написать для этого не меньше десяти строк кода, хотя на самом деле нужно всего три. Вот первая – добавьте ее под CChadGame в файле chad.h, но до определения OceanNode:

double m_OceanFlowNum;

Теперь добавьте следующие две строки в конец frameStarted() в файле chad.cpp перед выражением return true:

OceanNode->translate(0, sin(m_OceanFlowNum) / 2000, 0);
m_OceanFlowNum += 0.001;

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

Берем еще одну планку

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

Заинтригованы? Отлично. Создайте новый файл chadplayer.h и запишите в него следующее:

class CChadPlayer {
public:
  CChadPlayer();
  float getSpeed();
  bool m_IsRunning;
  bool m_IsCrouching;
};

Это определение простого класса игрока; позже мы добавим и другие элементы. Сейчас он содержит только метод getSpeed(), который возвращает скорость, с которой надо передвинуть игрока, а также две булевые переменные, отслеживающие наше состояние – бежим или ползем. Реализация этого класса, chadplayer.cpp, выглядит так:

#include "chadplayer.h"
CChadPlayer::CChadPlayer() {
  m_IsRunning = false;
  m_IsCrouching = false;
}
float CChadPlayer::getSpeed() {
  if (m_IsCrouching) return 0.005f;
  if (m_IsRunning) return 0.2f;
  return 0.1f;
}

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

Следующим шагом создадим и удалим объект игрока внутри chad.cpp, поэтому добавьте эту строку после объявления m_Viewport в chad.h...

CChadPlayer* m_Player;

...затем в chad.cpp добавьте строку в начало метода run()...

m_Player = new CChadPlayer();

...и, наконец, строку в деструктор ~CChadGame() в chad.cpp

delete m_Player;

Мы добавили код для игрока, но чтобы игра заново скомпилировалась, необходимо подредактировать Makefile так, чтобы chadplayer.cpp оказался в списке сразу после chad.cpp в цели all.

Теперь надо изменить код, отвечающий за передвижение игрока в методе frameStarted(), чтобы он использовал метод getSpeed(). Самым простым будет вызвать getSpeed() четыре раза, но гораздо лучше использовать промежуточную переменную, например:

float playerspeed = m_Player->getSpeed();
if (m_InputReader->isKeyDown(KC_W)) translateVector.z = -playerspeed;
if (m_InputReader->isKeyDown(KC_S)) translateVector.z = +playerspeed;
if (m_InputReader->isKeyDown(KC_A)) translateVector.x = -playerspeed;
if (m_InputReader->isKeyDown(KC_D)) translateVector.x = +playerspeed;

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

Идем верным путем

Тестируем воду

На каждом новом уроке мы будем увеличивать реализм игры, чтобы игрок все больше втягивался в игровой мир. Но, пытаясь быть реалистичным, вы столкнетесь с проблемой, похожей на ту, что мы получили с водой – что считать реализмом? Какого цвета должна быть вода – синего, зеленого, смешанного или просто прозрачной? Должны ли отдаленные предметы заволакиваться дымкой? Если да, то насколько? Если вы живете в правильном месте, можете выйти наружу и посмотреть, какие цвета и настройки использовать для игры; увы, для большинства из нас это роскошь. Но не горюйте: иногда реализм не означает приближения к реальной жизни. На самом деле необходимо выдерживать стереотип, который сложился у сотен людей, листавших каталоги путешествий. Сделайте вашу воду синей (или зеленой), на собственный вкус, лишь бы она казалась реальной лично вам и не мешала продолжать разработку – только это нам и важно.

Это сравнительно небольшое изменение, но оно действительно придаст игре достойный вид. Мы собираемся отслеживать нажатие левого Ctrl (ползти) и левого Shift (бежать). Придется немного поработать, потому что надо расширить класс CChadGame, чтобы реализовать класс KeyListener (а также классы FrameListener, MouseListner, MouseMotionListener).

Шаг первый: добавьте в проект файл OgreKeyEvent.h. Он позволит определять, какая клавиша была нажата. Добавьте следующий код рядом с другими выражениями #include в chad.h:

#include "OgreKeyEvent.h"

Ниже вы увидите строку:

class CChadGame : public FrameListener, public MouseListener, public MouseMotionListener {

Добавьте после MouseMotionListner запятую, а затем KeyListner.

Идем дальше вниз – перед строкой Root* m_Ogre добавьте эти три строки:

void keyPressed(KeyEvent *e);
void keyReleased(KeyEvent *e);
void keyClicked(KeyEvent *e) { }

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

Покончив с файлом chad.h, переходим к chad.cpp, в котором нам надо реализовать методы keyPressed() и keyReleased():

void CChadGame::keyPressed(KeyEvent *e) {
  printf("Key pressed!\n");
  switch (e->getKey()) {
    case KC_LCONTROL:
    m_Player->m_IsCrouching = true;
    break;
    case KC_LSHIFT:
    m_Player->m_IsRunning = true;
    break;
  }
}
void CChadGame::keyReleased(KeyEvent *e) {
  switch (e->getKey()) {
    case KC_LCONTROL:
    m_Player->m_IsCrouching = false;
    break;
    case KC_LSHIFT:
    m_Player->m_IsRunning = false;
    break;
  }
}

Не думаю, что стоит комментировать этот код, но на всякий случай скажу, что KC_LCONTROL означает 'код клавиши левого Ctrl', а KC_LSHIFT означает 'код левого Shift'.

Далее найдите метод addFrameListner(). Сразу же за ним добавьте эту строчку:

m_Ogre->addKeyListener(this);

Сохраните все файлы и запустите make – теперь вы играете в полноценную игру: можете и бегать, и на четвереньках ползти, наблюдать волны и даже наслаждаться простым обнаружением столкновений. Система ввода Ogre хотя и слабовата, но работает, и поэтому мы можем в следующем номере заняться чем-нибудь поинтереснее. Присоединяйтесь!

Персональные инструменты
купить
подписаться
Яндекс.Метрика