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

LXF86:Ogre

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

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

Содержание

Ogre: Лазеры и звук

ЧАСТЬ 5: Последний урок в данной серии – музыка для ушей Пола Хадсона: под такую сподручно убивать роботов.

На данном этапе наша игра содержит все базовые элементы стрелялки от первого лица, но имеет легкий недостаток: п одстрелить-то вы никого и не можете. Пожалуй, это скорее тяжелый недостаток, если учесть, что Висельник Чед для стрельбы и задуман. Не хватает также звука и музыки, да и прицела оружия, чтоб видеть, куда мы стреляем. А фанаты С++, наверно, заметили, что отсутствует какое-либо высвобождение памяти.

Изящно завершим Висельника Чеда: реализуем все эти элементы на следующих четырех страницах, и притом запросто – обещаю!

Включаем громкость

Имя Ogre не дает забыть о сильных сторонах программы: это акроним, означающий Объектно-ориентированный Графический Движок Рендеринга [Object-Oriented Graphics Rendering Engine]. Звук – как для эффектов, так и фоновый – не принимается в расчет и, согласно разработчикам Ogre, приниматься не будет. Но это не проблема, благодаря библиотеке SDL и ее расширению SDL_Mixer: вместе они позаботились о поддержке аудио. Если вы следили за нашими уроками с LXF82, то уже установили библиотеки libsdl-devel и libsdl-mixer-devel; а те, кто этот номер пропустил, пусть начнут с их установки, иначе код данного урока работать не будет. Прежде всего, надо изменить файл chad.h, объявив в нем звуковые файлы стрельбы (я использую laser1.wav) и музыкального фона (tipperary.mp3). В SDL-терминах это Mix_Chunk и Mix_Music соответственно, поэтому добавьте две строки в конец класса CChadGame:

Mix_Chunk* m_MixFire;
Mix_Music* m_Music;

Загрузка нашего аудиоматериала осуществляется в файле chad. cpp, в методе initialise(). В конец этого метода (т .е. после установки m_SceneMgr в NULL), добавьте следующие четыре строки:

SDL_Init(SDL_INIT_AUDIO);
Mix_OpenAudio(44100, AUDIO_S16SYS, 2, 2048);
m_MixFire = Mix_LoadWAV("laser1.wav");
m_Music = Mix_LoadMUS("tipperary.mp3");

Первая строка инициирует поддержку звука, потому она и идет первой. Функция SDL_Init() сообщает SDL, какие части вы хотите использовать – графику, звук, ввод, таймеры и т.д., обычно через передачу списка констант, объединенных оператором ИЛИ – например, SDL_INIT_AUDIO | SDL_INIT_TIMER | SDL_INIT_CDROM. Для инициализации всех доступных в библиотеке подсистем (это изрядная расточительность, если вы не намерены все их использовать!), просто укажите SDL_INIT_EVERYTHING. Инициализировав звуковую подсистему SDL, можно открывать звуковое устройство. Это делает функция Mix_OpenAudio(): у нее четыре параметра, определяющих свойства звука. Первый параметр – частота дискретизации: 44100 соответствует CD-качеству; чтобы игра лучше работала на старых компьютерах, попробуйте уменьшить ее до 22050. Второй параметр – формат сэмпла (AUDIO_S16SYS означает 16 бит, какой байт старший – определяется системой), третий – количество каналов (1 для моно, 2 для стерео), а четвертый определяет размер буфера для проигрывания звука. Вам эти параметры ни о чем не говорят? Можете их проигнорировать. Просто скопируйте и вставьте приведенную мной строку кода и больше о ней не вспоминайте. Магия!

Запускаем звуковые файлы

Настроив звуковую систему, мы, наконец, можем заказывать наши звук и музыку. SDL_Mixer берет на себя их загрузку, а вам остается сделать два вызова функций Mix_LoadWAV() и Mix_LoadMSU(). Они принимают имя загружаемого файла и автоматически обрабатывают множество популярных форматов – WAV, MP3, OGG, MID и MOD, но если ваш дистрибутив не поддерживает формат MP3, то SDL, скорее всего, не сможет его проиграть. Кому интересно, общедоступную запись tipperary.mp3 я нашел в Сети – она совершенно не подходит для игры [«Путь далекий до Типперери» – популярная песенка английских солдат времен I Мировой войны, – прим. ред.], поэтому вы уж сами подберите нужный файл! Чтобы покончить с поддержкой звука, остается еще два шага. Добавьте в методе frameStarted() следующие три строки кода:

