Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.15 Объекты в игре
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.15 Объекты в игре


В этой Главе:

  • Изучим понятие "игровой объект" а также то, как они применяются в играх.
  • Исследуем принципы движения объектов с использованием векторов и мировой матрицы (world matrix).
  • Внедрим в движок 3 базовых типа объектов, встречающиеся в каждой FPS-игре.


Содержание

В предыдущей части мы разработали систему поддержки 3D-мешей, которая позволяет загружать 3D-меши и рендерить (в русскоязычной литературе чаще можно встретить синоним этого слова - "визуализировать") их на экране. В то же время, нам так и не удалось сдвинуь их с места (т.к. в предыдущем примере двигалась сама камера). Представь себе игру, в которой все меши статичны и не могут двигаться по игровой сцене либо быть анимированными. Любой игрок хочет видеть в FPS-игре 3D-меши игроков могут бегать по игровой карте, меши оружия и аптечек, которые появляются, непрерывно вращаются на одном месте до тех пор, пока их не подберёт игрок а затем вновь респаунятся (от англ. "respawn(external link)" - дословно "перерождение") на том же месте через определённый промежуток времени. В этой Главе мы разработаем необходимую инфраструктуру для поддержки всех этих фич, и вдохнём жизнь в наши 3D-меши.

Применение игровых объектов

В большинстве игр по игровой сцене разбросаны различные штуки, с которыми игрок может взаимодействовать. На деле персонаж игрока также представлен неким 3D-объектом, который может перемещаться по 3D-окружению. Например, объект игрока может быть представлен в виде маленького космического корабля, летающего по экрану и уничтожающего другие корабли. Также в игре моуть быть т.н. "аптечки" (power-ups) для восстановления жизней или энергии, которые летают вокруг объекта игрока, а тот их собирает. Кроме того в игре могут быть препятствия, которые также двигаются и игрок должен избегать столкновения с ними. Все вышеперечисленные объекты обладают рядом общих свойств. Во-первых все они (или почти все) обязательно представлены на экране в виде изображения или 3D-меша. Во вторых, у каждого из них есть набор физических свойств, которые позволяют им двигаться по экрану и сталкиваться друг с другом. Как видим, все эти объекты очень схожи друг с другом. Именно их обычно и называют игровыми объектами (game objects) или для краткости просто объектами.

Рис.1 Ветвление игровых объектов
Рис.1 Ветвление игровых объектов

Создание игровых объектов (далее - объектов) это та сфера, где активно применяются наследование и полиморфизм. Мы обсуждали эти понятия в предыдущих Главах. Для остальных напомним, что обе этих техники лежат в основе объектно-ориентированного программирования. Наследованием называют технику создания дочернего класса на основе базового, с сохранением полного функционала "родителя". Полиморфизм определяет порядок вызова функции базового класса, если при этом он переопределён (overriden) в дочернем классе (derived class). Рассмотрим простой пример. Допустим, у нас есть класс, в котором определена виртуальная функция. Ты можешь создать на основе этого класса другой класс, который будет являться дочерним (derived) по отношению к первому, базовому (base) классу. В дочернем классе ты можешь переопределить виртуальную функцию базового класса, чтобы при её вызове вызывалась именно переопределённая версия из дочернего класса.
Эти две техники необычайно важны при определении объектов в игре. Они позволяют создать всего один базовый объект, обладающий общими свойствами, присущими всем другим объектам. К примеру, ты захочешь определить, что все объекты в игре должны иметь определённое положение в 3D-пространстве. Вместо того, чтобы добавлять вектор положения в каждый из классов игровых объектов, куда проще создать класс базового объекта (base object class), описав в нём вектор положения. Затем мы просто ответвим (derive) от него несколько дочерних классов объектов, каждый из которых унаследует вектор положения, определённый в базовом классе (классе базового объекта), а следовательно сможет использовать его без ограничений. Тот же принцип мы применим к нашим игровым объектам. Мы определим базовый объект, который будет содержать в себе описание наиболее общих свойств, присущих всем объектам в игре. Всякий раз, когда мы будем создавать новый игровой объект, нам будет достаточно ответвить от него новый дочерний класс. И новый объект автоматически будет иметь весь набор свойств базового объекта. И весь процесс здесь не останавливается. От дочернего класса ты можешь также ветвить другие дочерние классы игровых объектов и все они сохранят в себе полный функционал базового объекта (см. Рис.1).
Класс базового объекта, который мы создадим, позволит нам размещать базовые объекты на нашей 3D-сцене. Они даже будут иметь собственные 3D-меши, назначенный им, а значит будут видимыми на сцене. Как вариант, ты можешь вовсе не назначать меш объекту. Например при нанесении т.н. вейпоинтов (англ. "waypoint" - точка маршрута; здесь - условные точки, по отрезкам которых перемещаются игровые объекты, контролируемые компьютером), которые изначально задуманы как невидимые ориентиры. Путём применения векторов и специального типа матрицы, называемой мировая матрица (world matrix) мы сможем задавать положение, вращать и перемещать объекты в 3D-пространстве. И это будет лишь часть возможностей базового объекта. Создав его однажды, ты можешь ветвить от него другие, более сложные, объекты которые сразу после своего создания будут обладать всеми свойствами базового объекта.
Прежде чем начать разрабатывать наш базовый объект, обсудим ещё пару важных тем, связанных с позиционированием и перемещением игровых объектов в 3D-пространстве. Точнее говоря мы детально рассмотрим ту самую мировую матрицу, т.к. именно через неё происходит размещение любого объекта или меша в 3D-пространстве.

Движение объектов

Раз уж наши объекты существуют в 3D-пространстве, они должны соответствовать Картезианской координатной системе. Другими словами объект должен иметь позицию в 3D-пространстве, определённую координатами (x, y, z). Самый простой способ представить эти координаты - рассмотреть простой вектор положения (position vector). Вообще просто разместить объекты на сцене и оставить их неподвижными - плёвое дело. Но очевидно, что некоторые из них мы захотим привести их в движение. Сам по себе процесс движения объекта не так сложен: он лишь перемещается из одного положения в другое, меняя свои позиционные координаты. Если у тебя есть объект, расположенный в точке, имеющей координату 12.0 по оси X и ты хочешь переместить его ещё на 2 единицы по той же оси, то необходимо просто изменить его позиционную координату по оси X с 12.0 на 14.0 . Как видим, в перемещении объектов в самом деле нет ничего сложного. Намного труднее отследить направление движения.
Вместо того, чтобы пытаться указать как далеко должен двигаться объект и с какой периодичностью он должен преодолевать эту дистанцию, куда проще разработать простую, основанную на реальной физике, систему движения (movement system), которая сама вычислит нужные временные интервалы. Звучит пугающе, но всё не так страшно, как может показаться на первый взгляд.
Рассмотрим ещё один пример, который поможет прояснить ситуацию. Допустим, мы работаем в 3D-пространстве, отмасштабированном так, что 1 единица измерения (в 3DS Max - 1 юнит) равная 1-му метру. Это означает, что 1 метр в реальном мире соответствует 1-му метру в виртуальной сцене. Представим, что в этом 3D-пространстве объект автомобиль движется по оси X со скоростью 10 метров в секунду (м/с). Мы можем сохранить данное движение в отдельном векторе, который назовём вектором скорости и который будет иметь координаты (10.0, 0.0, 0.0). Суть заключается в том, чтобы быть уверенным, что данный автомобиль перемещается на 10.0 единиц по оси X каждую секунду. Данное обстоятельство является ключевым для понимания всего процесса. Мы не хотим, чтобы автомобиль перемещался на 10.0 единиц в каждом кадре. Потому что в этом случае при частоте обновления, например, 50 кадров в секунду мы получим перемещение автомобиля на все 500.0 единиц по оси X каждую секунду, что эквивалентно скорости 500 м/с! Проблема в том, что мы хотим обновлять перемещение автомобиля в каждом кадре, поэтому это не будет выглядеть как простой прыжок на все 10.0 единиц в течение 1-й секунды, т.к. это создаст отрывочное, "дёрганое" движение.
Так как же нам заставить двигаться автомобиль в каждом кадре так, чтобы он перемещался каждую секунду не более чем на 10.0 единиц по оси X? На самом деле это довольно просто. Если нам необходимо распределить движение автомобиля по 50 кадрам (подразумеваем, что игра запущена в режиме 50 кадров в секунду), необходимо просто разделить скорость движения автомобиля на 50, что даст нам величину, которую проходит автомобиль в каждом кадре. В нашем случае нам необходимо перемещать его на о.2 единицы (10.0 делить на 50 получается 0.2) по оси X в каждом кадре. И спустя 50 кадров, автомобиль как раз переместиться на 10.0 единиц. А так как скорость смены кадров составляет 50 кадров в секунду, значит скорость движения автомобиля составит как раз 10 м/с. Проблема решена, не так ли? На самом деле не совсем. Мы двигаемся в верном направлении, но в нашей идее есть один основной промах. Мы не можем гарантировать, что наше приложение всегда будет работать с частотой смены кадров 50 кадров в секунду. Что если наше приложение будет запущено на другом, более мощном или наоборот слабом ПК? Да, оно будет выдавать разные показатели частоты кадров в секунду и таким образом отправит все наши предыдущие вычисления коту под хвост. Допустим, мы запустили наше приложение на слабом ПК, и оно выдаёт всего 25fps. В этом случае наш автомобиль будет двигаться со скоростью всего 5.0 единиц в секунду, т.к. число кадров в секунду составляет лишь половину от расчётных 50 fps, в то время как дистанция, проходимая автомобилем в каждом кадре осталась прежней. Верным решением здесь будет принятие во внимание тот факт, что частота обновления (частота кадров) может изменяться. Причём часто даже на одном и том же компьютере.
Для решения этой проблемы мы должны принять во внимание время смены кадров и использовать этот временной интервал для определения того, как далеко должен переместиться автомобиль в каждом кадре. Такой подход часто называют движением, основанным на времени (time-based movement) и у нас уже готова инфраструктура для его реализации. В Главе 1.2 при разработке класса Engine в тестовом приложении во время выполнения функции Run мы подсчитывали время показа каждого кадра. Как ты уже знаешь, время, затраченное на показ кадра представляет отрезок времени (определённое число миллисекунд), прошедший с момента показа предыдущего кадра. Так как кадры обрабатываются очень быстро, то часто за одну секунду обрабатывается несколько десятков кадров. То есть затраченное время (elapsed time) всегда будет очень малой величиной, равной приблизительно 0,02 секунды (20 миллисекунд) или около того. Это единственная величина, которая нам необходима для организации движения, основанного на времени.
Зная это, вернёмся к нашему примеру с движущимся автомобилем. В случае, когда наше приложение работает с частотой обновления 50 кадров в секунду (50 fps), изменение положения нашего автомобиля будет также обновляться 50 раз за 1 секунду. Таким образом показатель elapsed time (время, затраченное на показ одного кадра) составит 0,02 секунды. Представь себе показатель затраченного времени в виде определённой процентной доли секунды, необходимой для обработки текущего кадра. Это значит, что при частоте обновления 50 кадров в секунду каждый кадр "ответственен" за подготовку 2% от одной секунды игрового процесса. То есть в каждом кадре нам необходимо перемещать автомобиль всего на 2% от скорости его движения. Чтобы проделать это необходимо лишь умножить скорость движения автомобиля (10 метров в секунду) на показатель затраченного времени (0.02 сек.), что даст нам результат 0.2 м/с, что означает величину, на которую мы должны передвигать автомобиль в каждом кадре для достижения скорости 10 м/с за 50 кадров игрового процесса.
Кажется в нашем текущем примере ничего не изменилось, по сравнению с предыдущим. Но это лишь потому, что частота обновления осталась прежней - 50 кадров в секунду. Если частота обновления изменится (которая часто меняется даже в каждой секунде игрового процесса), ты увидишь, что в предыдущем примере автомобиль двигается неаккуратно, часто слишком быстро либо слишком медленно. Во втором примере в случае правильно настроенного движения, основанного на времени, любые изменения частоты обновления, подсчитанные покадровом уровне и с учётом показателя затраченного времени (elapsed time), будут учитываться и корректно отражаться на времени, прошедшем с показа предыдущего и до начала следующего кадра.

Мировая матрица (World matrix)

В конце предыдущей Главы мы впервые познакомились с матрицей вида (view matrix), а в Главе 1.5 мы также обсуждали матрицу проекции (projection matrix). Сейчас пришёл черёд изучить последнюю из трёх матриц, используемых Direct3D - мировую матрицу.
Но для начала быстро вспомним, что из себя представляют первые две.
Матрица вида (view matrix) применяется для позиционирования и ориентации зрителя (= виртуальной камеры) в 3D-пространстве. Как ты видел в предыдущей Главе, библиотека D3DX предоставляет несколько функций, которые применяются для построения матрицы вида. Наш движок не будет использовать ни одну из них, т.к. мы будем вычислять матрицу вида несколько иным путём, как ты увидишь позднее.
Матрица проекции (projection matrix) выступает в качестве своеобразной "линзы" виртуальной камеры. Матрица проекции позволяет задать угол обзора (angle of view) для поля видимости (field of view), ближней плоскости отсечения (near clipping plane) и дальней плоскости отсечения (far clipping plane). С матрицей проекции можно проделывать ряд трюков, как например сужение или расширение угла обзора, создавая иллюзию зума и фокусировки камеры на отдельных объектах сцены (особенно при использовании совместно с матрицей вида). Вообще эта тема выходит за пределы данного курса, но со значениями этих матриц ты можешь "поиграть" самостоятельно, для уяснения что там и к чему.

Рис.2 Сравнение пространства модели (Model space) и пространства мира (World space)
Рис.2 Сравнение пространства модели (Model space) и пространства мира (World space)

