Добавление вратаря
Движение мяча уже готово, ворота на месте, теперь нужно добавить вратаря, который будет ловить мяч. В роли вратаря у нас будет искаженный куб. В папке Assets добавьте новый элемент (новую трехмерную сцену) и назовите его goalkeeper.fbx.
Добавьте куб из набора инструментов и выберите его. Задайте масштаб: 0,3 по оси X, 1,9 по оси Y и 1 по оси Z. Для свойства MaterialAmbient установите значение 1 для красного цвета и значение 0 для синего и зеленого цвета, чтобы сделать объект красным.
Измените значение свойства Red в разделе MaterialSpecular на 1 и значение свойства MaterialSpecularPower на 0,2.
Загрузите новый ресурс в методе CreateDeviceDependentResources:
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels)
.then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"field.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
}).then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"soccer_goal.cmo",
L"",
L"",
m_meshModels, false // Do not clear the vector of meshes
);
}).then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"goalkeeper.cmo",
L"",
L"",
m_meshModels, false // Do not clear the vector of meshes
);
});
Теперь нужно расположить вратаря в середине ворот и отрисовать его. Это нужно сделать в методе Render в Game.cpp:
void Game::Render()
{
// snip
auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5f, 0);
auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.4f, 0) ;
for (UINT i = 0; i < m_meshModels.size(); i++)
{
XMMATRIX modelTransform = rotation;
String^ meshName = ref new String (m_meshModels [i]->Name ()) ;
m_graphics.UpdateMiscConstants(m_miscConstants);
if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
m_meshModels[i]->Render(m_graphics, modelTransform);
else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0)
m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
else if (String::CompareOrdinal(meshName, L"Cube_Node") == 0)
m_meshModels[i]->Render(m_graphics, goalkeeperTransform);
else
m_meshModels[i]->Render(m_graphics, goalTransform);
}
}
Этот код размещает вратаря в середине ворот. Теперь нужно сделать так, чтобы вратарь мог перемещаться влево и вправо, чтобы ловить мяч. Для управления движением вратаря пользователь будет нажимать на клавиши со стрелками влево и вправо.
Движение вратаря ограничено штангами ворот, расположенными на расстоянии +7 и -7 единиц по оси Z. Ширина вратаря составляет 1 единицу в каждую сторону, поэтому он может перемещаться на 6 единиц влево или вправо.
Нажатие клавиши перехватывается на странице XAML (Directxpage.xaml) и перенаправляется в класс Game. Добавляем обработчик событий KeyDown в Directxpage.xaml:
<Page
x:Class="StarterKit.DirectXPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:StarterKit"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" KeyDown="OnKeyDown">
Обработчик событий в DirectXPage.xaml.cpp:
void DirectXPage::OnKeyDown(Platform::ObjectΛ sender,
Windows::UI::Xaml::Input::KeyRoutedEventArgsΛ e)
{
m_main->OnKeyDown( ->Key);
}
m_main является экземпляром класса StarterKitMain, который отрисовывает сцены игры и счетчик кадровой скорости. Нужно объявить публичный метод в StarterKitMain.h:
class StarterKitMain : public DX::IDeviceNotify
{
public:
StarterKitMain(const std::shared_ptr<DX::DeviceResources>& deviceResources);
~StarterKitMain();
// Public methods passed straight to the Game renderer.
Platform: : String'^ OnHitObject (int x, int y) {
return m_sceneRenderer->OnHitObject(x, y); }
void OnKeyDown(Windows::System::VirtualKey key) {
m_sceneRenderer->OnKeyDown(key); }
… .
Этот метод перенаправляет клавишу методу OnKeyDown в классе Game. Теперь нужно объявить метод OnKeyDown в файле Game.h:
class Game
{ public:
Gamefconst std::shared_ptr<DX::DeviceResources>& deviceResources);
void CreateDeviceDependentResources();
void CreateWindowSizeDependentResources();
void ReleaseDeviceDependentResources();
void Update(DX::StepTimer consts timer);
void Render();
void OnKeyDown(Windows::System::VirtualKey key);
…
Этот метод обрабатывает нажатие клавиш и перемещает вратаря в соответствующую сторону. Перед созданием этого метода нужно объявить частное поле в файле Game.h для сохранения положения вратаря:
class Game {
// snip
private:
// snip
float m_goalkeeperPosition;
Изначально вратарь занимает положение 0. Это значение будет увеличиваться или уменьшаться при нажатии пользователем клавиши со стрелкой. Если положение больше 6 или меньше -6, положение вратаря не изменяется. Это нужно сделать в методе OnKeyDown в Game.cpp:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6.0;
const float MinGoalkeeperPosition = -6.0;
if (key == Windows::System::VirtualKey::Right)
m_goalkeeperPosition = m_goalkeeperPosition >= MaxGoalkeeperPosition ?
m_goalkeeperPosition : m_goalkeeperPosition + 0.1f;
else if (key == Windows::System::VirtualKey::Left)
m_goalkeeperPosition = m_goalkeeperPosition <= MinGoalkeeperPosition ?
m_goalkeeperPosition : m_goalkeeperPosition - 0.1f;
}
Новое положение вратаря используется в методе Render файла Game.cpp, где вычисляется перемещение вратаря:
auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.40f, m_goalkeeperPosition);
Применив эти изменения, можно запустить игру: вы увидите, что вратарь движется вправо или влево при нажатии соответствующих клавиш со стрелками (см. рис. 8).
Увеличить
Рисунок 8. Игра с вратарем в нужном положении
До сих пор мяч двигался постоянно, но это нам не нужно. Мяч должен начинать движение непосредственно после удара и останавливаться при достижении ворот. Вратарь также не должен двигаться до удара по мячу.
Необходимо объявить частное поле m_isAnimating в файле Game.h, чтобы игра «знала», когда мяч движется:
class Game
{
public:
// snip
private:
// snip
bool m_isAnimating;
Эта переменная используется в методах Update и Render в Game.cpp, поэтому мяч перемещается, только когда m_isAnimating имеет значение true:
void Game::Update(DX::StepTimer consts timer)
{
if (m_isAnimating)
{
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.Of * totalTime*totalTime;
m_translationZ = 3.0f * totalTime;
}
}
void Game::Render()
{
// snip
XMMATRIX modelTransform;
if (m_isAnimating)
{
modelTransform = XMMatrixRotationY(m_rotation);
modelTransform *= XMMatrixTranslation(m_translationX, m_translationY,
m_translationZ);
}
else
modelTransform = XMMatrixTranslation(63.0f, 0.0f, 0.0f);
… .
Переменная modelTransform перемещается из цикла к началу. Нажатие клавиш со стрелками следует обрабатывать в методе OnKeyDown, только когда m_isAnimating имеет значение true:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6.0f;
if (m_isAnimating)
{
auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
0.1f : -0.1f;
m_goalkeeperPosition = fabs(m_goalkeeperPosition) >=
MaxGoalkeeperPosition ?
m_goalkeeperPosition :
m_goalkeeperPosition +
goalKeeperVelocity;
}
}
Теперь нужно ударить по мячу. Это происходит, когда пользователь нажимает пробел. Объявите новое частное поле m_isKick в файле Game.h:
class Game
{
public:
// snip
private:
// snip
bool m_isKick;
Установите для этого поля значение true в методе OnKeyDown в Game.cpp:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6. Of;
if (m_isAnimating)
{
auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
0.1f : -0.1f;
m_goalkeeperPosition = fabs(m_goalkeeperPosition) >=
MaxGoalkeeperPosition ?
m_goalkeeperPosition :
m_goalkeeperPosition + goalKeeperVelocity;
}
else if ( y == Windows::System::VirtualKey::Space)
m_isKick = true;
}
Когда m_isKick имеет значение true, в методе Update запускается анимация:
void Game::Update(DX::StepTimer consts timer)
{
if (m_isKick)
{
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
}
if (m_isAnimating)
{
auto totalTime = static_cast<float>(timer.GetTotalSeconds()) –
m_startTime;
m_rotation = totalTime * 0.5f; m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.Of * totalTime*totalTime;
m_translationZ = 3.0f * totalTime;
if (totalTime > 2.3f)
ResetGame();
}
}
Начальное время удара хранится в переменной m_startTime (объявленной как приватное поле в файле Game.h), которая используется для вычисления времени удара. Если оно превышает 2,3 секунды, игра сбрасывается (за это время мяч уже должен был достигнуть ворот). Метод ResetGame объявляется как частный в Game.h:
void Game::ResetGame()
{
m_isAnimating = false;
m_goalkeeperPosition = 0;
}
Этот метод устанавливает для m_isAnimating значение false и сбрасывает положение вратаря. Положение мяча изменять не нужно: мяч будет отрисован на 11-метровой отметке, если m_isAnimating имеет значение false. Также нужно изменить угол удара. Этот код направляет удар вблизи правой штанги:
m_translationZ = 3.0f * totalTime;
Нужно изменить этот подход, чтобы удары были случайными и пользователь не знал, куда будет направлен следующий удар. Необходимо объявить приватное поле m_ballAngle в файле Game.h и инициализировать его при ударе по мячу в методе Update:
void Game::Update(DX::StepTimer const& timer)
{
if (m_isKick)
{
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
m_ballAngle = (static_cast <float> (rand()) /
static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
}
… .
Rand()/RAND_MAX дает результат от 0 до 1. Нужно вычесть из результата 0,5, чтобы получить число от -0,5 до 0,5, а затем умножить на 6, чтобы получить итоговый угол до -3 до 3. Чтобы в каждой игре использовать разные последовательности, нужно инициализировать генератор, вызвав srand в методе CreateDeviceDependentResources:
void Game::CreateDeviceDependentResources()
{
srand(static_cast <unsigned int> (time(0)));
… .
Чтобы вызвать функцию времени, нужно включить ctime. Чтобы использовать новый угол для мяча, нужно применить m_ballAngle в методе Update:
m_translationZ = m_ballAngle * totalTime;
Теперь почти весь код готов, но нужно понять, поймал ли вратарь мяч, или же пользователь забил гол. Это можно определить простым способом: проверить, пересекается ли прямоугольник мяча с прямоугольником вратаря в момент достижения мячом линии ворот. Разумеется, для определения забитых голов можно использовать и более сложные методики, но для нашего случая описанного способа вполне достаточно. Все вычисления осуществляются в методе Update:
void Game::Update(DX::StepTimer consts timer) {
if (m_isKick) {
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
m_isGoal = m_isCaught = false;
m_ballAngle = (static_cast <float> (rand()) /
static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
}
if (m_isAnimating)
{
auto totalTime = static_cast<float>(timer.GetTotalSeconds()) –
m_startTime;
m_rotation = totalTime * 0.5f; if ( !m_isCaught)
{
// ball traveling
m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
m_translationZ = m_ballAngle * totalTime;
}
else
{
// if ball is caught, position it in the center of the goalkeeper
m_translationX = 83.35f;
m_translationY = 1.8f;
m_translationZ = m_goalkeeperPosition;
}
if (!m_isGoal && !m_isCaught && m_translationX >= 85.5f)
{
// ball passed the goal line - goal or caught
auto ballMin = m_translationZ - 0.5f + 7.0f;
auto ballMax = m_translationZ + 0.5f + 7.0f;
auto goalkeeperMin = m_goalkeeperPosition - 1.0f + 7.0f;
auto goalkeeperMax = m_goalkeeperPosition + 1.0f + 7.0f;
m_isGoal = (goalkeeperMax < ballMin || goalkeeperMin > ballMax);
m_isCaught = !m_isGoal;
}
if (totalTime > 2.3f)
ResetGame();
}
}
Объявляем два частных поля в файле Game.h: m_isGoal и m_IsCaught. Эти поля говорят нам о том, что произошло: пользователь забил гол или вратарь поймал мяч. Если оба поля имеют значение false, мяч еще летит. Когда мяч достигает вратаря, программа вычисляет границы мяча и вратаря и определяет, налагаются ли границы мяча на границы вратаря. Если посмотрите в код, то увидите, что я добавил 7.0 f к каждой границе. Я сделал это, поскольку границы могут быть положительными или отрицательными, а это усложнит вычисление наложения. Добавив 7.0 f, я добился того, что все значения стали положительными, чтобы упростить вычисление. Если мяч пойман, его положение устанавливается по центру вратаря. m_isGoal и m_IsCaught сбрасываются при ударе.
Итак, мы получили вполне рабочий вариант игры. Желающих продолжить ее совершенствовать, в частности, добавить ведение счета или сенсорное управление, отсылаем к исходному материалу на IDZ.
Заключение
Итак, дело сделано. От танцующего чайника мы пришли к игре на DirectX. Языки программирования становятся все более похожими, поэтому использование C++/DX не вызвало особых затруднений у разработчика, привыкшего пользоваться C#.
Основное затруднение состоит в освоении трехмерных моделей, в их движении и расположении привычным образом. Для этого потребовалось применить знания физики, геометрии, тригонометрии и математики.
Как бы то ни было, можно заключить, что разработка игры не является непосильной задачей. При наличии терпения и нужных инструментов можно создать великолепные игры с превосходной производительностью.