if (!Mix_PlayingMusic()) {
 Mix_PlayMusic(m_Music, 0);
}

Я не собираюсь вас унижать, объясняя этот код, кроме 0 в конце: это число повторов нашей мелодии [0 значит, что она будет проиграна всего один раз, без повтора, – прим. ред.]. Добавление звука лазера потребует немного мозгов, поскольку потребуется определить метод mousePressed(). В настоящий момент он пуст и сидит в chad.h. Заменим «заглушку» в chad.h на прототип и напишем реализацию этого метода в chad.cpp (чтобы лазер зазвучал). В chad.h, превратим строку....

void mousePressed(MouseEvent* e) { }
... в...
void mousePressed(MouseEvent* e);

Тело этого метода надо поместить где-то в файле chad.cpp:

void CChadGame::mousePressed(MouseEvent* e) {
  Mix_PlayChannel(-1, m_MixFire, 0);
}

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

Прицел

Если вы не снайпер сразу после дембеля, то вряд ли поражение цели на дальней дистанции покажется вам несложным. Для упрощения этой задачи многие игры содержат на экране небольшой прицел. Добавим и мы прицел в виде точки в игру Висельник Чед. Для этого необходимо проделать три шага: 1 Создать материал, который даст имя файлу. 2 Создать слой, который использует материал, и позиционировать его на экране. 3 Отобразить слой. Первые два пункта реализуются через систему скриптов Ogre; но для последнего шага придется написать код на С++. Начнем с материала. Сохраните следующий 'код' как target.material:

material Chad/TargetSights
{
           technique
           {
                      pass
                      {
                           lighting off
                           scene_blend alpha_blend
                           texture_unit
                           {
                                        texture terrain_detail.jpg
                           }
                      }
           }
}

Заметили? Я использовал для прицела текстуру terrain_detail.jpg, но только потому, что прицел очень мал: игроки увидят лишь небольшую серую точку. Вы можете взять свою картинку, но пока сойдет и эта. Следующий шаг – определить слой, который принимает материал и помещает его на экранной панели. Затем можно пристроить эту панель на экране, используя координату относительно левого верхнего угла, а также ширину и высоту. В любом случае, вот код – сохраните его в файл target.overlay:

chadtarget
{
           zorder 650
           container Panel(chadsight)
           {
                      metrics_mode relative
                      left 0.495
                      top 0.495
             width 0.004
             height 0.007
             transparent false
             material Chad/
TargetSights
           }
}

Параметр zorder определяет, где панель отобразится на экране, в терминах глубины, то есть мы можем (если пожелаем) задать способ расположения элементов по слоям. В Ogre его максимальное значение 650 (прицел на вершине стека). Последний шаг – создание слоя, он займет всего две строки. Добавьте такой код в конец метода createOutdoorScene():

Overlay *TargetSight = (Overlay*)OverlayManager::getSingleton().
getByName("chadtarget");
TargetSight->show();

Код загружает слой с именем, определенным в target.overlay, а затем отображает его. Не надо беспокоиться о потере указателя на слой – Ogre автоматически удерживает его посреди экрана, как определено в файле. Наши три шага проделаны; запустите игру и загляните в прицел. Пусть программировать было скучновато, но зато как удобно теперь целиться!

Стреляем на поражение

Настает главное событие этого урока: отстрел роботов, которые резвились в прошлом номере. Правду сказать, я не особо хотел отягчать насилием Висельника Чеда, но Ребекка – спец по насилию в нашей команде – отказалась плодить опечатки, пока мы не разнесем когонибудь на куски. Пусть будет так. У нас уже есть метод mousePressed() для проигрывания звука лазера, поэтому код для стрельбы подойдет именно сюда. Стрелять будем так: с позиции камеры проводим луч, аналогично тому, как мы делали для определения высоты игрока над уровнем земли. Это непростая геометрическая задача, но, к счастью, Ogre все делает сам, одним методом: getCameraToViewportRay(), который преобразует позицию на экране в позицию в нашем мире и позволяет пустить луч из положения камеры. При необходимости выбирать объекты мышью, можно было бы использовать указатель на объект-событие, передаваемый методу mousePressed(), но мы хотим просто пустить луч через центр экрана, и поэтому используем для координат X и Y значения 0.5. Вот код улучшенного метода mousePressed():