Самой интересной из всех трёх матриц является мировая матрица, т.к. именно она применяется для фактического размещения объектов в 3D-пространстве. Когда ты размещаешь камеру для того, чтобы увидеть объект в 3D-пространстве, Direct3D необходимо подсчитать, в каком месте на экране рендерить данный объект, чтобы он был виден в объективе виртуальной камеры. Представь, что экран монитора является своеобразным "окном" в 3D-пространство, которое также определяет позицию "линз" виртуальной камеры. Для корректного отображения объекта Direct3D необходимо спроектировать трёхмерный объект на двумерную плоскость экрана монитора. Это достигается путём применения всех трёх матриц.
Рассмотрим этот процесс более подробно. Единичный меш (который также может быть использован как меш объекта на экране) изначально размещается в так называемом пространстве модели (model, object, local space). Это лишь означает, что меш имеет свою собственную локальную систему координат, схожую с картезианской, которую использует Direct3D. Если ты отметил точку с координатой 2.0 на положительном луче оси X в локальной системе координат меша (mesh's model space), то в этому случае куда бы мы не переместили сам меш в 3D-пространстве (которое также называется "мировое пространство" - world space), отмеченная точка будет иметь ту же самую координату 2.0 в локальной системе координат (в локальном пространстве - model space), т.к. само локальное пространство всегда перемещается вместе с мешем, к которому оно привязано (см. Рис.2).
Так вот. Direct3D использует мировую матрицу (world matrix) для перемещения меша из пространства модели (model space, она же локальная система координат) в пространство мира (world space). При этом меш размещается в одном пространстве вместе с другими мешами сцены (даже если каждый из них имеет своё собственное пространство модели). Сразу вслед за этим Direct3D применяет матрицу вида (view matrix) для трансформирования (как ты помнишь, матрицы применяются именно для трансформаций) меша в пространство камеры (camera space), чтобы таким образом он был расположен относительно текущего положения и ориентации виртуальной камеры. Далее применяется матрица проекции (projection matrix) для трансформирования меша в пространство проекции (projection space). На этом этапе вершины меша масштабируются (scaled) и вытягиваются для получения впечатления 3D-перспективы. Наконец, меш обрезается (при необходимости) для размещения на экране и проектируется на пространство экрана (screen space) для рендеринга и показа на экране монитора. Очевидно, что на всех вышеперечисленных этапах "в фоновом режиме" проделывается уйма других всевозможных операций. К счастью, Direct3D берёт всю "чёрную" работу по обеспечению 3D-рендеринга на себя. Программеру остаётся лишь корректно настроить матрицы, что на самом деле сделать совсем несложно.
В Главе 1.5 мы уже видели как настраивается матрица проекции. И, как мы уже говорили, будучи однажды настроенной, она не требует вмешательства в свою работу в дальнейшем. По крайней мере до тех пор, пока ты не решишь изменить текущую проекцию по каким-либо причинам.
Матрица вида (view matrix) вычисляется и настраивается один раз в каждом кадре. Это логично, ведь в большинстве динамичных игр вид изменяется в каждом кадре вместе с движением игрока по игровой сцене. О настройке матрицы вида мы поговорим чуть позднее в этой Главе.
Что касается мировой матрицы, то нам потребуется создать по одной такой для каждого объекта в игре. Это позволит редактировать и поднастраивать мировую матрицу каждого объекта независимо от других. Другими словами для каждого объекта игровой сцены необходимо вычислять отдельную мировую матрицу. Это вовсе не означает, что мировые матрицы всех объектов будут вычисляться в каждом кадре. Мировую матрицу того или иного объекта необходимо перерассчитывать вновь только при его перемещении. Таким образом, если в игровой сцене присутствует дерево, которое всегда неподвижно, его мировую матрицу необходимо вычислять лишь однажды - при его создании. С другой стороны, мировую матрицу постоянно движущегося объекта необходимо высчитывать заново в каждом кадре.
Когда придёт время рендерить объекты на экране, первым делом настраиваем их мировые матрицы (или просто регистрируем их через интерфейсы Direct3D) ДО рендеринга их 3D-мешей. Эти мы укажем Direct3D, где именно должны находиться объекты в мировом пространстве.

Закрыть
noteОбрати внимание

Важно помнить, что Direct3D может одновременно работать лишь с одним набором мировых матриц. Это означает, что в каждом кадре программер должен установить мировую матрицу каждого объекта ДО того, как начнётся рендеринг их мешей. При этом необходимо сначала начать рендеринг текущего меша до установки мировой матрицы следующего объекта. Другими словами ты не можешь сначала установить мировые матрицы для всех объектов сцены, а затем начать одновременно рендерить их меши. Если это сделать, то все меши будут отрендерены с использованием мировой матрицы последнего объекта, которому она назначена.

Теперь ты должен иметь чёткое представление о том, как работает мировая матрица применительно к игровым объектам и двум другим матрицам, используемым Direct3D. Тема матриц преобразований крайне обширна и мы не будем углубляться в неё далее. Вместо этого мы рассмотрим реализацию нашего базового объекта. Здесь ты как раз и увидишь, как мировая матрица создаётся и настраивается перед рендерингом.

Объекты сцены

Базовый объект (base object) это самый фундаментальный объект, который только можно использовать. Он содержит в себе весь базовый функционал, который является общим для объектов любых других типов, которые будут созданы на его основе. Все новые объекты "ветвятся" (derived) от базового объекта и потому полностью наследуют его общий для всех функционал. Раз уж мы назвали игровое 3D-пространство "сценой", то и наш базовый объект мы назовём объект сцены - scene object. Объект сцены представляет собой самый базовый объект, который только может существовать в наших сценах.

Создаём SceneObject.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "SceneObject.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле SceneObject.h набираем следующий код:

SceneObject.h (Проект Engine)
//-----------------------------------------------------------------------------
// Необходимый минимум функционала для существования объекта в игровой сцене.
// Объекты, специфичные для приложения, должны ветвиться (derive) от данного класса.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef SCENE_OBJECT_H
#define SCENE_OBJECT_H

//-----------------------------------------------------------------------------
// Определение типа объекта сцены
//-----------------------------------------------------------------------------
#define TYPE_SCENE_OBJECT 0

//-----------------------------------------------------------------------------
// Scene Object Class
//-----------------------------------------------------------------------------
class SceneObject : public BoundingVolume
{
public:
	SceneObject( unsigned long type = TYPE_SCENE_OBJECT, char *meshName = NULL, char *meshPath = "./", bool sharedMesh = true );
	virtual ~SceneObject();

	virtual void Update( float elapsed, bool addVelocity = true );
	virtual void Render( D3DXMATRIX *world = NULL );

	virtual void CollisionOccurred( SceneObject *object, unsigned long collisionStamp );

	void Drive( float force, bool lockYAxis = true );
	void Strafe( float force, bool lockYAxis = true );
	void Stop();

	void SetTranslation( float x, float y, float z );
	void SetTranslation( D3DXVECTOR3 translation );
	void AddTranslation( float x, float y, float z );
	void AddTranslation( D3DXVECTOR3 translation );
	D3DXVECTOR3 GetTranslation();

	void SetRotation( float x, float y, float z );
	void SetRotation( D3DXVECTOR3 rotation );
	void AddRotation( float x, float y, float z );
	void AddRotation( D3DXVECTOR3 rotation );
	D3DXVECTOR3 GetRotation();

	void SetVelocity( float x, float y, float z );
	void SetVelocity( D3DXVECTOR3 velocity );
	void AddVelocity( float x, float y, float z );
	void AddVelocity( D3DXVECTOR3 velocity );
	D3DXVECTOR3 GetVelocity();

	void SetSpin( float x, float y, float z );
	void SetSpin( D3DXVECTOR3 spin );
	void AddSpin( float x, float y, float z );
	void AddSpin( D3DXVECTOR3 spin );
	D3DXVECTOR3 GetSpin();

	D3DXVECTOR3 GetForwardVector();
	D3DXVECTOR3 GetRightVector();

	D3DXMATRIX *GetTranslationMatrix();
	D3DXMATRIX *GetRotationMatrix();
	D3DXMATRIX *GetWorldMatrix();
	D3DXMATRIX *GetViewMatrix();

	void SetType( unsigned long type );
	unsigned long GetType();

	void SetFriction( float friction );

	unsigned long GetCollisionStamp();

	void SetVisible( bool visible );
	bool GetVisible();

	void SetEnabled( bool enabled );
	bool GetEnabled();

	void SetGhost( bool ghost );
	bool GetGhost();

	void SetIgnoreCollisions( bool ignoreCollisions );
	bool GetIgnoreCollisions();

	void SetTouchingGroundFlag( bool touchingGround );
	bool IsTouchingGround();

	void SetMesh( char *meshName = NULL, char *meshPath = "./", bool sharedMesh = true );
	Mesh *GetMesh();

protected:
	D3DXVECTOR3 m_forward; // Вектор объекта, направленный вперёд (Object's forward vector).
	D3DXVECTOR3 m_right; // Вектор объекта, направленный вправо (Object's right vector).

	D3DXMATRIX m_worldMatrix; // Мировая матрица (World matrix).
	D3DXMATRIX m_viewMatrix; // Матрица вида (View matrix).

private:
	D3DXVECTOR3 m_translation; // Трансляция объекта в 3D-пространстве.
	D3DXVECTOR3 m_rotation; // Вращение объекта в радианах.

	D3DXVECTOR3 m_velocity; // Скорость объекта в единиц/сек.
	D3DXVECTOR3 m_spin; // Скорость вращения объекта в радиан/сек.

	D3DXMATRIX m_translationMatrix; // Матрица трансляции текущей позиции (Translation matrix).
	D3DXMATRIX m_rotationMatrix; // Матрица вращения (Rotation matrix).

	unsigned long m_type; // Идентифицирует родительский класс объекта сцены.
	float m_friction; // Коэффициент трения (затухания), применяемый к скоростям движения и вращения объекта.
	unsigned long m_collisionStamp; // Отмечает последний кадр, в котором произошло столкновение.
	bool m_visible; // Флаг видимости объекта. Невидимые объекты просто не рендерятся.
	bool m_enabled; // Флаг "включения" объекта в сцену. Отключенные объекты сцены не обновляются.
	bool m_ghost; // Флаг того, является ли объект "призраком" (ghost). Объекты-призраки не могут сталкиваться с другими предметами и проходят через них.
	bool m_ignoreCollisions; // Флаг, отвечающий за то, будет ли объект игнорировать столкновения. "Физические" столкновения будут происходить, но они просто не будут регистрироваться.
	bool m_touchingGround; // Флаг, отмечающий касание объекта с поверхностью земли/пола.
	bool m_sharedMesh; // Флаг указывает, использует ли объект 3D-меш совместно с другими объектами либо имеет эксклюзивный доступ к нему.
	Mesh *m_mesh; // Указатель на 3D-меш объекта.
};

#endif

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код SceneObject.h

Класс SceneObject является самым большим их трёх типов объектов, которые мы рассмотрим в этой Главе. Так что если ты без труда разобрался в приведённом выше исходном коде, то оставшаяся часть Главы покажется тебе не сложнее прогулки в парке.
Для начала отметим, что класс SceneObject ветвится от класса BoundingVolume. Это позволяет нам "обнести" создаваемые на основе базового класса объекты набором из трёх ограничивающих объёмов (прямоугольник, сфера, эллипс) для самых разных целей, самая очевидная из которых - определение столкновений (collision detection). Помимо этого класс SceneObject имеет ставшие уже привычными конструктор, деструктор, функции Update и Render, без которых нам теперь никуда. Их работу мы обсудим чуть позднее в данной Главе.
Класс SceneObject содержит в себе множество различных функций, которые позволяют нам тонко настраивать (adjust) положение объекта в пространстве, а также его вращение и скорость. Помимо этого класс также имеет несколько служебных (utility) функций, которые дают возможность запрашивать различные матрицы, используемые объектами, а также различные свойства и константы, запрашиваемые у движка.

Создаём SceneObject.cpp (Проект Engine)

В файле исходного кода SceneObject.cpp будут размещаться реализации функций, объявленных в SceneObject.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "SceneObject.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле SceneObject.cpp набираем следующий код:

SceneObject.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация классов и функций, объявленных в SceneObject.h.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The scene object class constructor.
//-----------------------------------------------------------------------------
SceneObject::SceneObject( unsigned long type, char *meshName, char *meshPath, bool sharedMesh )
{
	// Устанавливаем тип объекта;
	SetType( type );

	// Обнуляем трансляцию позиции и вращение объекта сцены.
	SetTranslation( 0.0f, 0.0f, 0.0f );
	SetRotation( 0.0f, 0.0f, 0.0f );

	// Изначально объект находится в состоянии покоя.
	SetVelocity( 0.0f, 0.0f, 0.0f );
	SetSpin( 0.0f, 0.0f, 0.0f );

	// Изначально объект устремлён "лицом" по направлению положительного руча оси Z.
	m_forward = D3DXVECTOR3( 0.0f, 0.0f, 1.0f );
	m_right = D3DXVECTOR3( 1.0f, 0.0f, 0.0f );

	// Изначально объект не имеет трения.
	m_friction = 0.0f;

	// Очищаем отметку о столкновении (collision stamp).
	m_collisionStamp = -1;

	// По умолчанию объект видим, включён (enabled), твёрд (не "призрак"), и регистрирует столкновения.
	m_visible = true;
	m_enabled = true;
	m_ghost = false;
	m_ignoreCollisions = false;

	// Изначально объект не касается земли.
	m_touchingGround = false;

	// Устанавливаем 3D-меш объекта.
	m_mesh = NULL;
	SetMesh( meshName, meshPath, sharedMesh );
}

//-----------------------------------------------------------------------------
// The scene object class destructor.
//-----------------------------------------------------------------------------
SceneObject::~SceneObject()
{
	// Уничтожаем меш объекта.
	if( m_sharedMesh == true )
		g_engine->GetMeshManager()->Remove( &m_mesh );
	else
		SAFE_DELETE( m_mesh );
}

//-----------------------------------------------------------------------------
// Обновляем состояние объекта.
//-----------------------------------------------------------------------------
void SceneObject::Update( float elapsed, bool addVelocity )
{
	// Рассчитываем трение для данного обновления (если оно есть).
	float friction = 1.0f - m_friction * elapsed;

	// Перемещаем объект.
	m_velocity *= friction;
	if( addVelocity == true )
	{
		D3DXVECTOR3 velocity = m_velocity * elapsed;
		AddTranslation( velocity.x, velocity.y, velocity.z );
	}

	// Вращаем объект.
	m_spin *= friction;
	D3DXVECTOR3 spin = m_spin * elapsed;
	AddRotation( spin.x, spin.y, spin.z );

	// Обновляем мировую матрицу (world matrix) объекта.
	D3DXMatrixMultiply( &m_worldMatrix, &m_rotationMatrix, &m_translationMatrix );

	// Создаём матрицу вида (view matrix) для объекта.
	D3DXMatrixInverse( &m_viewMatrix, NULL, &m_worldMatrix );

	// Обновляем вектор объекта, направленный вперёд (object's forward vector).
	m_forward.x = (float)sin( m_rotation.y );
	m_forward.y = (float)-tan( m_rotation.x );
	m_forward.z = (float)cos( m_rotation.y );
	D3DXVec3Normalize( &m_forward, &m_forward );

	// Обновляем вектор объекта, направленный вправо (object's right vector).
	m_right.x = (float)cos( m_rotation.y );
	m_right.y = (float)tan( m_rotation.z );
	m_right.z = (float)-sin( m_rotation.y );
	D3DXVec3Normalize( &m_right, &m_right );

	// Обновляем ограничивающий объём объекта с применением одной только матрицы трансляции (translation matrix).
	// Это установит оси, приложенные к ограничивающему прямоугольнику вокруг объекта в мировом пространстве
	// (world space) вместо локального пространства объекта (object's local space).
	RepositionBoundingVolume( &m_translationMatrix );
}

//-----------------------------------------------------------------------------
// Renders the object.
//-----------------------------------------------------------------------------
void SceneObject::Render( D3DXMATRIX *world )
{
	// Игнорируем объект, если он не имеет 3D-меша.
	if( m_mesh == NULL )
		return;

	// Проверяем случай, когда мировая матрица преобразований объекта (the object's world tranformation matrix) была переопределена.
	if( world == NULL )
		g_engine->GetDevice()->SetTransform( D3DTS_WORLD, &m_worldMatrix );
	else
		g_engine->GetDevice()->SetTransform( D3DTS_WORLD, world );

	// Рендерим меш объекта.
	m_mesh->Render();
}

//-----------------------------------------------------------------------------
// Вызывается, когда что-то столкнулось (collide) с объектом.
//-----------------------------------------------------------------------------
void SceneObject::CollisionOccurred( SceneObject *object, unsigned long collisionStamp )
{
	// Ставим отметку о столкновении (collision stamp).
	m_collisionStamp = collisionStamp;
}

//-----------------------------------------------------------------------------
// Применяем данную силу к объекту в направлении вперёд/назад.
//-----------------------------------------------------------------------------
void SceneObject::Drive( float force, bool lockYAxis )
{
	D3DXVECTOR3 realForce = m_forward * force;

	m_velocity.x += realForce.x;
	m_velocity.z += realForce.z;

	if( lockYAxis == false )
		m_velocity.y += realForce.y;
}

//-----------------------------------------------------------------------------
// Применяем данную силу к объекту в напарвлении вправо/влево.
//-----------------------------------------------------------------------------
void SceneObject::Strafe( float force, bool lockYAxis )
{
	D3DXVECTOR3 realForce = m_right * force;

	m_velocity.x += realForce.x;
	m_velocity.z += realForce.z;

	if( lockYAxis == false )
		m_velocity.y += realForce.y;
}

//-----------------------------------------------------------------------------
// Останавливает движение объекта.
//-----------------------------------------------------------------------------
void SceneObject::Stop()
{
	m_velocity = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
	m_spin = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
}

//-----------------------------------------------------------------------------
// Устанавливаем новую трансляцию позиции объекта (object's translation) на основе координат трансляции x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::SetTranslation( float x, float y, float z )
{
	m_translation.x = x;
	m_translation.y = y;
	m_translation.z = z;

	D3DXMatrixTranslation( &m_translationMatrix, m_translation.x, m_translation.y, m_translation.z );
}

//-----------------------------------------------------------------------------
// Устанавливаем новую трансляцию позиции объекта (object's translation) на основе вектора трансляции.
//-----------------------------------------------------------------------------
void SceneObject::SetTranslation( D3DXVECTOR3 translation )
{
	m_translation = translation;

	D3DXMatrixTranslation( &m_translationMatrix, m_translation.x, m_translation.y, m_translation.z );
}

//-----------------------------------------------------------------------------
// Прибавляем (add) данную трансляцию к текущей трансляции позиции объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::AddTranslation( float x, float y, float z )
{
	m_translation.x += x;
	m_translation.y += y;
	m_translation.z += z;

	D3DXMatrixTranslation( &m_translationMatrix, m_translation.x, m_translation.y, m_translation.z );
}

//-----------------------------------------------------------------------------
// Прибавляем (add) данную трансляцию к текущей трансляции позиции объекта на основе вектора трансляции.
//-----------------------------------------------------------------------------
void SceneObject::AddTranslation( D3DXVECTOR3 translation )
{
	m_translation += translation;

	D3DXMatrixTranslation( &m_translationMatrix, m_translation.x, m_translation.y, m_translation.z );
}

//-----------------------------------------------------------------------------
// Возвращает текущую трансляцию объекта.
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetTranslation()
{
	return m_translation;
}

//-----------------------------------------------------------------------------
// Устанавливает вращение объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::SetRotation( float x, float y, float z )
{
	m_rotation.x = x;
	m_rotation.y = y;
	m_rotation.z = z;

	D3DXMATRIX rotationX, rotationY;
	D3DXMatrixRotationX( &rotationX, m_rotation.x );
	D3DXMatrixRotationY( &rotationY, m_rotation.y );
	D3DXMatrixRotationZ( &m_rotationMatrix, m_rotation.z );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationX );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationY );
}

//-----------------------------------------------------------------------------
// Устанавливает вращение объекта на основе вектора вращения (rotation vector).
//-----------------------------------------------------------------------------
void SceneObject::SetRotation( D3DXVECTOR3 rotation )
{
	m_rotation = rotation;

	D3DXMATRIX rotationX, rotationY;
	D3DXMatrixRotationX( &rotationX, m_rotation.x );
	D3DXMatrixRotationY( &rotationY, m_rotation.y );
	D3DXMatrixRotationZ( &m_rotationMatrix, m_rotation.z );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationX );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationY );
}

//-----------------------------------------------------------------------------
// Добавляем данное вращение к текущему вращению объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::AddRotation( float x, float y, float z )
{
	m_rotation.x += x;
	m_rotation.y += y;
	m_rotation.z += z;

	D3DXMATRIX rotationX, rotationY;
	D3DXMatrixRotationX( &rotationX, m_rotation.x );
	D3DXMatrixRotationY( &rotationY, m_rotation.y );
	D3DXMatrixRotationZ( &m_rotationMatrix, m_rotation.z );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationX );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationY );
}

