Разработка приложений с использованием пакета Visual Studio 3D Starter Kit. Часть 2 из 3

Не так давно мы приступили к разработке простого приложения для игры в кости с помощью Visual Studio 3D Starter Kit. Теперь мы доработаем приложение, добавив анимацию. Ссылка на предыдущую статью.

Как заставить кубик крутиться

Чтобы заставить любой объект в графическом приложении двигаться, необходимо:

  1. Определить какое-либо состояние (например, с помощью логического флага), чтобы указать, что должна быть запущена анимация. Также на этом этапе можно сохранить сведения об исходном положении и продолжительности анимации, если это необходимо.
  2. В методе Update, который вызывается для каждого кадра перед рендерингом, обрабатывается положение/вращение/масштабирование объекта (с точки зрения 3D-модели, мы говорим о преобразованиях); следует вычислить разницу во времени между началом анимации и выводом на экран текущего кадра. Когда анимация завершается, необходимо надлежащим образом обновить состояние, чтобы остановить анимацию.
  3. В методе Render преобразования должны применяться к каждому объекту надлежащим образом.

Мы добавим новый метод RollDie(), который установит состояние и сохранит время запуска. Этот метод будет использоваться для вычисления результата каждого броска кубика, но пока мы просто запустим анимацию — вращение кубика и показ цифр от 1 до 6.

Чтобы создать эту анимацию, нам понадобится набор переменных для отслеживания преобразований куба, а также времени анимации. Мы создадим одну переменную типа Boolean для отслеживания выполнения анимации, одну переменную типа Float для хранения времени анимации и три вектора для хранения начального, конечного и текущего вращений куба. Векторы типа XMFLOAT3 будут хранить вращения Yaw, Pitch и Roll (см. рисунок ниже).

Давайте создадим эти поля и метод RollDie(). Добавьте следующий код в класс Game.h:

ref class Game sealed : public GameBase { // (фрагмент) объявление других классов... Platform::String^ OnHitObject(int x, int y); void RollDie(); private: std::vector<VSD3DStarter::Mesh*> m_meshModels; bool m_isAnimationRunning; float m_animationTime; DirectX::XMFLOAT3 m_initialRotation; DirectX::XMFLOAT3 m_currentRotation; DirectX::XMFLOAT3 m_targetRotation; };

Давайте создадим анимацию, начнем с метода RollDie(). Добавьте следующий код в класс Game.cpp:

void Game::RollDie() { m_initialRotation = m_currentRotation; m_targetRotation = XMFLOAT3(0.0f, XM_PI, 0.0f); // always rotate to 6 m_animationTime = 0.0f; m_isAnimationRunning = true; }

RollDie() запустит анимацию путем задания значения True для переменной m_isAnimationRunning. Нам необходимо добавить код в метод Update() для поворота куба в каждом кадре. Добавьте следующий код в метод Update(): 

void Game::Update(float timeTotal, float timeDelta) { if (m_isAnimationRunning) { m_animationTime += timeDelta; static const float animationDuration = 0.5f; float animationProgress = std::min<float>(m_animationTime / animationDuration, 1.0f); XMVECTOR initial = XMLoadFloat3(&m_initialRotation); XMVECTOR target = XMLoadFloat3(&m_targetRotation); XMVECTOR current = initial + animationProgress * (target - initial); XMStoreFloat3(&m_currentRotation, current); if (animationProgress >= 1.0f) m_isAnimationRunning = false; } }

Описание кода:

  1. Анимация должна запускаться только в том случае, когда установлен флаг m_isAnimationRunning.
  2. Сначала вычисляется значение переменных calculate m_animationTime (время от начала анимации 0–0,5 секунды) и m_animationProgress (процент завершения анимации 0–1). Обратите внимание, «1.0» означает, что анимация завершена.
  3. Загрузите исходный и целевой векторы вращения в XMVECTORs, это позволяет ускорить расчеты благодаря макроопределениям CPU.
  4. Расчеты линейным способом по формуле:

текущий = исходный + прогресс * (целевой – исходный)

  1. Вычисленное значение сохраняется обратно в m_currentRotation.
  2. Проверка выполнения анимации и ее остановка в случае необходимости.

На следующем шаге анимации вычисленное вращение используется в методе Render для поворота куба. Для этого измените следующую строку кода в методе Render():

void Game::Render() { GameBase::Render(); // (фрагмент) очистить... XMMATRIX transform = XMMatrixRotationRollPitchYawFromVector(XMLoadFloat3(&m_currentRotation)); for (UINT i = 0; i < m_meshModels.size(); i++) { m_meshModels[i]->Render(m_graphics, transform); } // (фрагмент) MSAA... }

Метод, который мы вызываем, получает объект XMVECTOR, содержащий значения переменных для разворота, уклона и поворота относительно вертикальной оси. Этот метод возвращает матрицу вращения, которая соответствует комбинированному преобразованию. Для наших целей также подходит метод XMMatrixRotationRollPitchYaw, который обрабатывает параметры разворота, уклона и поворота относительно вертикальной оси по отдельности. Кроме того, вы можете вычислить каждый компонент с помощью отдельного метода, а затем самостоятельно перемножить результаты. Полный перечень методов, используемых для создания матриц преобразования, представлен на сайте MSDN.

Все, что нам необходимо сделать,— вызвать метод RollDie(). Мы будем вызывать его каждый раз, когда пользователь касается или щелкает по экрану. Добавьте следующий код в файл DirectXPage.xaml.cpp (в метод DirectXPage::OnTapped):

void DirectXPage::OnTapped(Platform::Object^ sender, TappedRoutedEventArgs^ e) { m_renderer->RollDie(); }

Если вы запустите приложение сейчас, то после щелчка или касания экрана в любом месте кубик будет вращаться, показывая цифры от 1 до 6!

Логика приложения: генератор случайных чисел

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

Сначала генератор rand() нужно инициализировать с помощью некоторого начального значения. Если мы не инициализируем генератор или инициализируем его с использованием фиксированного значения, то последовательность генерируемых чисел будет неизменна при каждом запуске приложения, а нам это не подходит. Мы будем инициализировать генератор случайных чисел с использованием текущего времени CPU. Добавьте следующий код в метод Game::Initialize():

void Game::Initialize() { Mesh::LoadFromFile(m_graphics, L"die.cmo", L"", L"", m_meshModels); srand ((unsigned int) time(NULL)); }

Также следует подключить библиотеку для метода Game.cpp, чтобы функция time() была доступна:

#include "pch.h" #include "Game.h" #include <DirectXMath.h> #include <DirectXColors.h> #include <algorithm> #include <time.h>

После инициализации мы можем воспользоваться функцией rand() в методе RollDie(), чтобы получить цифру, которая выпадет, когда пользователь бросит кости. Мы также должны правильно задать целевые вращения кубика при каждом броске. Для этого измените строку, в которой задается значение переменной m_targetRotation. Окончательный вариант метода RollDie():

void Game::RollDie() { m_initialRotation = m_currentRotation; int currentRoll = rand() % 6 + 1; switch (currentRoll) { case 1: m_targetRotation = XMFLOAT3(0.0f, 0.0f, 0.0f); break; case 2: m_targetRotation = XMFLOAT3(0.0f, XM_PIDIV2, 0.0f); break; case 3: m_targetRotation = XMFLOAT3(XM_PIDIV2, 0.0f, 0.0f); break; case 4: m_targetRotation = XMFLOAT3(-1.0f * XM_PIDIV2, 0.0f, 0.0f); break; case 5: m_targetRotation = XMFLOAT3(0.0f, -1.0f * XM_PIDIV2, 0.0f); break; case 6: m_targetRotation = XMFLOAT3(0.0f, XM_PI, 0.0f); break; } m_animationTime = 0.0f; m_isAnimationRunning = true; }

Запустите приложение. Вот наш анимированный кубик! Бросьте кости, коснувшись экрана в любом месте. Можете перейти в режим Snap View.

Как заставить кубик прыгать

Это небольшое приложение можно использовать и в таком виде, но мы можем сделать его еще интереснее. Сейчас мы покажем, как заставить брошенный кубик подпрыгивать, а также добавим дополнительные случайные вращения, чтобы сделать приложение более реалистичным.

Чтобы запрограммировать «прыжки», мы добавим еще одну переменную в нашу анимацию — вертикальную координату (Y). Значение этой переменной будет изменяться после того, как кости будут брошены (в методе Update()), мы также внесем изменения в метод Render(), чтобы применить трансляцию к кубу. Сначала нужно добавить переменную трансляции в Game.h:

ref class Game sealed : public GameBase { // (фрагмент) объявление других классов... private: std::vector<VSD3DStarter::Mesh*> m_meshModels; bool m_isAnimationRunning; float m_animationTime; DirectX::XMFLOAT3 m_initialRotation; DirectX::XMFLOAT3 m_currentRotation; DirectX::XMFLOAT3 m_targetRotation; float m_currentTranslationY; };

Чтобы переменная изменялась с течением времени (метод Update()):

void Game::Update(float timeTotal, float timeDelta) { if (m_isAnimationRunning) { // (фрагмент) XMVECTOR current = initial + animationProgress * (target - initial); XMStoreFloat3(&m_currentRotation, current); const float maxHeight = 2.0f; m_currentTranslationY = 4.0f * maxHeight * animationProgress * (1 - animationProgress); if (animationProgress >= 1.0f) m_isAnimationRunning = false; } }

Те, кто помнит курс математики старших классов, узнали уравнение параболы с полюсами в точках 0 и 1 и вершиной в точке 0,5 (MaxHeight). Наш кубик будет достаточно реалистично подпрыгивать. График этой функции можно увидеть здесь: WolframAlpha.

На заключительном этапе мы воспользуемся расчетной трансляцией для преобразования куба в процессе рендеринга. Добавьте следующий код в метод Render:

oid Game::Render() { // (фрагмент) очистить... m_d3dContext->ClearDepthStencilView( /* (фрагмент) */ ); XMMATRIX transform = XMMatrixRotationRollPitchYawFromVector(XMLoadFloat3(&m_currentRotation)); transform *= XMMatrixTranslation(0.0f, m_currentTranslationY, 0.0f); for (UINT i = 0; i < m_meshModels.size(); i++) { m_meshModels[i]->Render(m_graphics, transform); } // (фрагмент) MSAA... }

Запустите приложение и щелкните по экрану. Вы увидите, что кубик после броска реалистично подпрыгивает. Наконец, чтобы анимация выглядела еще более реалистичной, добавим случайные вращения по обеим осям при каждом броске кубика. Для этого внесем изменения в метод RollDie() и добавим случайные повороты в целевое вращение:

void Game::RollDie() { // (фрагмент) инициализация, бросок и переключение(currentRoll) XMVECTOR target = XMLoadFloat3(&m_targetRotation); XMVECTOR current = XMLoadFloat3(&m_currentRotation); // учет текущего вращения target += XMVectorFloor(current / XM_2PI) * XM_2PI; // -1, 0 или 1 дополнительный оборот XMVECTOR randomVector = XMLoadFloat3(&XMFLOAT3(rand() % 3 - 1.0f, rand() % 3 - 1.0f, rand() % 3 - 1.0f)); target += randomVector * XM_2PI; XMStoreFloat3(&m_targetRotation, target); m_animationTime = 0.0f; m_isAnimationRunning = true; }

Этот код вносит два изменения в целевое вращение:

  1. Указывается общее количество полных оборотов, совершенных до настоящего момента в рамках целевого вращения, в результате чего каждый поворот будет выполняться относительно предыдущего поворота, а не исходного положения кубика.
  2. Добавляется -1, 0 и 1 полный оборот к целевому относительному вращению. Для этого мы создаем вектор на основе трех значений, выбранных случайно из ряда -1, 0 и 1, и умножаем полученную величину на 2 * PI (полный оборот в радианах).

Также обратите внимание на то, что мы используем XMVECTORs еще раз для ускорения расчета, это также позволяет использовать только одну формулу вместо трех (если бы компоненты х, у, z вычислялись по отдельности).

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

Осталось всего несколько шагов, и наше приложение будет работать на устройствах с Windows RT и Windows Phone. Эти шаги мы рассмотрим в третьей и последней статье блога из этой серии. Следите за обновлениями!

Автор статьи: Дмитрий Андреев.