void CChadGame::mousePressed(MouseEvent* e) {
 Mix_PlayChannel(-1, m_MixFire, 0);
 Ray mouseray = m_Camera->getCameraToViewportRay(0.5,  0.5);
 RaySceneQuery* scenequery = m_SceneMgr ->createRayQuery(Ray());
 scenequery->setRay(mouseray);
 RaySceneQueryResult &result = scenequery->execute();
 if (!result.empty()) {
  for (unsigned int i = 0; i < result.size(); ++i) {
   RaySceneQueryResultEntry &re = result[i];
   if (re.movable && re.movable->getMovableType() == "Entity") {
    Entity *ent = (Entity*)(re.movable);
    String name = ent->getName();
    if (name == "water") continue; // игнорируем воду
     for (unsigned int j = 0; j < Enemies.size(); ++j) {
      if (Enemies[j]->m_EnemyName ==  name) {
       Enemies[j]->Hit();
       return;
       }
      }
     }
    }
   }
  }

Послав луч, я прошелся в цикле по откликам в поисках сущностей (в отличие от элементов ландшафта), а затем отсек водную поверхность. Остаются только роботы; получим имя жертвы и найдем в списке роботов соответствующий объект, а затем вызовем его метод Hit(). Те, кто следил за нашими уроками с самого начала, возможно, воскликнет: у наших врагов нет ни имени, ни метода Hit()! Исправим это: откройте файл chadenemy.h и добавьте эти строки до объявления m_Speed:

char m_EnemyName[32];
bool m_IsDead

Теперь добавьте следующие две строчки после метода Update():

void SetAnimation(String animation, bool loop);
void Hit();

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

Мы уже устанавливали имена врагов в конструкторе (файл chadenemy.cpp), но использовали временную локальную пере менную. Теперь, когда у нас есть m_EnemyName, мы можем хранить ее там, чтобы найти правильный объект сцены, поп ав в робота. Просто замените следующие три строки кода…