//-----------------------------------------------------------------------------
// Добавляем данное вращение к текущему вращению объекта на основе вектора вращения.
//-----------------------------------------------------------------------------
void SceneObject::AddRotation( D3DXVECTOR3 rotation )
{
	m_rotation += rotation;

	D3DXMATRIX rotationX, rotationY;
	D3DXMatrixRotationX( &rotationX, m_rotation.x );
	D3DXMatrixRotationY( &rotationY, m_rotation.y );
	D3DXMatrixRotationZ( &m_rotationMatrix, m_rotation.z );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationX );
	D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationY );
}

//-----------------------------------------------------------------------------
// Возвращает текущее вращение объекта.
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetRotation()
{
	return m_rotation;
}

//-----------------------------------------------------------------------------
// Устанавливает скорость движения объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::SetVelocity( float x, float y, float z )
{
	m_velocity.x = x;
	m_velocity.y = y;
	m_velocity.z = z;
}

//-----------------------------------------------------------------------------
// Устанавливает скорость движения объекта на основе вектора скорости.
//-----------------------------------------------------------------------------
void SceneObject::SetVelocity( D3DXVECTOR3 velocity )
{
	m_velocity = velocity;
}

//-----------------------------------------------------------------------------
// Добавляет данную скорость к текущей скорости объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::AddVelocity( float x, float y, float z )
{
	m_velocity.x += x;
	m_velocity.y += y;
	m_velocity.z += z;
}

//-----------------------------------------------------------------------------
// Добавляет данную скорость к текущей скорости объекта на основе вектора скорости.
//-----------------------------------------------------------------------------
void SceneObject::AddVelocity( D3DXVECTOR3 velocity )
{
	m_velocity += velocity;
}

//-----------------------------------------------------------------------------
// Возвращает текущую скорость объекта.
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetVelocity()
{
	return m_velocity;
}

//-----------------------------------------------------------------------------
// Устанавливает кручение (spin) объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::SetSpin( float x, float y, float z )
{
	m_spin.x = x;
	m_spin.y = y;
	m_spin.z = z;
}

//-----------------------------------------------------------------------------
// Устанавливает кручение (spin) объекта на основе вектора кручения.
//-----------------------------------------------------------------------------
void SceneObject::SetSpin( D3DXVECTOR3 spin )
{
	m_spin = spin;
}

//-----------------------------------------------------------------------------
// Добавляет данное кручение к текущему кручению объекта на основе координат x, y, z.
//-----------------------------------------------------------------------------
void SceneObject::AddSpin( float x, float y, float z )
{
	m_spin.x += x;
	m_spin.y += y;
	m_spin.z += z;
}

//-----------------------------------------------------------------------------
// Добавляет данное кручение к текущему кручению объекта на основе вектора кручения.
//-----------------------------------------------------------------------------
void SceneObject::AddSpin( D3DXVECTOR3 spin )
{
	m_spin = spin;
}

//-----------------------------------------------------------------------------
// Возвращает текущее кручение объекта.
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetSpin()
{
	return m_spin;
}

//-----------------------------------------------------------------------------
// Возвращает вектор объекта, направленный вперёд (object's forward vector).
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetForwardVector()
{
	return m_forward;
}

//-----------------------------------------------------------------------------
// Возвращает вектор объекта, направленный вправо (object's right vector).
//-----------------------------------------------------------------------------
D3DXVECTOR3 SceneObject::GetRightVector()
{
	return m_right;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущую матрицу трансляции позиции объекта (object's current translation matrix).
//-----------------------------------------------------------------------------
D3DXMATRIX *SceneObject::GetTranslationMatrix()
{
	return &m_translationMatrix;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущую матрицу вращения объекта (object's current rotation matrix).
//-----------------------------------------------------------------------------
D3DXMATRIX *SceneObject::GetRotationMatrix()
{
	return &m_rotationMatrix;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущую мировую матрицу объекта (object's current world matrix).
//-----------------------------------------------------------------------------
D3DXMATRIX *SceneObject::GetWorldMatrix()
{
	return &m_worldMatrix;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущую матрицу вида объекта (object's current view matrix).
//-----------------------------------------------------------------------------
D3DXMATRIX *SceneObject::GetViewMatrix()
{
	return &m_viewMatrix;
}

//-----------------------------------------------------------------------------
// Устанавливает тип объекта.
//-----------------------------------------------------------------------------
void SceneObject::SetType( unsigned long type )
{
	m_type = type;
}

//-----------------------------------------------------------------------------
// Возвращает тип объекта.
//-----------------------------------------------------------------------------
unsigned long SceneObject::GetType()
{
	return m_type;
}

//-----------------------------------------------------------------------------
// Устанавливает трение объекта (object's friction).
//-----------------------------------------------------------------------------
void SceneObject::SetFriction( float friction )
{
	m_friction = friction;
}

//-----------------------------------------------------------------------------
// Возвращает отметку о столкновении (collision stamp).
//-----------------------------------------------------------------------------
unsigned long SceneObject::GetCollisionStamp()
{
	return m_collisionStamp;
}

//-----------------------------------------------------------------------------
// Устанавливает флаг видимости объекта (object's visible flag).
//-----------------------------------------------------------------------------
void SceneObject::SetVisible( bool visible )
{
	m_visible = visible;
}

//-----------------------------------------------------------------------------
// Возвращает флаг видимости объекта (object's visible flag).
//-----------------------------------------------------------------------------
bool SceneObject::GetVisible()
{
	return m_visible;
}

//-----------------------------------------------------------------------------
// Устанавливает флаг "включения" объекта (object's enabled flag).
//-----------------------------------------------------------------------------
void SceneObject::SetEnabled( bool enabled )
{
	m_enabled = enabled;
}

//-----------------------------------------------------------------------------
// Возвращает флаг "включения" объекта (object's enabled flag).
//-----------------------------------------------------------------------------
bool SceneObject::GetEnabled()
{
	return m_enabled;
}

//-----------------------------------------------------------------------------
// Устанавливает флаг "призрачности" объекта (object's ghost flag).
//-----------------------------------------------------------------------------
void SceneObject::SetGhost( bool ghost )
{
	m_ghost = ghost;
}

//-----------------------------------------------------------------------------
// Возвращает флаг "призрачности" объекта (object's ghost flag).
//-----------------------------------------------------------------------------
bool SceneObject::GetGhost()
{
	return m_ghost;
}

//-----------------------------------------------------------------------------
// Устанавливает флаг игнорирования столкновений с данным объектом (object's ignore collisions flag).
//-----------------------------------------------------------------------------
void SceneObject::SetIgnoreCollisions( bool ignoreCollisions )
{
	m_ignoreCollisions = ignoreCollisions;
}

//-----------------------------------------------------------------------------
// Возвращает флаг игнорирования столкновений с данным объектом (object's ignore collisions flag).
//-----------------------------------------------------------------------------
bool SceneObject::GetIgnoreCollisions()
{
	return m_ignoreCollisions;
}

//-----------------------------------------------------------------------------
// Устанавливает флага касания объектом земли.
//-----------------------------------------------------------------------------
void SceneObject::SetTouchingGroundFlag( bool touchingGround )
{
	m_touchingGround = touchingGround;
}

//-----------------------------------------------------------------------------
// Возвращает true (истина) если объект касается земли.
//-----------------------------------------------------------------------------
bool SceneObject::IsTouchingGround()
{
	return m_touchingGround;
}

//-----------------------------------------------------------------------------
// Устанавливает 3D-меш для данного объекта сцены.
//-----------------------------------------------------------------------------
void SceneObject::SetMesh( char *meshName, char *meshPath, bool sharedMesh )
{
	// Уничтожаем текущий меш объекта (если есть).
	if( m_sharedMesh == true )
		g_engine->GetMeshManager()->Remove( &m_mesh );
	else
		SAFE_DELETE( m_mesh );

	// Ставим флаг, что объект будет использовать данный меш совместно с другими объектами.
	m_sharedMesh = sharedMesh;

	// Проверяем, что меш указан.
	if( meshName != NULL && meshPath != NULL )
	{
		// Загружаем меш объекта.
		if( m_sharedMesh == true )
			m_mesh = g_engine->GetMeshManager()->Add( meshName, meshPath );
		else
			m_mesh = new Mesh( meshName, meshPath );

		// Клонируем ограничивающий объём (bounding volume) меша. Он будет использоваться
		//  для установления оси, приложенной вдоль ограничивающего объёма в мировом пространстве.
		CloneBoundingVolume( m_mesh->GetBoundingBox(), m_mesh->GetBoundingSphere() );
	}
}

//-----------------------------------------------------------------------------
// Возвращает указатель на меш объекта.
//-----------------------------------------------------------------------------
Mesh *SceneObject::GetMesh()
{
	return m_mesh;
}

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код SceneObject.cpp

Обширный список переменных членов мы изучим в порядке их появления в исходном коде.
Начнём с конструктора:

Фрагмент SceneObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The scene object class constructor.
//-----------------------------------------------------------------------------
SceneObject::SceneObject( unsigned long type, char *meshName, char *meshPath, bool sharedMesh )
{
	// Устанавливаем тип объекта;
	SetType( type );

	// Обнуляем трансляцию позиции и вращение объекта сцены.
	SetTranslation( 0.0f, 0.0f, 0.0f );
	SetRotation( 0.0f, 0.0f, 0.0f );

	// Изначально объект находится в состоянии покоя.
	SetVelocity( 0.0f, 0.0f, 0.0f );
	SetSpin( 0.0f, 0.0f, 0.0f );

	// Изначально объект устремлён "лицом" по направлению положительного руча оси Z.
	m_forward = D3DXVECTOR3( 0.0f, 0.0f, 1.0f );
	m_right = D3DXVECTOR3( 1.0f, 0.0f, 0.0f );

	// Изначально объект не имеет трения.
	m_friction = 0.0f;

	// Очищаем отметку о столкновении (collision stamp).
	m_collisionStamp = -1;

	// По умолчанию объект видим, включён (enabled), твёрд (не "призрак"), и регистрирует столкновения.
	m_visible = true;
	m_enabled = true;
	m_ghost = false;
	m_ignoreCollisions = false;

	// Изначально объект не касается земли.
	m_touchingGround = false;

	// Устанавливаем 3D-меш объекта.
	m_mesh = NULL;
	SetMesh( meshName, meshPath, sharedMesh );
}
...


Несмотря на свой значительный размер, конструктор выполняет совсем немного действий. Его основное назначение в данном случае - очищать (обнулять) все необходимые переменные члены либо приводить их значения к значениям по умолчанию. Мы обсудим значение каждого переменного члена как только разговор зайдёт об их применении, чуть ниже в этой Главе. Сейчас мы остановимся на двух основных точках конструктора.
Как видим, конструктор принимает type в качестве первого вводного параметра. Он устанавливается путём вызова специальной функции SetType, экспонированной классом SceneObject. В самом начале SceneObject.h стоит определение:

Фрагмент SceneObject.h (Проект Engine)
...
#define TYPE_SCENE_OBJECT 0
...

Оно определяет тип объекта сцены. Каждый новый объект будет обязательно иметь свой тип, хранящийся в переменном члене m_type, и как раз передаваемый в конструкторе класса SceneObject. Позднее, когда ты будешь создавать свои собственные типы объектов сцены, то будешь определять их именно этим способом. Мы используем эти типы в качестве идентификаторов классов, для того, чтобы точно знать, с каким типом объектов сцены мы работаем в данный момент. Например у нас может быть связный список, хранящий указатели на все объекты игры. Для проверки того, что хранимые в нём указатели совместимы со всеми типами объектов, поддерживаемыми игрой, достаточно просто разместить в нём указатели всех дочерних классов класса SceneObject. Для того, чтобы воспользоваться всем функционалом базового объекта, необходимо передать указатель класса SceneObject в соответствующие дочерние классы объектов различных типов. Вот здесь-то идентификатор типа (type's identifier) и выходит на первый план. Если, к примеру, ты проверяешь один из своих объектов и он возвращает значение TYPE_PLAYER_OBJECT, то ты точно знаешь, что данный объект является объектом игрока (PlayerObject). Кроме того, в этом случае будет необходимо разместить указатель класса SceneObject внутри указателя PlayerObject для получения доступа ко всему функционалу класса PlayerObject. Если что-то из данного абзаца осталось непонятным, то мы ещё раз остановимся на этом при разработке различных типов объектов при создании игры.
Второе, на что следует обратить внимание, это то, как конструктор обрабатывает меш объекта. Конструктор принимает имя файла с мешем (2-й параметр meshName), путь до меша (3-й параметр meshPath), а также статус использования меша (эксклюзивно/совместно с другими объектами; 4-й параметр sharedMesh). Имя файла меша и путь до него необходимы для загрузки меш-ресурса (mesh resource), Флаг sharedMesh указывает на то, использовать ли для загрузки меш-ресурса менеджер ресурсов или нет. Меш для совместного использования (shared mesh) загружается в память всего 1 раз а его инстансы запрашиваются различными объектами при необходимости. Меш для эксклюзивного использования (non-shared mesh) загружается специально для использования только с одним объектом. Из этого следует, что если создать 2 объекта и указать им использовать эксклюзивный (non-shared) меш, каждый из объектов загрузит в память по отдельной копии данного меш-ресурса.
Для статичных (static) мешей всегда необходимо выбирать разделяемые (shared) меш-ресурсы. В то же время для анимированных объектов это не всегда возможно. Анимированные объекты могут изменять свою форму и если два или более объекта ссылаются на один и тот же меш, то любые изменения хотя бы одного объекта моментально отразятся на их общем меше и, как следствие, на других объектах, которые его используют. Таким образом для анимированных мешей, как например меш объекта игрок (player object), необходимо использовать неразделяемые (non-shared) меши. К счастью, в типичной FPS-игре едва ли наберётся анимированных объектов, за исключением толпы объектов игроков. Поэтому неразделяемые (эксклюзивные) меши вряд ли доставят тебе массу хлопот.
В самом конце конструктора вызывается функция SetMesh, которая принимает последние 3 параметра, передаваемые в конструкторе. Она загружает меш и применяет при необходимости менеджер меш-ресурсов, необходимый для разделяемых (shared) меш-ресурсов. Загруженный меш уничтожается в деструкторе класса SceneObject при уничтожении самого объекта.
Деструктор класса SceneObject совсем небольшой:

Фрагмент SceneObject.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// The scene object class destructor.
//-----------------------------------------------------------------------------
...
SceneObject::~SceneObject()
{
	// Уничтожаем меш объекта.
	if( m_sharedMesh == true )
		g_engine->GetMeshManager()->Remove( &m_mesh );
	else
		SAFE_DELETE( m_mesh );
}
...

В нём уничтожается лишь меш. Если он был создан при участии меш-менеджера, то и удаляется он из него путём вызова специальной команды Remove.
Прежде чем мы двинемся к функции Update, рассмотрим те самые переменные члены, упомянутые вскользь чуть выше. Мы пропустим рассмотрение представленных матриц и позиционных векторов, т.к. мы также обсудили их чуть выше. Вместо этого рассмотрим векторы m_forward и m_right, которые представляют собой единичные векторы, не содержащие в себе ничего, кроме направления. Здесь всё просто: вектор m_forward применяется для указания передней стороны объекта, при его движении вперёд. Это то направление, куда "смотрит" объект. Вектор m_right применяется для указания направления, указывающего на правую сторону объекта. Если ты вытянешь свою левую руку перед собой, а правую - вдоль тела (перпендикулярно левой), то так ты сымитируешь оба этих вектора. В этом случае левая рука будет выступать в качестве вектора m_forward, в то время как правая рука будет играть роль вектора m_right. Мы ввели эти 2 вектора для наших объектов для выполнения расчётов, основываясь на направлении, куда они повёрнуты. Если заметил, мы вводим оба этих вектора именно в конструкторе класса SceneObject. Значения по умолчанию выставлены так, что объект смотрит "лицом" параллельно положительному лучу оси Z, а вектор m_right указывает в направлении положительного луча оси X. При вращении объекта эти векторы само собой изменяются, в то же время они сохраняют своё положение относительно друг друга и образуют прямой угол. Чуть позднее ты увидишь, как эти векторы рассчитываются и применяются.

Рис.3 Трение влияет на скорость движение объекта
Рис.3 Трение влияет на скорость движение объекта

Теперь рассмотрим переменный член m_friction. Как ты знаешь, наши объекты существуют в 3D-пространстве, а следовательно имеют 3D-координаты своей позиции. При движении объекта в течение времени его позиция также изменяется. Иногда мы будем передвигать наши объекты "вручную", то есть вводить заданные координаты в 3D-пространства, указывающие, куда объект должен двигаться. В то же время, нам необходимо, чтобы движение объекта было в определённой степени автоматизировано. То есть просто указать определённое направление и скорость движения, что, как мы знаем, задаётся вектором скорости (velocity vector). Мы уже определили вектор скорости, назвав его m_velocity. В этом случае в каждом кадре объект будет применять движение, основанное на времени (time-based movement), для перемещения себя самого с заданной скоростью.
Бывают случаи, когда мы не хотим, чтобы наши объекты двигались бесконечно, а хотим, чтобы со временем их движение замедлялось и через какое-то время они сами полностью останавливались. Представь, что мы создали объект автомобиль и хотим задать ему скорость, чтобы он начал движение в направлении "вперёд" (собственно для этого и применяется вектор m_forward) на протяжении всего времени, пока игрок удерживает нажатой клавишу "вперёд" (обычно это стрелка вверх на клавиатуре). Когда игрок отпускает кнопку "вперёд", необходимо, чтобы автомобиль самостоятельно стал замедляться (снижать скорость), а спустя какое-то время полностью остановился бы. Как этого добиться? Конечно же путём применения коэффициента трения (friction coefficient), для чего и нужен переменный член m_friction. Коэффициент трения применяется к вектору скорости в каждом кадре для того, чтобы замедлить движение объекта. В течение всего времени, пока игрок удерживает нажатой кнопку "вперёд", сила скорости (velocity force) будет прибавляться к вектору скорости автомобиля, преодолевая силу трения. В противном случае, при отжатой кнопке "вперёд", коэффициент трения (величина которого обычно составляет 0,99) будет снижать скорость в каждом кадре, что даст эффект постепенного замедления движения автомобиля (см. Рис.3).

Закрыть
noteОбрати внимание

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

Осталось ещё немного переменных членов класса SceneObject, которые нам необходимо обсудить.
m_collisionStamp применяется для сохранения в нём самого последнего кадра, в котором было зафиксировано столкновение объекта. Мы уже обсуждали назначение "штампа столкновений" и даже применили его на практике, при разработке системы ввода. Сейчас нам на самом деле не нужны штампы столкновений (collision stamps). Но в последующих главах ты обязательно увидишь как он применяется для определения момента времени, когда объект столкнулся с чем-либо.
И ещё 5 флагов, которые используются движком для различных задач по управлению объектом:

Флаг Значение
m_visible При значении TRUE объект видим. Невидимые объекты просто не рендерятся.
m_enabled При значении TRUE объект "включен" (существует). Отключенные объекты не обновляются.
m_ghost При значении TRUE объект является "призраком". Объекты-призраки "физически" не могут столкнуться с чем-либо.
m_ignoreCollisions При значении TRUE объект детектирует столкновения. При значении FALSE объект тем не менее сталкивается с другими объектами, но столкновения не регистрируются.
m_touchingGround При значении TRUE объект коснулся "земной поверхности".

Во второй половине файла SceneObject.cpp размещены реализации служебных функций для установки (set) и запроса состояния (retrieve) каждого из этих флагов. Они используются при необходимости.

Обновление и рендеринг объектов

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

Функция Update

Её реализация выглядит так:

Фрагмент SceneObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляем состояние объекта.
//-----------------------------------------------------------------------------
void SceneObject::Update( float elapsed, bool addVelocity )
{
	// Рассчитываем трение для данного обновления (если оно есть).
	float friction = 1.0f - m_friction * elapsed;

	// Перемещаем объект.
	m_velocity *= friction;
	if( addVelocity == true )
	{
		D3DXVECTOR3 velocity = m_velocity * elapsed;
		AddTranslation( velocity.x, velocity.y, velocity.z );
	}

	// Вращаем объект.
	m_spin *= friction;
	D3DXVECTOR3 spin = m_spin * elapsed;
	AddRotation( spin.x, spin.y, spin.z );

	// Обновляем мировую матрицу (world matrix) объекта.
	D3DXMatrixMultiply( &m_worldMatrix, &m_rotationMatrix, &m_translationMatrix );

	// Создаём матрицу вида (view matrix) для объекта.
	D3DXMatrixInverse( &m_viewMatrix, NULL, &m_worldMatrix );

	// Обновляем вектор объекта, направленный вперёд (object's forward vector).
	m_forward.x = (float)sin( m_rotation.y );
	m_forward.y = (float)-tan( m_rotation.x );
	m_forward.z = (float)cos( m_rotation.y );
	D3DXVec3Normalize( &m_forward, &m_forward );

	// Обновляем вектор объекта, направленный вправо (object's right vector).
	m_right.x = (float)cos( m_rotation.y );
	m_right.y = (float)tan( m_rotation.z );
	m_right.z = (float)-sin( m_rotation.y );
	D3DXVec3Normalize( &m_right, &m_right );

	// Обновляем ограничивающий объём объекта с применением одной только матрицы трансляции (translation matrix).
	// Это установит оси, приложенные к ограничивающему прямоугольнику вокруг объекта в мировом пространстве
	// (world space) вместо локального пространства объекта (object's local space).
	RepositionBoundingVolume( &m_translationMatrix );
}
...

Функция Update принимает 2 вводных параметра:

  • elapsed - время, прошедшее между выводом предыдущего и текущего кадра;
  • addVelocity - флаг, указывающий функции, применять или нет к объекту его внутреннюю (internal) скорость. Если помнишь, мы говорили о том, что объект должен обладать вектором скорости, который позволит автоматизировать его движение в пространстве. По сути данный флаг включает (TRUE) и отключает (FALSE) автоматическое движение объекта.

Если необходимо регулировать скорость движения объекта "вручную", то во втором параметре достаточно просто указать значение FALSE. В этом случае объект будет двигаться только если явно задать его скорость. При выборе автоматического режима движения объекта (когда во втором параметре указано TRUE) мы указываем объекту начальную скорость движения и он двигается с учётом законов физики и коэффициента трения, конечно же. В функции Update стоит проверка данного флага на истинность:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Перемещаем объект.
	m_velocity *= friction;
	if( addVelocity == true )
	{
		D3DXVECTOR3 velocity = m_velocity * elapsed;
		AddTranslation( velocity.x, velocity.y, velocity.z );
	}
...

В случае, когда второй параметр выставлен в TRUE, мы подсчитываем величину движения объекта в данном кадре, основываясь на переменной elapsed (временной интервал между предыдущим и текущим кадрами) и затем добавляем координаты вектора скорости к трансляции позиции объекта (object's translation). В расчётах также участвует коэффициент трения, применяемый в данном кадре на основе всё той же переменной elapsed. Скорость затем перемножается на коэффициент трения с целью её снижения до применения трансляции позиции объекта.
...
Класс SceneObject также включает в себя ещё один вектор, о котором мы не упоминали ранее - m_spin. Данный вектор работает по тому же принципу, что и вектор скорости, разве что в этом случае он указывает, как быстро объект будет вращаться ("spin" - англ. вращаться, вертеться вокруг оси) по любой из своих осей. Мы будем применять данный вектор для создания например таких объектов, как разбросанное по карте оружие (weapon pickup), которое лежит на полу, вращаясь по оси Z в ожидании, когда его подберёт игрок. Как видно из исходного кода, вектор m_spin обрабатывается тем же способом, что и m_velocity:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Вращаем объект.
	m_spin *= friction;
	D3DXVECTOR3 spin = m_spin * elapsed;
	AddRotation( spin.x, spin.y, spin.z );
...

Здесь первым делом применяем к скорости вращения коэффициент трения. Затем величина вращения применяется к объекту в текущем кадре, с учётом переменной elapsed. И уже затем скорость вращения добавляется к объекту.
Как только трансляция позиции объекта и скорость его вращения были обновлены, рассчитываем мировую матрицу (world matrix) и матрицу вида (view matrix) объекта. Для расчёта мировой матрицы применяем функцию D3DXMatrixMultiply, которая просто перемножает две матрицы друг с другом. Таким нехитрым способом мы можем комбинировать эффекты обеих матриц преобразований. Так, для создания мировой матрицы необходимо скомбинировать (т.е. перемножить) матрицу трансляции позиции (translation matrix) и матрицу вращения (rotation matrix) объекта.
Матрица трансляции позиции объекта необходима для отслеживания положения объекта в 3D-пространстве. Другими словами она является матричным представлением вектора трансляции позиции объекта (object's translation vector).
Матрица вращения применяется для отслеживания вращения объекта (или его направление - facing) и представляет собой матричное представление вектора вращения. Скомбинировав эти две матрицы вместе, мы получаем в результате мировую матрицу для данного объекта, которая трансформирует соответствующим образом 3D-меш объекта (если объект рендерится) из пространства модели (model, local space) в мировое пространство (world space), чтобы он отображался в корректной позиции и с корректным вращением в 3D-пространстве.

Если для тебя осталось загадкой, откуда мы берём матрицы трансляции позиции и вращения, то сейчас мы поясним это. Наши объекты используют 3D-векторы для постоянного отслеживания своего положения и вращения. В нашем случае мы назвали их m_translation и m_rotation соответственно. Ты перемещаешь объект путём изменения его вектора трансляции позиции (translation vector) и вращаешь его, изменяя вектор вращения. Для создания мировой матрицы необходимо сперва создать ещё две других, которые представляют данные, сохранённые в этих двух векторах. В этом случае, повторимся, матрица трансляции позиции (translation matrix) представляет собой трансляцию положения объекта в матрице, а матрица вращения (rotation matrix) - трансляцию вращения объекта в матрице. Всякий раз, при изменении векторов трансляции и вращения их соответствующие матрицы должны быть обновлены.
Матрица трансляции создаётся так:

Функция D3DXMatrixTranslation
D3DXMatrixTranslation( &m_translationMatrix, m_translation.x, m_translation.y, m_translation.z);

Функция D3DXMatrixTranslation экспонирована библиотекой D3DX и вызывается всякий раз при изменении вектора трансляции. Она специально разработана для создания матрицы трансляции позиции из 3D-вектора. Последние 3 её параметра позволяют вводить координаты x, y, z, которые берутся прямо из вектора трансляции. Первый параметр - это указатель на матрицу, в которой будет сохранён результат операции после завершения работы функции.
Создание матрицы вращения чуть сложнее:

D3DXMATRIX rotation X, rotation Y;
D3DXMatrixRotationX( &rotationX, m_rotation.x );
D3DXMatrixRotationY( &rotationY, m_rotation.y );
D3DXMatrixRotationZ( &m_rotationMatrix, m_rotation.z );
D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationX );
D3DXMatrixMultiply( &m_rotationMatrix, &m_rotationMatrix, &rotationY );

Функции D3DXMatrixRotationX, D3DXMatrixRotationY и D3DXMatrixRotationZ экспонированы библиотекой D3DX и вызываются всякий раз при изменении вектора вращения. Они применяются для создания матрицы вращения объекта вкруг его соответствующих осей X, Y и Z. Как только мы подготовили 3 отдельные матрицы вращения (по одной на каждую из осей) мы их комбинируем (= перемножаем) с помощью функции D3DXMatrixMultiply, причём, как видно из кода, делаем это 2 раза. В результате получаем матрицу вращения, в которой скомбинировано вращение объекта по каждой из трёх осей.
Получив матрицы трансляции и вращения мы можем смело комбинировать их в одну мировую матрицу:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Обновляем мировую матрицу (world matrix) объекта.
	D3DXMatrixMultiply( &m_worldMatrix, &m_rotationMatrix, &m_translationMatrix );
...

Но как создать матрицу вида (view matrix)? На самом деле это очень просто и выполняется всего одной строкой кода. Матрица вида представляет собой "инверсию" мировой матрицы и рассчитывается путём вызова функции D3DXMatrixInverse, также экспонированной библиотекой D3DX:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Создаём матрицу вида (view matrix) для объекта.
	D3DXMatrixInverse( &m_viewMatrix, NULL, &m_worldMatrix );
...

Функция D3DXMatrixInverse получает в качестве вводного параметра указатель на имеющуюся мировую матрицу, инвертирует её и сохраняет результат в заранее подготовленную матрицу вида (см. первый параметр). Для чего же необходимо рассчитывать матрицу вида для наших объектов? Когда мы обсуждали эту тему, мы говорили, что матрицу вида достаточно рассчитать лишь однажды в данной сцене, которая будет служить своеобразным "смотровым окном" в 3D-пространство игровой сцены. Тогда зачем её рассчитывать для каждого объекта сцены?
Ответ очень прост. Чуть позднее от класса SceneObject мы ответвим объект игрока (player object). Оснастив матрицей вида каждый создаваемый объект, мы получаем возможность видеть мир (игровую сцену) из перспективы любого из них, путём использования их матриц вида. Это очень здорово, особенно принимая во внимание тот факт, что игроки сами будут являться объектами сцены. Таким образом мы можем видеть сцену "глазами" не только каждого игрока, но и любого объекта, даже упаковки с патронами. Данную возможность можно применить, например, для создания камер видеонаблюдения, закреплённых на стенах. С каждой из них можно получить вид, просто запросив матрицу вида. Круто, не правда ли?
Мы уже почти полностью рассмотрели функцию Update. Как только мы рассчитали мировую матрицу и матрицу вида, следующий шаг - рассчитать векторы, указывающие вперёд (forward vector) и вправо (right vectror) объекта, что потребует сведений из школьного курса тригонометрии. В исходном коде наглядно представлено использование переменного члена m_rotation для расчёта векторов, указывающих вперёд и вправо, которые изменяются только в случае вращения объекта:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Обновляем вектор объекта, направленный вперёд (object's forward vector).
	m_forward.x = (float)sin( m_rotation.y );
	m_forward.y = (float)-tan( m_rotation.x );
	m_forward.z = (float)cos( m_rotation.y );
	D3DXVec3Normalize( &m_forward, &m_forward );

	// Обновляем вектор объекта, направленный вправо (object's right vector).
	m_right.x = (float)cos( m_rotation.y );
	m_right.y = (float)tan( m_rotation.z );
	m_right.z = (float)-sin( m_rotation.y );
	D3DXVec3Normalize( &m_right, &m_right );
...

Мы не будем вдаваться в тригонометрическую теорию. Для этого достаточно открыть любой школьный учебник геометрии и пролистать разделы основных тригонометрических функций: синус (sin), косинус (cos), тангенс (tan).
Как только векторы, указывающие вперёд и вправо рассчитаны, мы должны убедиться, что они представляют собой единичные (unit) векторы, применив к обоим функцию нормализации D3DXVec3Normalize, экспонированную библиотекой D3DX. В обоих случаях её применение даст на выходе вектор длиной ровно 1 единицу (unit). Другими словами, так векторы станут чисто направляющими (directional), т.е. без учёта их длины, что, собственно, и требуется.
Финальный шаг - репозиционировать ограничивающий объём (bounding volume) объекта:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Обновляем ограничивающий объём объекта с применением одной только матрицы трансляции (translation matrix).
	// Это установит оси, приложенные к ограничивающему прямоугольнику вокруг объекта в мировом пространстве
	// (world space) вместо локального пространства объекта (object's local space).
	RepositionBoundingVolume( &m_translationMatrix );
...
Рис.4 Трансформация ограничивающего объёма из пространства модели в мировое пространство
Рис.4 Трансформация ограничивающего объёма из пространства модели в мировое пространство

Как ты знаешь, класс SceneObject ветвится от класса BoundingVolume. Это значит, что наш объект наделён набором из трёх ограничивающих объёмов (куб, сфера, эллипсоид), встроенные в него путём наследования, и полностью заключающие в себе 3D-меш объекта. Ограничивающие объёмы, "обведённые" вокруг меша объекта, расположены в пространстве модели (model, local space). Но для того, чтобы быть уверенным в том, что ограничивающие объёмы корректно позиционированы по отношению к другим объектам игровой сцены (например, для целей определения столкновений), мы обязательно должны трансформировать их в мировое пространство.
Мы делаем это с помощью специальной функции RepositionBoundingVolume (экспонирована классом BoundingVolume), указав в качестве её единственного параметра матрицу трансляции (translation matrix). Здесь принцип тот же, что и при трансформировании в мировое пространство (world space) объектов сцены. Проблема здесь заключается в том, что мировая матрица также учитывает вращение объекта или, как в нашем случае, ограничивающих объёмов. На деле это означает, что ограничивающие объёмы будут вращаться вместе с мешами объектов, внутри которых они заключены, что крайне нежелательно, т.к. усложняет определение столкновений. Куда проще рассчитать столкновение ограничивающих объёмов, сориентированных по осям проекций, что означает их корректное позиционирование в мировом пространстве, вокруг меша объекта. При этом они не должны вращаться вместе с объектом! Другими словами, объект может свободно вращаться внутри своего ограничивающего объёма. Для достижения этой цели, вместо того, чтобы трансформировать ограничивающие объёмы путём применения мировой матрицы, мы будем трансформировать их матрицей трансляции (translation matrix; См. Рис.4).
Так вот. Для выполнения данного репозиционирования мы просто вызываем функцию RepositionBoundingVolume и передаём в её единственном параметре матрицу трансляции объекта. Это обеспечит "физическое" присутствие ограничивающего объёма именно в мировом пространстве (world space), а не в пространстве модели объекта (object's model space).
Вот и весь рассказ о функции Update. Настоятельно рекомендуем просмотреть документацию DirectX SDK, по теме векторов и матриц (всё на английском!).

Функция Render

Её реализация совсем небольшая по объёму и выглядит так:

Фрагмент SceneObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Renders the object.
//-----------------------------------------------------------------------------
void SceneObject::Render( D3DXMATRIX *world )
{
	// Игнорируем объект, если он не имеет 3D-меша.
	if( m_mesh == NULL )
		return;

	// Проверяем случай, когда мировая матрица преобразований объекта (the object's world tranformation matrix) была переопределена.
	if( world == NULL )
		g_engine->GetDevice()->SetTransform( D3DTS_WORLD, &m_worldMatrix );
	else
		g_engine->GetDevice()->SetTransform( D3DTS_WORLD, world );

	// Рендерим меш объекта.
	m_mesh->Render();
}
...

Первое, на что ты наверняка обратил внимание, это тот факт, что функция Render принимает в качестве вводного параметра указатель на матрицу. По её имени нетрудно догадаться, что это указатель на мировую матрицу (world matrix). Причина того, что в качестве вводного параметра мы указываем мировую матрицу довольно проста - для большей гибкости. Несмотря на то, что класс SceneObject имеет свою собственную внутреннюю мировую матрицу, иногда бывает необходимость переопределить её и позиционировать объект, применив свою собственную мировую матрицу. В функции Render, кстати, даже есть проверка, не равна ли мировая матрица из вводного параметра NULL. Если она равна NULL, значит альтернативная (пользовательская) мировая матрица не передаётся в функцию, поэтому мы будем использовать внутреннюю мировую матрицу класса SceneObject. Установка мировой матрицы производится тем же способом, что и установка двух других матриц (вида и проекции), а именно путём применения функции SetTransform на объекте устройства Direct3D. Разница состоит лишь в том, что здесь в качестве первого параметра передаётся D3DTS_WORLD для информирования Direct3D о том, что мы хотим использовать мировую матрицу.
После установки мировой матрицы, следующий шаг - вызов функции Render на 3D-меше объекта. Не забываем, что объект может вовсе не иметь меша. В этом случае локальная переменная m_mesh будет равна NULL. Если при это всё равно вызвать на меше функцию Render, это вызовет ошибку доступа (access violation error). Поэтому для предотвращения подобных ситуаций уже в самом начале реализации функции Render класса SceneObject мы ставим соответствующую проверку:

Фрагмент SceneObject.cpp (Проект Engine)
...
	// Игнорируем объект, если он не имеет 3D-меша.
	if( m_mesh == NULL )
		return;
...
Закрыть
noteОбрати внимание

Подобные проверки нужны только в тех случаях, где существует хотя бы небольшая вероятность того, что указатель будет равен NULL. Они позволяют создавать более надёжный код и повышают его юзабилити.

Уже в следующем подпункте мы рассмотрим класс AnimatedObject, первый из классов, ветвящихся от класса SceneObject.

Анимированные объекты

Анимированный объект - это обычный объект с мешем, который может быть анимирован. Базовый класс SceneObject не поддерживает анимирование меша. В этом нет ничего страшного, если вспомнить, что большинство игровых объектов зачастую не требуют анимирования. К тому же базовый класс SceneObject мы оснастили минимальным, самым общим функционалом.
Но раз уж оставшееся меньшинство объектов всё-таки включают в себя анимированные меши, ассоциированные с ними (например объект игрока), то мы ответвим от базового класса SceneObject новый класс AnimatedObject, который и будет специализироваться на воспроизведении анимаций меша объекта.

Создаём AnimatedObject.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "AnimatedObject.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле AnimatedObject.h набираем следующий код:

AnimatedObject.h (Проект Engine)
//-----------------------------------------------------------------------------
// Потомок класса SceneObject, поддерживающий анимацию мешей.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef ANIMATED_OBJECT_H
#define ANIMATED_OBJECT_H

//-----------------------------------------------------------------------------
// Определение типа анимированного объекта
//-----------------------------------------------------------------------------
#define TYPE_ANIMATED_OBJECT 1

//-----------------------------------------------------------------------------
// Animated Object Class
//-----------------------------------------------------------------------------
class AnimatedObject : public SceneObject, public ID3DXAnimationCallbackHandler
{
public:
	AnimatedObject( char *meshName, char *meshPath = "./", unsigned long type = TYPE_ANIMATED_OBJECT );
	virtual ~AnimatedObject();

	virtual void Update( float elapsed, bool addVelocity = true );

	void PlayAnimation( unsigned int animation, float transitionTime, bool loop = true );
	ID3DXAnimationController *GetAnimationController();

private:
	virtual HRESULT CALLBACK HandleCallback( THIS_ UINT Track, LPVOID pCallbackData );

private:
	ID3DXAnimationController *m_animationController; // Контроллер управления проигрыванием анимации меша.
	unsigned int m_currentTrack; // Трек, на котором в данный момент проигрывается анимация.
	float m_currentTime; // Таймер, применяемый для анимации.
};

#endif

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код AnimatedObject.h

Как и ожидалось, класс AbimatedObject ветвится от класса SceneObject. Вместе с тем, класс AnimatedObject также наследуется от интерфейса ID3DXAnimationCallbackHandler, экспонированного библиотекой D3DX. Данный интерфейс содержит в себе всего одну функцию HandleCallback, которая, как видно из её названия, является функцией обратного вызова и которую нам необходимо переопределить (см. второй параметр объявления класса AnimatedObject). Забегая вперёд, скажем, что в реализации класса AnimatedObject, которую мы скоро разместим в AnimatedObject.cpp, данная функция не делает ровным счётом ничего. Нам и не требуется использовать её в данном классе, но нам необходимо сделать её виртуальной (virtual) для того, чтобы она могла быть переопределена другими классами, которые мы чуть позднее ответвим от класса AnimatedObject (например, класс PlayerObject). Чуть позднее мы обязательно разберём действие функции HandleCallback, но сейчас всё-таки вернёмся к разбору объявления класса AnimatedObject.
Следующая точка интереса - это конструктор класса, где мы применяем идентификатор типа TYPE_ANIMATED_OBJECT. Это новый идентификатор типа, определённый в самом начале AnimatedObject.h и используемый по умолчанию всякий раз при создании новых инстансов (экземпляров) класса AnimatedObject.
Другой интересный момент заключается в том, что класс AnimatedObject имеет собственную функцию Update, переопределяющую (override) ту, что представлена в родительском классе SceneObject.
Чуть ниже идёт функция PlayAnimation, которая применяется для проигрывания одной из анимаций, ассоциированных с мешем, назначенным объекту.

Создаём AnimatedObject.cpp (Проект Engine)

В файле исходного кода AnimatedObject.cpp будут размещаться реализации функций, объявленных в AnimatedObject.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "AnimatedObject.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле AnimatedObject.cpp набираем следующий код:

AnimatedObject.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация классов и функций, объявленных в AnimatedObject.h
// 
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The animated object class constructor.
//-----------------------------------------------------------------------------
AnimatedObject::AnimatedObject( char *meshName, char *meshPath, unsigned long type ) : SceneObject( type, meshName, meshPath, false )
{
	// Создаём клон контроллера анимации меша.
	if( GetMesh() != NULL )
		GetMesh()->CloneAnimationController( &m_animationController );
	else
		m_animationController = NULL;

	// Устанавливаем скорость обоих треков на полную.
	if( m_animationController != NULL )
	{
		m_animationController->SetTrackSpeed( 0, 1.0f );
		m_animationController->SetTrackSpeed( 1, 1.0f );
	}

	// Очищаем переменные, используемые в анимации.
	m_currentTrack = 0;
	m_currentTime = 0.0f;
}

//-----------------------------------------------------------------------------
// The animated object class destructor.
//-----------------------------------------------------------------------------
AnimatedObject::~AnimatedObject()
{
	SAFE_RELEASE( m_animationController );
}

//-----------------------------------------------------------------------------
// Обновляем состояние объекта.
//-----------------------------------------------------------------------------
void AnimatedObject::Update( float elapsed, bool addVelocity )
{
	// Позволяем обновиться базовому объекту сцены.
	SceneObject::Update( elapsed, addVelocity );

	// Проверяем, есть ли у меша объекта контроллер анимации.
	if( m_animationController )
	{
		// Увеличиваем время действия контроллера анимации объекта.
		m_animationController->AdvanceTime( elapsed, this );

		// Следим за текущим временем для выполнения анимации.
		m_currentTime += elapsed;
	}

	// Обновляем меш.
	if( GetMesh() != NULL )
		GetMesh()->Update();
}

//-----------------------------------------------------------------------------
// Проигрывает данную анимацию с данным временем перехода (transition time).
//-----------------------------------------------------------------------------
void AnimatedObject::PlayAnimation( unsigned int animation, float transitionTime, bool loop )
{
	// Проверяем наличие у объекта валидного контроллера анимации.
	if( m_animationController == NULL )
		return;

	// Проверяем, что время перехода (transition time) всегда больше 0.
	if( transitionTime <= 0.0f )
		transitionTime = 0.000001f;

	// Находим, на каком треке воспроизводить новую анимацию.
	unsigned int newTrack = ( m_currentTrack == 0 ? 1 : 0 );

	// Получаем указатель на набор анимаций для воспроизведения.
	ID3DXAnimationSet *as;
	m_animationController->GetAnimationSet( animation, &as );

	// Устанавливаем набор анимаций на новый трек.
	m_animationController->SetTrackAnimationSet( newTrack, as );

	// Очищаем все события, которые в данный момент установлены на треках.
	m_animationController->UnkeyAllTrackEvents( m_currentTrack );
	m_animationController->UnkeyAllTrackEvents( newTrack );

	// Проверяем случаи, когда анимация зациклена (looped) или проигрывается лишь однажды.
	if( loop == true )
	{
		// Переходим на новый трек в пределах времени перехода (transition time).
		m_animationController->KeyTrackEnable( m_currentTrack, false, m_currentTime + transitionTime );
		m_animationController->KeyTrackWeight( m_currentTrack, 0.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->KeyTrackWeight( newTrack, 1.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
	}
	else
	{
		// Останавливаем текущий трек, и начинаем воспроизводить новый без перехода.
		m_animationController->SetTrackEnable( m_currentTrack, false );
		m_animationController->SetTrackWeight( m_currentTrack, 0.0f );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->SetTrackWeight( newTrack, 1.0f );
		m_animationController->SetTrackPosition( newTrack, 0.0f );
		m_animationController->KeyTrackEnable( newTrack, false, m_currentTime + as->GetPeriod() );
	}

	// Очищаем указатель на набор анимаций.
	as->Release();

	// Новый трек становится текущим треком.
	m_currentTrack = newTrack;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на контроллер анимации объекта.
//-----------------------------------------------------------------------------
ID3DXAnimationController *AnimatedObject::GetAnimationController()
{
	return m_animationController;
}

//-----------------------------------------------------------------------------
// Функция обратного вызова анимации.
//-----------------------------------------------------------------------------
HRESULT CALLBACK AnimatedObject::HandleCallback( THIS_ UINT Track, LPVOID pCallbackData )
{
	return S_OK;
}

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код AnimatedObject.cpp

Начнём с конструктора класса AnimateObject:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The animated object class constructor.
//-----------------------------------------------------------------------------
AnimatedObject::AnimatedObject( char *meshName, char *meshPath, unsigned long type ) : SceneObject( type, meshName, meshPath, false )
{
	// Создаём клон контроллера анимации меша.
	if( GetMesh() != NULL )
		GetMesh()->CloneAnimationController( &m_animationController );
	else
		m_animationController = NULL;

	// Устанавливаем скорость обоих треков на полную.
	if( m_animationController != NULL )
	{
		m_animationController->SetTrackSpeed( 0, 1.0f );
		m_animationController->SetTrackSpeed( 1, 1.0f );
	}

	// Очищаем переменные, используемые в анимации.
	m_currentTrack = 0;
	m_currentTime = 0.0f;
}
...

При вызове конструктора класса AnimatedObject, первым делом вызывается конструктор базового класса SceneObject с передачей ему необходимых параметров. Ты должно быть заметил, как передаётся идентификатор типа объекта (object's type identifier), который будет установлен базовым классом SceneObject. Таким образом, всякий раз, когда объект запрашивает свой тип, он не получит в ответ идентификатор типа базового объекта TYPE_SCENE_OBJECT (прописан в начале SceneObject.h). Вместо этого он вернёт TYPE_ANIMATED_OBJECT (прописан в начале AnimatedObject.h) или любой другой идентификатор, который изначально был передан в конструктор класса AnimatedObject. Также заметим, что в последнем параметре стоит значение FALSE, что означает, что мы не хотим использовать меш совместно с другими объектами.
Как только конструктор базового класса SceneObject закончил свою работу, мы знаем, что меш был загружен, поэтому теперь мы можем смело начинать выполнение конструктора класса AnimatedObject. В случае, когда меш загружен (напомним, что объект вовсе не обязательно должен иметь меш), первым делом необходимо клонировать (создать копию) его контроллера анимации. Контроллер анимации меша является сердцем класса AnimatedObject и его наиболее важным компонентом. Чтобы сделать копию интерфейса контроллера анимации (ID3DXAnimationController) мы просто вызываем функцию CloneAnimationController на меше. Её реализацию мы разместили чуть ранее в Mesh.cpp. Она предельно проста и всё, что она делает - это вызывает функцию CloneAnimationController на интерфейсе ID3DXAnimationController, определённом в классе Mesh. Ведь контроллер анимации загружается в момент создания меша. Интерфейс ID3DXAnimationController:

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

Как только у нас есть валидный контроллер анимации, нам необходимо первым делом установить скорость первого и второго треков. Установка скорости первого трека в 1.0 даёт проигрывание всех анимаций на нём на полной скорости. Чуть позднее мы рассмотрим этот момент более подробно. Ты увидишь как контроллер анимации применяется "на лету", как и те две переменные, которые мы очистили в конструкторе.

Реализация функции Update

Фрагмент AnimatedObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляем состояние объекта.
//-----------------------------------------------------------------------------
void AnimatedObject::Update( float elapsed, bool addVelocity )
{
	// Позволяем обновиться базовому объекту сцены.
	SceneObject::Update( elapsed, addVelocity );

	// Проверяем, есть ли у меша объекта контроллер анимации.
	if( m_animationController )
	{
		// Увеличиваем время действия контроллера анимации объекта.
		m_animationController->AdvanceTime( elapsed, this );

		// Следим за текущим временем для выполнения анимации.
		m_currentTime += elapsed;
	}

	// Обновляем меш.
	if( GetMesh() != NULL )
		GetMesh()->Update();
}
...

Функция Update класса AnimatedObject просто переопределяет оную класса SceneObject. Раз уж мы хотим, чтобы наш AnimatedObject вцелом вёл себя точно так же как и все другие объекты, мы обязательно должны первым делом сделать вызов функции Update класса SceneObject:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
	// Позволяем обновиться базовому объекту сцены.
	SceneObject::Update( elapsed, addVelocity );
...

И только после этого свои операции выполняет функция Update класса AnimatedObject. Здесь мы первым делом проверяем наличие валидного контроллера анимации. Если у объекта нет даже меша, то контроллера анимации нет и подавно. Если контроллер анимации присутствует, мы продолжаем его обновление путём вызова его функции AdvanceTime, которая принимает 2 вводных параметра:

  • количество времени опережения контроллера анимации (в секундах);
  • указатель на интерфейс функции обратного вызова пользовательской анимации (user-defined animation call-back handler interface).

В нашем случае в первом параметре выставляем заранее подсчитанную переменную elapsed (которая передаётся в качестве вводного параметра функции Update). Во втором параметре передаём указатель this, чтобы использовать данный класс в качестве интерфейса функции обратного вызова пользовательской анимации. Если помнишь, мы ветвим наш класс AnimatedObject в том числе от интерфейса ID3DXAnimationCallbackHandler (см. AnimatedObject.h). Поэтому мы можем переопределить функцию HandleCallback, встроенную в данный интерфейс. Функция AdvanceTime ищет указатель на пользовательский инстанс интерфейса ID3DXAnimationCallbackHandler, который представляет собой то, во что мы превратили класс AnimatedObject, ответвив его от интерфейса ID3DXAnimationCallbackHandler.
Когда мы увидим класс AnimatedObject в деле (при создании игры), ты увидишь, как в анимацию можно добавлять так называемые ключи анимации (animation keys). Ключ может быть установлен в качестве триггера (переключателя), действующего в определённое время при проигрывании анимации. Так вот, всякий раз при вызове функции AdvanceTime, контроллер анимации проверяет, задействованы ли в данной анимации пользовательские ключи. Если да, то контроллер анимации вызывает функцию HandleCallback, чтобы таким образом обработать данный ключ. Ты узнаешь об этом более подробно чуть позднее, когда мы будем применять ключи анимации для реализации шагов (footsteps) в анимации бега объекта игрока.
После обновления контроллера анимации осталось выполнить ещё 2 важных шага.
Во-первых необходимо инкрементировать (увеличить) переменный член m_currentTime на величину elapsed. Это позволит нам отслеживать течение глобального времени анимации (global animation time). Также мы будем применять данный переменный член в различных временных операциях функции PlayAnimation, которую мы рассмотрим уже очень скоро.
Последний шаг - обновление меша объекта, путём вызова не нём функции Update класса Mesh.

Закрыть
noteОбрати внимание

Ты должно быть заметил, что мы обновляем меш именно внутри реализации класса AnimatedObject, а не SceneObject. А всё из-за того, что функция Update класса Mesh применяется для обновления иерархии фреймов как раз с целью из анимирования. Раз мы решили, что только класс AnimatedObject будет поддерживать работу с анимациями, то и меш объекта нужно обновлять именно в этом классе.

Функция PlayAnimation

  • самая сложная и важная функция класса AnimatedObject.
Фрагмент AnimatedObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Проигрывает данную анимацию с данным временем перехода (transition time).
//-----------------------------------------------------------------------------
void AnimatedObject::PlayAnimation( unsigned int animation, float transitionTime, bool loop )
{
	// Проверяем наличие у объекта валидного контроллера анимации.
	if( m_animationController == NULL )
		return;

	// Проверяем, что время перехода (transition time) всегда больше 0.
	if( transitionTime <= 0.0f )
		transitionTime = 0.000001f;

	// Находим, на каком треке воспроизводить новую анимацию.
	unsigned int newTrack = ( m_currentTrack == 0 ? 1 : 0 );

	// Получаем указатель на набор анимаций для воспроизведения.
	ID3DXAnimationSet *as;
	m_animationController->GetAnimationSet( animation, &as );

	// Устанавливаем набор анимаций на новый трек.
	m_animationController->SetTrackAnimationSet( newTrack, as );

	// Очищаем все события, которые в данный момент установлены на треках.
	m_animationController->UnkeyAllTrackEvents( m_currentTrack );
	m_animationController->UnkeyAllTrackEvents( newTrack );

	// Проверяем случаи, когда анимация зациклена (looped) или проигрывается лишь однажды.
	if( loop == true )
	{
		// Переходим на новый трек в пределах времени перехода (transition time).
		m_animationController->KeyTrackEnable( m_currentTrack, false, m_currentTime + transitionTime );
		m_animationController->KeyTrackWeight( m_currentTrack, 0.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->KeyTrackWeight( newTrack, 1.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
	}
	else
	{
		// Останавливаем текущий трек, и начинаем воспроизводить новый без перехода.
		m_animationController->SetTrackEnable( m_currentTrack, false );
		m_animationController->SetTrackWeight( m_currentTrack, 0.0f );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->SetTrackWeight( newTrack, 1.0f );
		m_animationController->SetTrackPosition( newTrack, 0.0f );
		m_animationController->KeyTrackEnable( newTrack, false, m_currentTime + as->GetPeriod() );
	}

	// Очищаем указатель на набор анимаций.
	as->Release();

	// Новый трек становится текущим треком.
	m_currentTrack = newTrack;
}
...

Как всегда начинаем с изучения её вводных параметров:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
void AnimatedObject::PlayAnimation( unsigned int animation, float transitionTime, bool loop )
...

Первый параметр - идентификатор анимации, которую хотим воспроизвести. Контроллер анимации ссылается на ту или иную анимацию объекта, применяя её уникальный порядковый номер, начинающийся с 0. То есть первой анимации объекта соответствует порядковый номер 0, второй - 1 и т.д. Поэтому, чтобы, например, воспроизвести анимацию №4, в данном параметре передаём 3.

Закрыть
noteОбрати внимание

Здесь ты даже можешь применить энумерацию, дающую каждой из анимаций осмысленные имена (псевдонимы). В этом случае в первом параметре функции PlayAnimation можно просто указать ANIMATION_RUN, что, к примеру, будет соответствовать анимации с идентификатором 3.

Второй параметр представляет собой время перехода анимации (animation transition time). Анимированный меш всегда имеет анимацию, проигрываемую в данный момент (например, назначенную по умолчанию). Причём даже если анимация незациклена и закончилась, она всё равно рассматривается как текущая анимация. Это означает, что когда ты указываешь контроллеру анимации сменить текущую анимацию на другую, он вынужден остановить проигрывание текущей анимации и сразу начать воспроизведение новой. Проблема здесь заключается в том, что если резко прервать одну анимацию и начать другую, меш совершит резкий скачок из одного положения в другое. Ты наверняка видел подобный эффект в компьютерных играх. Время перехода как раз и нужно для того, чтобы указать контроллеру анимации необходимое время (в секундах) для совершения перехода от одной анимации к другой. Во время перехода контроллер анимации смешивает (blend) обе анимации вместе, при этом постепенно снижая "вес" текущей анимации и прибавляя оный новой. Примерно тот же принцип лежит в "сведении" двух музыкальных треков диск-жокеем.
Последний параметр - простой флаг, сигнализирующий должна ли анимация быть зацикленной (всё время повторяться) либо её необходимо воспроизвести всего 1 раз. При установке данного флага в TRUE, выбранная анимация будет повторяться снова и снова до тех пор, пока контроллер анимации не получит других команд от пользователя или игры. Зацикленная анимация великолепна для анимаций ходьбы (walk) или "ничегонеделанья" (idle). При установке в FALSE анимация будет воспроизведена всего 1 раз. При этом по достижении своего последнего кадра она остановится и меш будет выглядеть замершим на месте. Такие анимации часто применяются для анимирования смерти игрока, когда требуется, чтобы объект игрока упал замертво и больше не двигался.

Оказавшись внутри тела функции, первым делом снова проверяем наличие валидного контроллера анимации. Если его нет, функция досрочно завершает своё выполнение:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
	// Проверяем наличие у объекта валидного контроллера анимации.
	if( m_animationController == NULL )
		return;

	// Проверяем, что время перехода (transition time) всегда больше 0.
	if( transitionTime <= 0.0f )
		transitionTime = 0.000001f;

	// Находим, на каком треке воспроизводить новую анимацию.
	unsigned int newTrack = ( m_currentTrack == 0 ? 1 : 0 );

	// Получаем указатель на набор анимаций для воспроизведения.
	ID3DXAnimationSet *as;
	m_animationController->GetAnimationSet( animation, &as );
...

Даже при наличии валидного контроллера анимации мы снова проверяем, что переменная transitionTime больше 0. Делается это потому, что функции библиотеки D3DX большей частью "не любят", когда transitionTime вдруг оказывается равным 0 или меньше его, "вылетая" из приложения с трудноуловимыми ошибками. Причины данного "эффекта" неизвестны, документация DirectX SDK не сообщает ничего об этом. Поэтому для избежания неприятных ситуаций мы просто установим начальное значение transitionTime в 0.000001f, которое настолько близко к нолю, что этим можно пренебречь.
Далее выбираем, на каком из треков будет проигрываться новая анимация. Контроллер анимации использует треки (tracks) для проигрывания различных анимаций. Например, ты можешь воспроизводить анимацию WALK (ходьба) на первом треке, а анимацию SHOOT (выстрел) - на втором. Контроллер анимации смешивает (blend) обе этих анимации, в результате чего они воспроизводятся одновременно (игрок бежит и стреляет; в первых частях игр Resident Evil игрок мог стрелять только когда стоит на месте). Мы используем переменный член m_currentTrack для определения, на каком из треков проигрывается текущая анимация. Он необходим для определения, какой из треков свободен и готов к воспроизведению на нём новой анимации. Если текущая анимация воспроизводится на первом треке, то для новой анимации мы задействуем второй (в том числе для осуществления плавного перехода). И наоборот: если анимация воспроизводится на втором треке, то для новой мы задействуем первый.
Как только мы определили, на каком из треков будет воспроизведена новая анимация, сразу запрашиваем указатель на анимацию, которую хотим воспроизвести. Контроллер анимации рассматривает одну полную анимацию как анимационный набор (animation set). Для манипулирования анимационными наборами мы применим интерфейс с говорящим названием ID3DXAnimationSet. Получаем доступ к анимационному набору, который хотим воспроизвести, путём вызова функции GetAnimationSet, экспонированной контроллером анимации. Здесь в первом параметре передаём идентификатор анимации, которую будем воспроизводить (он берётся из первого вводного параметра функции PlayAnimation). Во втором стоит указатель на текущую анимацию, возвращаемый при завершении работы функции GetAnimationSet.
Следующий шаг - назначение новой анимации треку, на котором она будет воспроизводиться:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
	// Устанавливаем анимационный набор на новый трек.
	m_animationController->SetTrackAnimationSet( newTrack, as );

	// Очищаем все события, которые в данный момент установлены на треках.
	m_animationController->UnkeyAllTrackEvents( m_currentTrack );
	m_animationController->UnkeyAllTrackEvents( newTrack );
...

Это делается путём вызова функции SetTrackAnimationSet на контроллере анимации. В качестве параметров здесь передаётся идентификатор трека, на котором должна быть воспроизведена анимация (newTrack), а также указатель на саму анимацию (as).
Сразу после этого очищаем все события (events) на обоих треках путём поочерёдного вызова на каждом из них функции UnkeyAllTrackEvents. Здесь в качестве единственного параметра указывается идентификатор очищаемого трека.
Анимация работает путём назначения на треках т.н. событий, основанных на времени (time-based events), которые контролируют, как треки будут обрабатываться, а также когда именно в них необходимо вносить изменения. В таблице ниже представлены несколько наиболее часто встречающихся событий (ивентов), устанавливающихся на треки анимации:

Enable Событие, включающее или отключающее трек в данный момент времени.
Speed Событие, изменяющее скорость воспроизведения анимации на треке в данный момент времени.
Weight Событие, изменяющее т.н. "вес смешивания" (blending weight) анимации на треке в данный момент времени.

На данной стадии у нас всё готово для начала установки событий на обоих треках, чтобы контроллер анимации мог сменить текущую анимацию на новую. Как именно должны быть установлены события зависит лишь от одного фактора - будет ли анимация зациклена или воспроизведена лишь однажды. Другими словами, будет ли анимация на данном треке воспроизводиться снова и снова, либо она будет воспроизведена всего 1 раз и меш объекта "замрёт" на последнем кадре анимации. Далее в исходном коде как раз рассматриваются оба случая, обрамлённые условным переходом if. Первыми предопределены события для зацикленной (повторяющейся) анимации:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
	// Проверяем случаи, когда анимация зациклена (looped) или проигрывается лишь однажды.
	if( loop == true )
	{
		// Переходим на новый трек в пределах времени перехода (transition time).
		m_animationController->KeyTrackEnable( m_currentTrack, false, m_currentTime + transitionTime );
		m_animationController->KeyTrackWeight( m_currentTrack, 0.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->KeyTrackWeight( newTrack, 1.0f, m_currentTime, transitionTime, D3DXTRANSITION_LINEAR );
	}
...
Рис.5 Типы переходов от одной анимации меша к другой
Рис.5 Типы переходов от одной анимации меша к другой

Если новая анимация будет зацикленной (looped), тогда мы хотим плавно перейти от текущей анимации к новой. Для того, чтобы добиться этого, мы постепенно "вес смешивания" (blending weigth) трека с текущей анимацией, и одновременно увеличиваем "вес смешивания" трека с новой анимацией. В коде это выглядит как установка события веса (weight event) путём вызова функции KeyTrackWeight на каждом из треков. Здесь в первом параметре указываем идентификатор трека, на котором хотим установить данное событие. Во втором - передаём целевой вес смешивания, который хотим установить на данном треке. В нашем случае для старого трека устанавливаем 0.0, а для нового 1.0 (что соответствует 100%). Третий параметр указывает, когда мы хотим произвести переход. В четвёртом параметре указывается как долго будет длиться переход (transitionTime). В нашем случае в качестве начала перехода указана переменная m_currentTime, что означает начать переход немедленно. Время перехода обозначено переменной transitionTime, передаваемой в качестве вводного параметра функции PlayAnimation.
Все 4 строки внутри цикла if в совокупности означают, что в течение периода времени обозначенного в пределах от m_currentTrack до m_currentTrack+transitionTime вес старого трека изменится с 1.0 до 0.0, а нового - с 0.0 до 1.0. Последний параметр контролирует каким образом будет происходить переход и может принимать всего 2 значения:

  • D3DXTRANSITION_LINEAR,
  • D3DXTRANSITION_EASEINEASEOUT.

(См. Рис. 5) Мы, для случая зацикленной анимации, выбрали значение D3DXTRANSITION_LINEAR для равномерного изменения значений с течением времени.
Помимо этого мы активируем (включаем) трек с новой анимацией путём применения функции SetTrackEnable, в параметрах которой указаны идентификатор трека и флаг булева типа (TRUE - включить трек, FALSE - выключить). В нашем случае значение флага - TRUE. Данная функция немедленно включает трек.
Вместе с тем, нам необходимо отключить трек с текущей анимацией. Но мы не можем сделать это немедленно, т.к. необходимо "выждать" снижение его веса смешивания с 1.0 до 0.0. В противном случае во время анимации будет заметен "скачок", моментальный переход меша объекта от одного состояния к другому. Для этого мы снова применяем функцию SetTrackEnable, но уже с флагом FALSE во втором параметре. Так как отключаемому треку не требуется переход, то достаточно просто указать, когда мы хотим отключить трек. Мы знаем длительность перехода (transitionTime). Значит нам нужно подгадать время так, чтобы отключение текущего трека произошло сразу по завершению перехода. Мы можем подсчитать это время путём простого сложения значений переменных m_currentTime и transitionTime. Это и будет временем отключения трека.
Смотрим вторую часть условного перехода if для случая незацикленной анимации, воспроизводимой всего 1 раз:

Фрагмент AnimatedObject.cpp (Проект Engine)
...
	else
	{
		// Останавливаем текущий трек, и начинаем воспроизводить новый без перехода.
		m_animationController->SetTrackEnable( m_currentTrack, false );
		m_animationController->SetTrackWeight( m_currentTrack, 0.0f );
		m_animationController->SetTrackEnable( newTrack, true );
		m_animationController->SetTrackWeight( newTrack, 1.0f );
		m_animationController->SetTrackPosition( newTrack, 0.0f );
		m_animationController->KeyTrackEnable( newTrack, false, m_currentTime + as->GetPeriod() );
	}
...

Мда, рассмотрение функции PlayAnimation уже близится к концу. Так что держись!))
В случае, когда в последнем параметре функции PlayAnimation стоит значение FALSE, нам необходимо подредактировать трек с новой анимацией таким образом, чтобы он был воспроизведён только 1 раз. Вместо того, чтобы плавно переходить к новому треку, мы просто остановим текущий трек и незамедлительно начнём воспроизведение другого. Это делается путём последовательного вызова функций SetTrackEnable и SetTrackWeight на текущем и новом треках. Для текущего трека в функции SetTrackEnable во втором параметре указываем FALSE, а в функции SetTrackWeight - 0.0f. Для нового трека в функции SetTrackEnable во втором параметре указываем TRUE, а в функции SetTrackWeight - 1.0f.
Следующий шаг - вернуть позицию трека на начальную позицию. Никто не даст гарантию, что в силу определённых обстоятельств, новая анимация будет воспроизведена с самого начала. Поэтому мы сделаем это принудительно, чтобы увидеть нужную анимацию от начала и до конца. В зацикленной анимации это, в принципе, не проблема. В незацикленной анимации нам необходимо добавить на трек событие (ключ), автоматически отключающее (disabled) данный трек по его окончании. Делается это точно таким же способом, что и для зацикленной анимации. Единственная разница, что здесь мы будем вызывать на анимации функцию GetPeriod для определения, как долго она длится. Возвращённое данной функцией значение скажет нам, сколько времени необходимо ждать, прежде чем отключить трек.
В самом конце функции PlayAnimation мы освобождаем (release) анимацию, так как она больше не нужна:

Фрагмент AnimatedObject.cpp (Проект Engine)
...

	// Очищаем указатель на набор анимаций.
	as->Release();

	// Новый трек становится текущим треком.
	m_currentTrack = newTrack;
}
...

Последняя команда делает так, чтобы при следующем вызове функции PlayAnimation треки вновь сменились.

Объект-спавнер (Spawner objects)

Спавн (спаун, spawn - от англ. "появление на свет") - игроманский термин, означающий появление объекта на игровой сцене. В двух словах, объект-спавнер предназначен для спавна (здесь буквально "выбрасывания") на игровую сцену других объектов. Например ты захочешь разместить где-нить на игровой сцене подбираемое оружие (weapon pickup). Всё, что для этого нужно сделать - это разместить на этом месте объект-спавнер и указать, какой объект здесь должен появиться. Спавн-объект затем "выбросит" выбранное оружие в данном месте. Когда игрок подбирает выброшенные таким образом объекты, объект-спавнер затем снова "выбросит" их на тех же местах, спустя определённый промежуток времени. Всё это мы видели ещё в первом Quake. Ты лучше поймёшь принцип работы объекта-спавнера, когда мы разберём реализацию класса SpawnerObject. Но сперва, как всегда, начнём с создания соответствующих файлов исходного кода.

Создаём SpawnerObject.h (Проект Engine)

ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "SpawnerObject.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле SpawnerObject.h набираем следующий код:

SpawnerObject.h (Проект Engine)
//-----------------------------------------------------------------------------
// Данный класс ответвлён от класса SceneObject для поддержки объектов-спавнеров. 
// Спавнер - объект, "выбрасывающий" на игровую сцену другие объекты.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef SPAWNER_OBJECT_H
#define SPAWNER_OBJECT_H

//-----------------------------------------------------------------------------
// Определение типа Spawner Object
//-----------------------------------------------------------------------------
#define TYPE_SPAWNER_OBJECT 2

//-----------------------------------------------------------------------------
// Spawner Object Class
//-----------------------------------------------------------------------------
class SpawnerObject : public SceneObject
{
public:
	SpawnerObject( char *name, char *path = "./", unsigned long type = TYPE_SPAWNER_OBJECT );
	virtual ~SpawnerObject();

	virtual void Update( float elapsed, bool addVelocity = true );

	virtual void CollisionOccurred( SceneObject *object, unsigned long collisionStamp );

	Script *GetObjectScript();

private:
	char *m_name; // Имя объекта, который будет выброшен данным спавнером.
	float m_frequency; // Как часто спавнер будет выбрасывать объекты (в секундах).
	float m_spawnTimer; // Таймер, используемый для выбрасывания объектов на игровую сцену.
	Sound *m_sound; // Звук, воспроизводимый при подбирании объекта игроком.
	AudioPath3D *m_audioPath; // Аудиопуть, на котором данный звук будет воспроизведён.
	Script *m_objectScript; // Скрипт объекта-спавнера.
};

#endif

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код SpawnerObject.h

В самом начале мы определили новый тип объекта (TYPE_SPAWNER_OBJECT), который по умолчанию передаётся в конструкторе класса SpawnerObject. Мы также переопределили функции Update и CollisionOccured класса SceneObject, которые мы вскоре рассмотрим более подробно.
Ты, должно быть, также заметил, что объекты-спавнеры управляются скриптами. Это означает, что мы будем создавать скрипты для управления нашими спавнер-объектами. Все параметры, включая тип "выбрасываемого" на сцену объекта, периодичности, с которой он выбрасывается, звук воспроизводимый при подборе игроком выброшенного объекта, - будет контролироваться скриптом. Определение класса SpawnerObject совсем невелико, поэтому сразу перейдём к его реализации.

Создаём SpawnerObject.cpp (Проект Engine)

В файле исходного кода SpawnerObject.cpp будут размещаться реализации функций, объявленных в SpawnerObject.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "SpawnerObject.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле SpawnerObject.cpp набираем следующий код:

SpawnerObject.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// Реализация классов, объявленных в SpawnerObject.h.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// The spawner object class constructor.
//-----------------------------------------------------------------------------
SpawnerObject::SpawnerObject( char *name, char *path, unsigned long type ) : SceneObject( type )
{
	// Устновить объект-спавнер как призрак (ghost).
	SetGhost( true );

	// Загружаем скрипт объекта-спавнера.
	Script *script = new Script( name, path );

	// Извлекаем частоту обновления объекта-спавнера.
	m_frequency = *script->GetFloatData( "frequency" );

	// Очищаем таймер объекта-спавнера.
	m_spawnTimer = 0.0f;

	// Загружаем звук, воспроизводимый при подборе игроком спавн-объекта.
	if( script->GetStringData( "sound" ) != NULL )
	{
		m_sound = new Sound( script->GetStringData( "sound" ) );
		m_audioPath = new AudioPath3D;
	}
	else
	{
		m_sound = NULL;
		m_audioPath = NULL;
	}

	// Загружаем скрипт объекта-спавнера.
	m_objectScript = g_engine->GetScriptManager()->Add( script->GetStringData( "object" ), script->GetStringData( "object_path" ) );

	// Получаем имя объекта-спавнера.
	m_name = new char[strlen( m_objectScript->GetStringData( "name" ) ) + 1];
	strcpy( m_name, m_objectScript->GetStringData( "name" ) );

	// Назначаем спавнер-объекту меш "выбрасываемого" объекта.
	SetMesh( m_objectScript->GetStringData( "mesh" ), m_objectScript->GetStringData( "mesh_path" ) );

	// Придаём объекту медленное вращение.
	SetSpin( 0.0f, 1.0f, 0.0f );

	// Получаем радиус спавнер-объекта. Радиус 0.0 означает, что радиус
	// должен быть получен от "выбрасываемого" объекта.
	if( *script->GetFloatData( "radius" ) != 0.0f )
		SetBoundingSphere( D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), *script->GetFloatData( "radius" ) );
	else if( GetMesh() != NULL )
		SetEllipsoidRadius( *m_objectScript->GetVectorData( "ellipse_radius" ) );

	// Уничтожаем скрипт объекта-спавнера.
	SAFE_DELETE( script );
}

//-----------------------------------------------------------------------------
// The spawner object class destructor.
//-----------------------------------------------------------------------------
SpawnerObject::~SpawnerObject()
{
	// Уничтожаем строковой буфер (string buffer) содержащий имя объекта-спавнера.
	SAFE_DELETE_ARRAY( m_name );

	// Уничтожаем звук подбираемого спавн-объекта и его аудиопуть.
	SAFE_DELETE( m_sound );
	SAFE_DELETE( m_audioPath );

	// Уничтожаем скрипт объекта-спавнера.
	g_engine->GetScriptManager()->Remove( &m_objectScript );
}

//-----------------------------------------------------------------------------
// Обновляем объект-спавнер.
//-----------------------------------------------------------------------------
void SpawnerObject::Update( float elapsed, bool addVelocity )
{
	// Обновляем объект базового класса SceneObject.
	SceneObject::Update( elapsed, addVelocity );

	// Проверяем, невидим ли объект-спавнер
	if( GetVisible() == false )
	{
		// Объект-спавнер снова станет видимым после того как истечёт
		// установленный промежуток времени (frequency).
		m_spawnTimer += elapsed;
		if( m_spawnTimer >= m_frequency )
		{
			SetVisible( true );
			SetIgnoreCollisions( false );
			m_spawnTimer = 0.0f;
		}
	}

	// Обновляем аудиопуть звука подбора.
	if( m_audioPath != NULL )
	{
		m_audioPath->SetPosition( GetTranslation() );
		m_audioPath->SetVelocity( GetVelocity() );
	}
}

//-----------------------------------------------------------------------------
// Вызывается, когда что-либо столкнулось со спавнер-объектом.
//-----------------------------------------------------------------------------
void SpawnerObject::CollisionOccurred( SceneObject *object, unsigned long collisionStamp )
{
	// Разрешаем базовому объекту SceneObject зарегистрировать столкновение.
	SceneObject::CollisionOccurred( object, collisionStamp );

	// Предотвращаем возможный подбор спавнер-объекта каким-либо другим
	// объектом движка. Он может быть подобран только объектами, определёнными пользователем (user defined objects).
	if( object->GetType() == TYPE_SCENE_OBJECT || object->GetType() == TYPE_ANIMATED_OBJECT || object->GetType() == TYPE_SPAWNER_OBJECT )
		return;

	// Делаем спавнер-объект невидимым. Это предотвратит столкновение с ним других объектов
	// до тех пор, пока он не выбросит свой объект (т.е. снова не станет видимым).
	SetVisible( false );
	SetIgnoreCollisions( true );

	// Воспроизводим звук подбора.
	if( m_audioPath != NULL && m_sound != NULL )
		m_audioPath->Play( m_sound->GetSegment() );
}

//-----------------------------------------------------------------------------
// Возвращает скрипт выбрасываемого объекта.
//-----------------------------------------------------------------------------
Script *SpawnerObject::GetObjectScript()
{
	return m_objectScript;
}

  • Сохрани Решение (Файл->Сохранить все).

Исследуем код SpawnerObject.cpp

Конструктор класса SpawnerObject может показаться сложным и запутанным. Но всё, чем он занимается, это подготавливает объект-спавнер, исходя из настроек по умолчанию и настроек скрипта, назначенного ему. В нашем случае объект-спавнер появляется в качестве "призрака", что означает, что он будет регистрировать столкновения с ним, но физически не будет блокировать дальнейшее передвижение сталкиваемого с ним объекта, т.е. объект Игрок сможет свободно проходить сквозь него.
Для того, чтобы загрузить спавнер-объект с настройками его скрипта, в начале этот самый скрипт необходимо загрузить, основываясь на его (скрипта) имени и полном пути до него, передаваемые в конструктор. После этого мы получаем доступ к различным данным скрипта, необходимым для подготовки к работе спавнер-объекта. Например к его частоте обновления (т.е. временной "задержки" перед повторным появлением) или звуку, проигрываемому при подборе игроком спавн-объекта. В исходном коде видно, что при наличии "звука подбора" мы загружаем его и создаём 3D-аудиопуть для воспроизведения звуков на нём:

Фагмент SpawnerObject.cpp (Проект Engine)
...
	// Загружаем звук, воспроизводимый при подборе игроком спавн-объекта.
	if( script->GetStringData( "sound" ) != NULL )
	{
		m_sound = new Sound( script->GetStringData( "sound" ) );
		m_audioPath = new AudioPath3D;
	}
	else
	{
		m_sound = NULL;
		m_audioPath = NULL;
	}
...

Таким образом мы позиционируем звук в 3D-пространстве и другие игроки даже могут слышать "звуки подбора" (collection sounds) других игроков.
Следующий шаг в конструкторе - загрузка 3D-меша объекта, который будет спауниться (патроны, аптечка и т.д.). Данный меш будет использоваться объектом-спавнером для показа игроком объекта, который будет спауниться. Сперва мы загружаем скрипт объекта, который будет спауниться (который, в свою очередь, определён скриптом объект-спавнера), затем мы назначаем в качестве меша объекта-спавнера меш объекта, который будет спауниться:

Фагмент SpawnerObject.cpp (Проект Engine)
...
	// Загружаем скрипт объекта-спавнера.
	m_objectScript = g_engine->GetScriptManager()->Add( script->GetStringData( "object" ), script->GetStringData( "object_path" ) );

	// Получаем имя объекта-спавнера.
	m_name = new char[strlen( m_objectScript->GetStringData( "name" ) ) + 1];
	strcpy( m_name, m_objectScript->GetStringData( "name" ) );

	// Назначаем спавнер-объекту меш "выбрасываемого" объекта.
	SetMesh( m_objectScript->GetStringData( "mesh" ), m_objectScript->GetStringData( "mesh_path" ) );
...

Далее просто придаём спавн-объекту медленное вращение вокруг его оси Y:

Фагмент SpawnerObject.cpp (Проект Engine)
...
	// Придаём объекту медленное вращение.
	SetSpin( 0.0f, 1.0f, 0.0f );
...

Наконец мы просто задаём спавнер-объекту ограничивающие объёмы, взятые из скрипта меша. Они применяются для определения столкновения объекта игрока со спавнер-объектом и срабатывания "подбора" спавн-объекта игроком.
Деструктор класса SpawnerObject как всегда уничтожает всё, что мы загрузили в конструкторе и освобождает память. Там всё ясно из исходного кода.
Функция Update

  • является переопределением функции Update класса SceneObject.

Вот её реализация:

Фагмент SpawnerObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Обновляем объект-спавнер.
//-----------------------------------------------------------------------------
void SpawnerObject::Update( float elapsed, bool addVelocity )
{
	// Обновляем объект базового класса SceneObject.
	SceneObject::Update( elapsed, addVelocity );

	// Проверяем, невидим ли объект-спавнер
	if( GetVisible() == false )
	{
		// Объект-спавнер снова станет видимым после того как истечёт
		// установленный промежуток времени (frequency).
		m_spawnTimer += elapsed;
		if( m_spawnTimer >= m_frequency )
		{
			SetVisible( true );
			SetIgnoreCollisions( false );
			m_spawnTimer = 0.0f;
		}
	}

	// Обновляем аудиопуть звука подбора.
	if( m_audioPath != NULL )
	{
		m_audioPath->SetPosition( GetTranslation() );
		m_audioPath->SetVelocity( GetVelocity() );
	}
}
...

Как и любой другой анимированный объект, даже несмотря на то, что по сути спавнер-объект является объектом сцены, он должен быть обновлён. Для того, чтобы убедиться в том, что наш спавнер-объект ведёт себя именно как объект сцены, мы сперва вызываем функцию Update базового класса SceneObject. Только после завершения её работы мы вызываем функцию Update дочернего класса SpawnerObject.
Первым делом проверяем, видим ли объект. Если объект не видим, значит он подобран игроком. Когда игрок сталкивается (collide) с объектом-спавнером, мы устанавливаем флаг видимости в FALSE, после чего движок перестаёт его рендерить. Спустя некоторое время он снова появится на том же месте. При респауне (повторном появлении) флаг видимости вновь выставляется в TRUE и спаун-объект становится видим. Таким образом, всякий раз, когда флаг видимости спавнер-объекта установлен в FALSE, мы знаем, что он ждёт респауна и надо просто подсчитать, спустя какое время он должен произойти.
Когда объект подбора объекта-спавнера подобран, он появится вновь, спустя определённое количество времени, указанное в переменной m_frequenсy. Для данной проверки мы используем простой таймер, который обновляет переменную m_spawnTimer, принимая во внимание истекшее время. Когда переменная m_spawnTimer достигнет значения m_frequenсy, то это будет значить, что время отсрочки подошло к концу и можно респаунить объект. Это также повлечёт за собой восстановление видимости объекта и обнуление таймера.
Последний шаг в функции Update - обновление 3D-аудиопути спавнер-объекта. Для этого достаточно проверить, что текущие положение и скорость аудиопути совпадают с оными спавнер-объекта. Обычно спавнер-объект размещается на карте и никогда не двигается. Но, обновляя его подобным образом мы сохраняем возможность перемещать его в 3D-пространстве, что называется "на лету". Это делается на всякий случай. Например, когда творческий замысел требует, чтобы спавнер-объект был размещён в кузове движущегося грузовика.

Функция CollisionOccured

  • Экспонирована классом SceneObject.

До этого мы не видели её в деле, но тем не менее переопределили в классе SpawnerObject для того, чтобы дать ей реализацию, специфичную для спавнер-объекта:

Фагмент SpawnerObject.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Вызывается, когда что-либо столкнулось со спавнер-объектом.
//-----------------------------------------------------------------------------
void SpawnerObject::CollisionOccurred( SceneObject *object, unsigned long collisionStamp )
{
	// Разрешаем базовому объекту SceneObject зарегистрировать столкновение.
	SceneObject::CollisionOccurred( object, collisionStamp );

	// Предотвращаем возможный подбор спавнер-объекта каким-либо другим
	// объектом движка. Он может быть подобран только объектами, определёнными пользователем (user defined objects).
	if( object->GetType() == TYPE_SCENE_OBJECT || object->GetType() == TYPE_ANIMATED_OBJECT || object->GetType() == TYPE_SPAWNER_OBJECT )
		return;

	// Делаем спавнер-объект невидимым. Это предотвратит столкновение с ним других объектов
	// до тех пор, пока он не выбросит свой объект (т.е. снова не станет видимым).
	SetVisible( false );
	SetIgnoreCollisions( true );

	// Воспроизводим звук подбора.
	if( m_audioPath != NULL && m_sound != NULL )
		m_audioPath->Play( m_sound->GetSegment() );
}
...

В первую очередь вызывается функция CollisionOccurred базового класса SceneObject для регистрации штампа столкновения (collisionStamp). Затем мы просто проверяем, объект какого тип столкнулся с нашим спавнер-объектом. Так как мы знаем, что ни один из созданных ранее объектов не обладает способностью подбирать объекты из спавнер-объектов, то мы можем безопасно завершить работу функции при столкновении спавнер-объекта с любым из них. Мы определяем тип объекта, столкнувшегося со спавнер-объектом, путём простой проверки типа объекта. Если объект, который столкнулся со спавнер-объектом не относится ни к одному из ранее созданных стандартных типов объектов, то значит тип этого объекта является пользовательским (user-defined), например объект игрока (PLAYER_OBJECT). Исходя из этого, сделаем предположение, что объекты пользовательских типов могут подбирать объекты, выбрасываемые спавнер-объектами.

Закрыть
noteОбрати внимание

Не забывай, что любой объект одного из пользовательских типов обладает способностью задействовать функцию столкновения при контакте со спавнер-объектом. Это будет проблемой в нашей игре, где единственным пользовательским типом объектов, который может двигаться и контактировать со спавнер-объектами, будет объект игрока (PLAYER_OBJECT). Тем не менее позднее ты можешь решить добавить в игру другие пользовательские типы объектов, которые по замыслу не должны задействовать "подбор" объектов спавнер-объектов. Для решения этой задачи можно создать список (list) пользовательских объектов, столкновения с которыми будут игнорироваться. Одним из них и будет спавнер-объект.

Как только мы определили, что столкнулись с валидным объектом, переходим непосредственно к "подбору" объекта, выброшенному спавнер-объектом. Здесь мы первым делом устанавливаем флаг видимости спавнер-объекта в FALSE, а флаг игнорирования столкновений (ignore collisions) в TRUE. Это предотвратит спавнер-объект от регистрации последующих столкновений при ожидании респауна объекта подбора. Далее воспроизводим звук подбора (collection sound), чтобы таким образом игрок получил аудиооповещение о том, что он что-то подобрал.

Закрыть
noteОбрати внимание

Мы начали с обсуждения результатов столкновения объектов, но ни словом не обмолвились как именно два объекта сталкиваются друг с другом. На самом деле здесь всё просто. Мы знаем, что два объекта сталкиваются, когда их ограничивающие объёмы касаются друг друга. Для проверки этого достаточно проверить, вступают ли в контакт два ограничивающих бокса или сферы. Вообще у нас для этого уже есть готовый код, размещённый в Geometry.h (см. Глава 1.5). Тем не менее, помимо проверки на контакт ограничивающих объёмов, также необходимо провести другие проверки, о которых мы поговорим в одной из ближайших Глав.

Интеграция игровых объектов в движок

Принцип тот же, что и при интегрировании других систем.

Изменения в SceneObject.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла SceneObject.cpp (проверь её наличие).

Изменения в AnimatedObject.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла AnimatedObject.cpp (проверь её наличие).

Изменения в SpawnerObject.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла SpawnerObject.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь строки

#include "SceneObject.h" #include "AnimatedObject.h" #include "SpawnerObject.h"
сразу после строки #include "Mesh.h":

Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Font.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "Network.h"
#include "SoundSystem.h"
#include "BoundingVolume.h"
#include "Material.h"
#include "Mesh.h"
#include "SceneObject.h"
#include "AnimatedObject.h"
#include "SpawnerObject.h"
#include "RenderCache.h"
#include "State.h"
...

  • Добавь новый член char *spawnerPath; в структуру EngineSetup

сразу после строки void (*CreateMaterialResource)( Material **resource, char *name, char *path );:

Фрагмент Engine.h (Проект Engine)
...
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	GUID guid; // Application GUID.
	char *name; // Название приложения.
	float scale; // Масштаб (scale) в метрах/футах.
	unsigned char totalBackBuffers; // Число используемых бэкбуферов.
	void (*HandleNetworkMessage)( ReceivedMessage *msg ); // Обработчик сетевых сообщений.
	void (*StateSetup)(); // Функция подготовки стейта.
	void (*CreateMaterialResource)( Material **resource, char *name, char *path ); // Функция создания материала.
	char *spawnerPath; // Путь для поиска скриптов спавнер-объекта.
...

Он представляет собой путь, по которому движок будет искать все скрипты спавнер-объекта. Мы можем, например, сохранять все наши скрипты спавнер-объектов в папке Assets и затем просто указать движку искать их именно там. Затем мы ссылаемся на скрипт спавнер-объекта по его имени, зная что движок дополнит это самое имя корректным путём (path) до файла скрипта. Основной причиной того, что мы поступаем именно так, является то обстоятельство, что спавнер-объекты - объекты, специфичные для приложения. Движку абсолютно неведомо, что за спавнер-объекты мы создадим и где мы их будем хранить. Поэтому нам необходимо "объяснить" движку о их существовании заранее, чтобы они затем могли быть загружены им по требованию (on demand), без дополнительных команд от пользователя. Чуть позднее мы рассмотрим эту тему более детально.

Изменения в State.h (Проект Engine)

  • Добавь новый член SceneObject *viewer;

в структуру ViewerSetup:

Фрагмент State.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Viewer Setup Structure (структура установок вирт. просмотрщика)
//-----------------------------------------------------------------------------
struct ViewerSetup
{
			SceneObject *viewer; // Текущий объект, являющийся вьюером (viewing object).
			unsigned long viewClearFlags; // Флаги, применяемые для очищения вьюера.

	//-------------------------------------------------------------------------
	// The viewer setup structure constructor.
	//-------------------------------------------------------------------------
	ViewerSetup()
	{
		viewClearFlags = 0;
	};
};
...

Он используется в каждом кадре для информирования движка о том, какой из объектов используется для просмотра игровой сцены. Когда движок вызывает твою реализацию функции RequestViewer (от нашего ответвлённого класса State), мы можем указать, какой из объектов в данный момент использовать для просмотра игровой сцены (обычно это объект игрока).

Изменения в Engine.cpp (Проект Engine)

  • Добавь строки

if( viewer.viewer != NULL )
{
// Устанавливаем трансформацию вида (view transformation).
m_device->SetTransform( D3DTS_VIEW, viewer.viewer->GetViewMatrix() );

// Обновляем объект 3D-слушателя (3D sound listener).
m_soundSystem->UpdateListener( viewer.viewer->GetForwardVector(), viewer.viewer->GetTranslation(), viewer.viewer->GetVelocity() );
}
в реализацию функции Run класса Engine, сразу после строки запроса вьюера if( m_currentState != NULL ) m_currentState->RequestViewer( &viewer );:

Фрагмент Engine.cpp (Проект Engine)
...
				// Запрос вьюера текущего стейта (если таковой имеется).
				if( m_currentState != NULL )
					m_currentState->RequestViewer( &viewer );

				// Проверяем, валиден ли вьюер.
				if( viewer.viewer != NULL )
				{
					// Устанавливаем трансформацию вида (view transformation).
					m_device->SetTransform( D3DTS_VIEW, viewer.viewer->GetViewMatrix() );

					// Обновляем объект 3D-слушателя (3D sound listener).
					m_soundSystem->UpdateListener( viewer.viewer->GetForwardVector(), viewer.viewer->GetTranslation(), viewer.viewer->GetVelocity() );
				}
...

При наличии валидного вьюера нам необходимо выполнить два шага. Сперва мы устанавливаем матрицу трансформации вида (view transformation matrix) для данного вьюера, чтобы Direct3D мог отображать сцену "из глаз" объекта просмотра. Дальше обновляем объект 3D-слушателя (3D sound listener) путём вызова функции UpdateListener на нашей звуковой системе движка. В функцию передаётся несколько уточняющих параметров об объекте слушателя (который одновременно является объектом просмотра сцены). Таким образом мы размещаем виртуальный микрофон как раз в том месте 3D-пространства, где расположен объект просмотра сцены. Мы используем вектор объекта, направленный вперёд (object's forward vector), чтобы микрофон был ориентирован в нужном направлении.

Тестовая перекомпиляция Engine.lib

Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

Image
По окончании компиляции (обычно, при создании небольших проектов, она занимает несколько секунд) в панели "Вывод" (в нижней части главного окна IDE) будет представлен отчёт (лог) об успешной (либо неуспешной) компиляции.
В нашем случае компиляция прошла успешно.

Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).

Модифицируем тестовое приложение (Проект Test)

Наконец, после изучения столь сложной Главы, наши старания будут вознаграждены наиболее впечатляющим визуальным примером. Впервые с начала данного курса мы воспроизведём на экране нечто более красивое, чем обычный текст.
В Главе 1.6(external link) для проверки работоспособности движка в нашем Решении GameProject01 мы создали второй Проект Test, после компиляции которого получили исполняемое приложение (файл .EXE), показывающее окно. В последующих главах его функционал изменялся. Применив полученные в текущей Главе знания на практике, снова дополним исходный код тестового приложения, оснастив его новым "функционалом". Если по каким-то причинам Проект Test отсутствует в Решении GameProject01, создай его, следуя инструкциям Главы 1.6(external link) .
ОК, приступаем.

  • Открой для редактирования файл исходного кода Main.cpp (Проект Test) и замени содержащийся в нём код на следующий:
Main.cpp (Проект Test)
//-----------------------------------------------------------------------------
// System Includes
//-----------------------------------------------------------------------------
#include <windows.h>

//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "..\GameProject01\Engine.h"

//-----------------------------------------------------------------------------
// Test State Class
//-----------------------------------------------------------------------------
class TestState : public State
{
public:
	//-------------------------------------------------------------------------
	// Allows the test state to preform any pre-processing construction.
	//-------------------------------------------------------------------------
	virtual void Load()
	{
		// Создаём анимирвоанный объект, и воспроизводим одну из его анимаций.
		m_character = new AnimatedObject( "Marine.x", "./Assets/" );
		m_character->SetTranslation( -100.0f, 0.0f, 0.0f );
		m_character->SetRotation( 0.0f, 3.14f, 0.0f );
		m_character->PlayAnimation( 3, 0.0f );

		// Создаём объект сцены, который будет использоваться в качестве камеры.
		m_viewer = new SceneObject;
		m_viewer->SetTranslation( 0.0f, 0.0f, -400.0f );
		m_viewer->SetFriction( 5.0f );

		// Создаём тестовый спавнер-объект.
		m_spawner = new SpawnerObject( "Spawner.txt", "./Assets/" );
		m_spawner->SetTranslation( 100.0f, -50.0f, 0.0f );
	};

	//-------------------------------------------------------------------------
	// Allows the test state to preform any post-processing destruction.
	//-------------------------------------------------------------------------
	virtual void Close()
	{
		SAFE_DELETE( m_character );
		SAFE_DELETE( m_viewer );
		SAFE_DELETE( m_spawner );
	};

	//-------------------------------------------------------------------------
	// Возвращает установки просмотра для текущего кадра.
	//-------------------------------------------------------------------------
	virtual void RequestViewer( ViewerSetup *viewer )
	{
		viewer->viewer = m_viewer;
		viewer->viewClearFlags = D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER;
	}

	//-------------------------------------------------------------------------
	// Обновляем test state.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Разрешаем перемещать камеру (scene object) по нажатию кнопок на клавиатуре.
		if( g_engine->GetInput()->GetKeyPress( DIK_W, true ) == true )
			m_viewer->Drive( 1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_S, true ) == true )
			m_viewer->Drive( -1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_A, true ) == true )
			m_viewer->Strafe( -1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_D, true ) == true )
			m_viewer->Strafe( 1000.0f * elapsed, false );

		// Регулируем вращение камеры, основываясь на перемещении мыши.
		m_viewer->AddRotation( (float)g_engine->GetInput()->GetDeltaY() * elapsed, (float)g_engine->GetInput()->GetDeltaX() * elapsed, 0.0f );

		m_character->Update( elapsed );
		m_viewer->Update( elapsed );
		m_spawner->Update( elapsed );
	};

	//-------------------------------------------------------------------------
	// Рендерим test state.
	//-------------------------------------------------------------------------
	virtual void Render()
	{
		m_character->Render();
		m_spawner->Render();
	};

private:
	AnimatedObject *m_character; // Анимированный объект персонажа.
	SceneObject *m_viewer; // Объект просмотра сцены выполняет роль камеры.
	SpawnerObject *m_spawner; // Тестовый спавнер-объект.
};

//-----------------------------------------------------------------------------
// Установки стейта, специфичные для приложения.
//-----------------------------------------------------------------------------
void StateSetup()
{
	g_engine->AddState( new TestState, true );
}

//-----------------------------------------------------------------------------
// Входная точка в приложение.
//-----------------------------------------------------------------------------
int WINAPI WinMain( HINSTANCE instance, HINSTANCE prev, LPSTR cmdLine, int cmdShow )
{
	// Создаём экземпляр структуры EngineSetup.
	EngineSetup setup;
	setup.instance = instance;
	setup.name = "Object Test";
	setup.scale = 0.01f;
	setup.StateSetup = StateSetup;

	// Создаём экземпляр движка (используя структуру EngineSetuo), и стартуем его.
	new Engine( &setup );
	g_engine->Run();

	return true;
}


И вновь в нашем Проекте Test всего 1 файл Main.cpp, который содержит весь исходный код тестового приложения. Перед нами готовая система просмотра объектов трёх различных видов (в виде стейта) в действии.

Подготовка Проекта Test к перекомпиляции

Напомним, что для успешной компиляции библиотека Engine должна быть добавлена в Проект Test (см. Главу 1.6).
Компилируем Проект Test:

  • В Обозревателе решений щёлкни правой кнопкой мыши по названию Проекта Test.
  • Во всплывающем контекстном меню выбираем: Перестроить.

Компиляция завершилась успешно. Итоговый исполняемый двоичный файл (Test.exe) расположен на жёстком диске ПК в той же директории, что и библиотека движка (Engine.lib).
В нашем случае, это: С:\Users\<Имя пользователя>\documents\visual studio 2010\Projects\GameProject01\Debug. (В разных ОС путь к файлу может отличаться от представленного).
Полученная программа сразу после запуска будет автоматически искать файлы в подкаталоге Assets:

  • файл с мешем Marine.x (файл меша с фигуркой пехотинца);
  • файл текстуры пехотинца Maine.dds;
  • Файл скрипта текстуры пехотинца Marine.dds.txt;
  • Файл скрипта спавн-объекта Spawner.txt, который в свою очередь указывает на файл меша Gun.x, размещённый в подкаталоге Gun.

Если хотя бы одного файла там не окажется, приложение аварийно завершит свою работу, выдав сообщение об ошибке. Это нормально, т.к. проверку на присутствие запрашиваемого файла в указанном каталоге никто не писал.
Поэтому архив со всеми необходимыми файлами забираем здесь(external link). Напомним, что файлы ресурсов должны быть расположены в подкаталоге Assets каталога содержащего исполняемый файл скомпилированного приложения Test.exe (в нашем случае это c:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\).
*Найди и запусти получившееся приложение.
Приложение стартует долго, прибл. 15-20 секунд. Всё это время на экране вообще ничего может не меняться. Но затем в открывшемся окне появятся модель пехотинца и автомата (выброшенный объект спавнер-объекта, вращающийся на одном месте) на чёрном фоне. Нажимая клавиши w, a, s, d можно изменять положение виртуальной камеры. Клавиша выхода из приложения в исходном коде отсутствует. поэтому закрываем окно программы стандартной комбинацией клавиш Alt+F4.

Закрыть
noteОбрати внимание

Открой файл скрипта спавн-объекта Spawner.txt (например в Блокноте Windows) и рассмотри его содержимое. В скрипте определены:

  • Частота (периодичность) выброса объекта подбора (т.е. задержка между подбором объекта и его следующим выбросом).
  • Радиус детектирования столкновений (collision radius). Значение 0.0 означает, что для определения радиуса столкновения будет использоваться радиус меша спаун-объекта;
  • Имя и полный путь до объекта, который будет выброшен.

Также в него можно добавить звук подбора (в данном примере не показано).

Исследуем код тестового приложения (проект Test)

Для данного тестового приложения были подготовлены 3 объекта, по одному каждого вида.
В функции Load класса TestState хорошо видно, как создаётся каждый из этих трёх объектов.
Первым идёт анимированный объект (AnimatedObject), который использует 3D-меш со встроенной анимацией:

Фрагмент Main.cpp (Проект Test)
...
	virtual void Load()
	{
		// Создаём анимирвоанный объект, и воспроизводим одну из его анимаций.
		m_character = new AnimatedObject( "Marine.x", "./Assets/" );
		m_character->SetTranslation( -100.0f, 0.0f, 0.0f );
		m_character->SetRotation( 0.0f, 3.14f, 0.0f );
		m_character->PlayAnimation( 3, 0.0f );
...

Второй объект - объект сцены (SceneObject), который мы используем в качестве виртуальной камеры:

Фрагмент Main.cpp (Проект Test)
...
		// Создаём объект сцены, который будет использоваться в качестве камеры.
		m_viewer = new SceneObject;
		m_viewer->SetTranslation( 0.0f, 0.0f, -400.0f );
		m_viewer->SetFriction( 5.0f );
...

Третий объект - спавнер-объект (SpawnerObject), который спаунит (выбрасывает) объект, описанный в его скрипте:

Фрагмент Main.cpp (Проект Test)
...
		// Создаём тестовый спавнер-объект.
		m_spawner = new SpawnerObject( "Spawner.txt", "./Assets/" );
		m_spawner->SetTranslation( 100.0f, -50.0f, 0.0f );
...
Рис.6 Тестовое приложение в работе
Рис.6 Тестовое приложение в работе

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

Фрагмент Main.cpp (Проект Test)
...
	//-------------------------------------------------------------------------
	// Возвращает установки просмотра для текущего кадра.
	//-------------------------------------------------------------------------
	virtual void RequestViewer( ViewerSetup *viewer )
	{
		viewer->viewer = m_viewer;
		viewer->viewClearFlags = D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER;
	}
...

В функции Update видно, как мы управляем камерой при помощи клавиш клавиатуры W, A, S. D:

Фрагмент Main.cpp (Проект Test)
...
	//-------------------------------------------------------------------------
	// Обновляем test state.
	//-------------------------------------------------------------------------
	virtual void Update( float elapsed )
	{
		// Разрешаем перемещать камеру (scene object) по нажатию кнопок на клавиатуре.
		if( g_engine->GetInput()->GetKeyPress( DIK_W, true ) == true )
			m_viewer->Drive( 1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_S, true ) == true )
			m_viewer->Drive( -1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_A, true ) == true )
			m_viewer->Strafe( -1000.0f * elapsed, false );
		if( g_engine->GetInput()->GetKeyPress( DIK_D, true ) == true )
			m_viewer->Strafe( 1000.0f * elapsed, false );

		// Регулируем вращение камеры, основываясь на перемещении мыши.
		m_viewer->AddRotation( (float)g_engine->GetInput()->GetDeltaY() * elapsed, (float)g_engine->GetInput()->GetDeltaX() * elapsed, 0.0f );

		m_character->Update( elapsed );
		m_viewer->Update( elapsed );
		m_spawner->Update( elapsed );
	};
...

Итоги главы

Ты можешь "поиграть" с различными значениями исходного кода тестового приложения. Например, можно сменить объект-вьюер на другой.
Это была впечатляющая Глава т.к. мы значительно повысили функциональность нашего движка путём добавления различных видов объектов. Мы подробно обсудили о том, как работают игровые объекты а затем разработали 3 стандартных вида объектов, поддерживаемые нашим движком:

  • базовый объект сцены (Scene object);
  • анимированный объект;
  • спавнер-объект.

Данные объекты "заточены" для создания именно игр в жанре FPS. Так что если ты попытаешься создать игру другого жанра на данном движке, ты обнаружишь, что его система объектов может потребовать модификации.
В конце Главы мы интегрировали классы объектов в движок и разработали замечательное тестовое приложение, которое показывает в работе каждый из трёх видов объектов.
При разработке игры во 2-й части данного курса мы сможем выбрасывать на игровую сцену все виды объектов и они смогут корректно сталкиваться друг с другом, при условии, если они наследованы от базового объекта SceneObject.
Поэтому в следующей Главе мы расширим и углубим наши познания об объектах сцены. Она будет последней Главой первой части и ей мы завершим работу над движком. Но прежде чем увидеть свет в конце тоннеля нам ещё предстоит проделать большую работу. Мы оставили самые лучшие и сложные темы, что называется, на десерт. В конце следующей Главы у нас будет всё необходимое для управления и рендеринга всй игровой сцены со всеми объектами на ней.


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.15 Объекты в игре

Contributors to this page: slymentat .
Последнее изменение страницы Вторник 28 / Ноябрь, 2017 20:16:06 MSK автор slymentat.

Последние комментарии

No records to display