char enemyname[32];
sprintf(enemyname, "Robot %d", ++EnemyNum);
m_Entity = m_SceneMgr->createEntity(enemyname, "robot.
mesh");

... на следующие две...

sprintf(m_EnemyName, "Robot %d", ++EnemyNum);
m_Entity = m_SceneMgr->createEntity(m_EnemyName, "robot.
mesh");

Теперь нужно написать метод Hit(), изменить анимацию робота и установить переменную m_IsDead в значение true, например, так:

void CChadEnemy::Hit() {
           if (!m_IsDead) {
                      m_AnimationState->setEnabled(false);
                      SetAnimation("Die", false);
                      m_IsDead = true;
           }
}

То есть, если чудовище не сдохло на месте, отмените его текущую анимацию, измените его анимацию на «умирающий» (не в цикле, конечно), а затем добейте его. Теперь необходимо сказать игре, что делать, если робот убит. Для начала сделаем, чтобы убитые роботы переставали двигаться, освободив метод Update(), раз уж робот встретился со своим создателем. Далее, в методе Update() необходимо удалить робота-покойника из игры. Иначе все убитые роботы будут валяться вокруг – но, возможно, вам того и надо! В файле chadenemy.cpp измените начало метода Update() на следующее:

void CChadEnemy::Update(Real time) { m_AnimationState->addTime(time);
if (m_IsDead) return;

Здесь все понятно: мы хотим, чтобы робот продолжал проигрывать анимацию (чтобы анимация «Умер» отработала правильно), но не хотим, чтобы он перемещался или поворачивался к игроку. Если робот внезапно умер, мы скоренько выходим из функции.

Обломки роботов

Удалить мертвых роботов из вектора Enemies желательно элегантным способом, так что потребуется немного подумать. Если робот погиб, и текущее положение в анимации равно ее полной длине, освободим память и присвоим его сущности в массиве Enemies значение NULL. Затем используем метод erase() из библиотеки STL, чтобы удалить из вектора всех обнуленных врагов, а это, в свою очередь, требует использования особого алгоритма STL – remove_if. Звучит хитроумно, но все именно так: STL умеет автоматически удалять элементы из вектора на основе заданного критерия. Можно написать функцию, принимающую элемент вектора, и если функция возвратит значение true, элемент будет удален. Цикл обновления состояния врагов в методе frameStartedOutside() файла chad.cpp нужно привести к следующему виду:

for (unsigned int i = Enemies.size() - 1; i > 0; --i) {
           CChadEnemy* enemy = Enemies[i];
           enemy->Update(evt.timeSinceLastFrame);
           if (enemy->m_IsDead && enemy->m_AnimationState

->getTimePosition() == enemy->m_AnimationState->getLength()) {

                        m_SceneMgr->getRootSceneNode()-
>removeAndDestroyChil
d(enemy->m_EnemyName);
                        delete Enemies[i];
                        Enemies[i] = NULL;
           }
}
Enemies.erase(remove_if(Enemies.begin(), Enemies.end(), IsNull),
Enemies.end());

Мы перешли от использования цикла с итератором к циклу с целочисленным счетчиком, чтобы манипулировать отдельными элементами вектора Enemies. Убедившись, что враг мертв и доиграл свою анимацию до конца, мы вызываем метод removeAndDestroyChild() корневого узла сцены и выносим из нее покойника. Затем роботу присваивается значение NULL и освобождается занимаемая им память. После цикла вызываются чародеи erase() и remove_if(); последний принимает как параметры начало и конец диапазона, а также функцию обратного вызова. Эта строка пройдется по всему вектору Enemies и удалит элементы, которым при передаче их в функцию IsNull() возвращается значение true. Недостает только самой функции IsNull(), но с ней разобраться проще всего. Откройте файл chad.h и добавьте следующую строку к списку директив #include:

#include <algorithm> 

Эта строка задействует алгоритм remove_if. Теперь добавьте такую строку сразу после 'using namespace Ogre':

bool IsNull(void* somepointer) { return (somepointer == NULL); } 

Она отвечает на вопрос: не NULL ли значение некого указателя? Просто и мило.

Вместо заключения

На протяжении пяти уроков мы прошли путь создания 3D-игры с музыкой, звуками и врагами. Я думаю, неплохо. Причем согласитесь, вопросы математики мы почти не обсуждали. Может, от кватернионов у вас голова и пошла кругом, но в основном за вас думал Ogre. Надеюсь, вы кое-что узнали насчет процесса создания 3D игры – в частности, как просто, немного потрудившись, получить солидные результаты. И действительно, написав 500 строк кода, мы уже можем чем-то гордиться: пусть это не образцовая игра, но хороший демо-пример и отличная отправная точка для разработки таковой. Может, вам захочется создать новые уровни, добавить в рагов или изменить цель игры в Чеда. Желаем удачи – потом расскажете, как далеко вы продвинулись!


Наш эксперт

Пол Хадсон написал три книги по Linux и одну по PHP, он также поддерживает на SourceForge два проекта на Mono по лицензии GPL. Пол любит Emacs.

Очистка памяти

Я признаю, что люблю что-то делать, но не люблю убирать за собой. Возможно, именно поэтому мне нравится програм мировать под Mono – он автоматически подбирает за вами весь мусор! Пока что мы не беспокоились об освобождении отведенной памяти, поскольку Linux освобождает ОЗУ автоматически. Но если вы планируете что-то динамически подг ружать во время игры, тут уж об освобождении памяти придется позаботиться. Если вы, например, запустите звук и музыку, то в конце концов память будет исчерпана. Ogre прекрасно выполняет свои задачи, а вот памятью управляет плоховато, ибо пытается вмешаться в управление. Под С++ освобождение памяти не слишком поддается интуиции: вы беспокоитесь об одном, а нужно вовсе другое. Не тратьте время на попытки понять, что там вытворяет Ogre, лучше займитесь собственными обязанностями. Они сводятся к выполнению правила: при каждом применении new для создания нового объекта, не забывайте использовать delete для его удаления. В методе run() у нас есть следующая строка:

m_Player = new CChadPlayer();

В методе CChadGame::~CChadGame() необходимо добавить следующее:

delete m_Player;

SDL имеет собственные функции для освобождения аудиоресурсов, и понадобится вставить туда же такие две строчки:

Mix_FreeChunk(m_MixFire);
Mix_FreeChunk(m_Music);
Персональные инструменты
купить
подписаться
Яндекс.Метрика