Pull to refresh
0

Разработка трехмерных игр для Windows 8 с помощью C++ и Microsoft DirectX

Reading time20 min
Views44K
Original author: Bruno Sonnino


Разработка игр — постоянно актуальная тема: всем нравится играть в игры, их охотно покупают, поэтому их выгодно продавать. Но при разработке хороших игр следует обращать немало внимания на производительность. Никому не понравится игра, «тормозящая» или работающая рывками даже на не самых мощных устройствах.
В этой статье я покажу, как разработать простую футбольную 3D игру с использованием Microsoft DirectX и C++, хотя главным образом я занимаюсь разработкой на C#. В прошлом я довольно много работал с C++, но теперь этот язык для меня уже не столь прост. Кроме того, DirectX для меня является новинкой, поэтому эту статью можно считать точкой зрения новичка на разработку игр. Прошу опытных разработчиков простить меня за возможные ошибки.

Мы будем использовать пакет Microsoft Visual Studio 3D Starter Kit — естественный начальный ресурс для всех желающих разрабатывать игры для Windows 8.1.

Microsoft Visual Studio 3D Starter Kit

После загрузки пакета Starter Kit можно распаковать его в папку и открыть файл StarterKit.sln. В этом решении есть уже готовый проект C++ для Windows 8.1. При его запуске появится изображение, похожее на рис. 1.


Рисунок 1. Начальное состояние Microsoft Visual Studio 3D Starter Kit

Эта программа в составе Starter Kit демонстрирует несколько полезных элементов.
  • Анимировано пять объектов: четыре фигуры вращаются вокруг чайника, а чайник, в свою очередь, «танцует».
  • Каждый предмет сделан из отдельного материала; некоторые имеют сплошной цвет, а поверхность куба представляет собой растровый рисунок.
  • Источник света находится в верхнем левом углу сцены.
  • В правом нижнем углу экрана расположен счетчик кадровой скорости (количество кадров в секунду).
  • Сверху находится индикатор очков.
  • Если щелкнуть какой-либо предмет, он выделяется и увеличивается количество очков.
  • Если щелкнуть экран игры правой кнопкой мыши или провести по экрану от нижнего края к середине, появятся две кнопки для последовательного переключения цвета чайника.


Основной цикл игры находится в файле StarterKitMain.cpp, где отрисовывается страница и счетчик кадровой скорости. Game.cpp содержит игровой цикл и проверку нажатий. В этом файле в методе Update вычисляется анимация, а в методе Render происходит отрисовка всех объектов. Счетчик кадровой скорости отрисовывается в SampleFpsTextRenderer.cpp. Объекты игры находятся в папке Assets. Teapot.fbx — это чайник, а файл GameLevel.fbx содержит четыре фигуры, которые вращаются вокруг танцующего чайника.
Теперь, ознакомившись с образцом приложения в пакете Starter Kit, можно перейти к созданию собственной игры.

Добавление ресурсов в игру

Мы разрабатываем игру в футбол, поэтому самым первым нашим ресурсом должен быть футбольный мяч, который мы добавим в Gamelevel.fbx. Сначала нужно удалить из этого файла четыре фигуры, выделив каждую из них и нажав кнопку Delete. В обозревателе решений удалите и файл CubeUVImage.png, поскольку он нам не нужен: это текстура для куба, который мы только что удалили.
Теперь добавляем сферу в модель. Откройте инструменты (если их не видно, щелкните View > Toolbox) и дважды щелкните сферу, чтобы добавить ее в модель. Нам также требуется растянутая текстура, такая как на рис. 2.


Рисунок 2. Текстура футбольного мяча, приспособленная к сфере

Если вы не хотите создавать собственные модели в Visual Studio, можно найти готовые модели в Интернете. Visual Studio поддерживает любые модели в формате FBX, DAE и OBJ: достаточно добавить их в состав ресурсов решения. Например, можно использовать файл .obj, подобный показанному на рис. 3 (бесплатная модель с сайта TurboSquid). 


Рисунок 3. Трехмерная OBJ-модель мяча

Анимация модели

Модель готова, теперь пора ее анимировать. Но сначала нужно убрать чайник, поскольку он нам не понадобится. В папке Assets удалите файл teapot.fbx. Теперь удалите его загрузку и анимацию. В файле Game.cpp загрузка моделей происходит асинхронно в CreateDeviceDependentResources:
Код
// Load the scene objects.
auto loadMeshTask = Mesh::LoadFromFileAsync(
	m_graphics,
	L"gamelevel.cmo",
	L"",
	L"",
	m_meshModels)
	.then([this]()
{
	// Load the teapot from a separate file and add it to the vector of meshes.
	return Mesh::LoadFromFileAsync(

Нужно изменить модель и удалить продолжение задачи, чтобы загружался только мяч:
Код
void Game::CreateDeviceDependentResources()
{
	m_graphics.Initialize(m_deviceResources->GetD3DDevice(), m_deviceResources->GetD3DDeviceContext(), m_deviceResources->GetDeviceFeatureLevel());

	// Set DirectX to not cull any triangles so the entire mesh will always be shown.
	CD3D11_RASTERIZER_DESC d3dRas(D3D11_DEFAULT);
	d3dRas.CullMode = D3D11_CULL_NONE;
	d3dRas.MultisampleEnable = true;
	d3dRas.AntialiasedLineEnable = true;

	ComPtr<ID3D11RasterizerState> p3d3RasState;
	m_deviceResources->GetD3DDevice()->CreateRasterizerState(&d3dRas, &p3d3RasState);
	m_deviceResources->GetD3DDeviceContext()->RSSetState(p3d3RasState.Get());

	// Load the scene objects.
	auto loadMeshTask = Mesh::LoadFromFileAsync(
		m_graphics,
		L"gamelevel.cmo",
		L"",
		L"",
		m_meshModels);
		

	(loadMeshTask).then([this]()
	{
		// Scene is ready to be rendered.
		m_loadingComplete = true;
	});
}

Методу ReleaseDeviceDependentResources нужно лишь очистить сетки:
Код
void Game::ReleaseDeviceDependentResources()
{
	for (Mesh* m : m_meshModels)
	{
		delete m;
	}
	m_meshModels.clear();

	m_loadingComplete = false;
}

Теперь нужно изменить метод Update, чтобы вращался только мяч:
Код
void Game::Update(DX::StepTimer const& timer)
{
	// Rotate scene.
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
}

Для управления скоростью вращения используется множитель (0.5f). Чтобы мяч вращался быстрее, нужно просто увеличить этот множитель. За каждую секунду мяч будет поворачиваться на угол 0,5/(2 * Пи) радиан. Метод Render отрисовывает мяч с нужным углом вращения:
Код
void Game::Render()
{
// Loading is asynchronous. Only draw geometry after it's loaded.
if (!m_loadingComplete)
{
return; 
}

auto context = m_deviceResources->GetD3DDeviceContext();

// Set render targets to the screen.
auto rtv = m_deviceResources->GetBackBufferRenderTargetView(); 
auto dsv = m_deviceResources->GetDepthStencilView(); 
ID3DllRenderTargetView *const targets[1] = { rtv }; 
context->OMSetRenderTargets(1, targets, dsv);

// Draw our scene models.
XMMATRIX rotation = XMMatrixRotationY(m_rotation);
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);

m_meshModels[i]->Render(m_graphics, modelTransform); } }
ToggleHitEffect здесь не будет работать: свечение мяча не изменится при его нажатии.
void Game :: ToggleHitEf feet (String^ object) 
{

}

Нам не нужно, чтобы изменялась подсветка мяча, но нужно получать данные о его касании. Для этого используем измененный метод onHitobject:
Код
String^ Game: :OnHitobject (int x , int y)
{
String^ result = nullptr;

XMFLOAT3 point;
XMFLOAT3 dir;
m_graphics.GetCamera().GetWorldLine(x, y, &point, &dir);
XMFLOAT4X4 world;
XMMATRIX worldMat = XMMatrixRotationY(m_rotation);
XMStoreFloat4x4(&world, worldMat);
 
float closestT = FLT_MAX;
for (Mesh* m : m_meshModels) {
XMFLOAT4X4 meshTransform = world;

auto name = ref new String(m->Name());
float t = 0;
bool hit = HitTestingHelpers::LineHitTest(*m, &point, &dir, SmeshTransform, &t);
if (hit && t < closestT)
{
result = name;
} }

return result; }

Если сейчас запустить проект, вы увидите, что мяч вращается вокруг своей оси Y. Теперь приведем мяч в движение.

Движение мяча

Чтобы мяч двигался, нужно перемещать его, например, вверх и вниз. Сначала нужно объявить переменную для текущего положения мяча в Game.h:
Код
class Game
{
public:
// snip private:
// snip
float m_translation;

Затем в методе Update нужно вычислить текущее положение:
Код
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
const float maxHeight = 7. Of;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
m_translation = totalTime > 1.0f ?
maxHeight - (maxHeight * (totalTime - 1.0f)) : maxHeight *totalTime;
}

Теперь мяч будет подниматься и опускаться каждые 2 секунды. В течение первой секунды мяч будет подниматься, в течение следующей секунды — опускаться. Метод Render вычисляет получившуюся матрицу и отрисовывает мяч в новом положении:
Код
void Game::Render() {
// snip
// Draw our scene models.
XMMATRIX rotation = XMMatrixRotationY(m_rotation);
rotation *= XMMatrixTranslation(0, m_translation, 0);

Если сейчас запустить проект, вы увидите, что мяч движется вверх и вниз с постоянной скоростью. Теперь нужно придать мячу физические свойства.

Добавление физики мяча

Чтобы придать мячу физические эффекты, нужно сымитировать воздействие на него силы, представляющей гравитацию. Если вы помните школьный курс физики, то знаете, что ускоренное движение тела описывается следующими уравнениями:
s = s0 + v0t + 1/2at2
v = v0 + at

Где s — положение тела в момент t, s0 — начальное положение, v0 — начальная скорость, a — ускорение. Для движения по вертикали a — ускорение свободного падения (-10 м/с2), а s0 = 0 (сначала мяч находится на земле, то есть на нулевой высоте). Уравнения превращаются в следующие:
s = v0t-5t2
v = v0-10t

Мы хотим достигнуть максимальной высоты за 1 секунду. На максимальной высоте скорость равна 0. Поэтому второе уравнение позволяет найти начальную скорость:
0 = v0 — 10 * 1 => v0 = 10 м/с
Это дает нам перемещение мяча:
s = 10t — 5t2
Нужно изменить метод Update, чтобы использовать это уравнение:
Код
void Game::Update(DX::StepTimer consts timer) {
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
m_translation = 10*totalTime - 5 *totalTime*totalTime;
}

Теперь, когда мяч реалистично движется вверх и вниз, пора добавить футбольное поле.

Добавление футбольного поля

Чтобы добавить футбольное поле, нужно создать новую сцену. В папке Assets щелкните правой кнопкой мыши, чтобы добавить новую трехмерную сцену, и назовите ее field.fbx. Из набора инструментов добавьте плоскость и выберите ее, измените ее размер по оси X на 107, а по оси Z на 60. Задайте для свойства этой плоскости Texture1 изображение футбольного поля.  
Теперь можно использовать средство масштабирования (или нажимать клавишу Z) для уменьшения изображения.
Затем нужно загрузить модель в CreateDeviceDependentResources в Game.cpp:
Код
void Game::CreateDeviceDependentResources() {
// snip
// Load the scene objects.
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
); 
});
(loadMeshTask) .then([this] ()
{
// Scene is ready to be rendered. 
m_loadingComplete = true;
});
}

При запуске программы вы увидите, что поле прыгает вместе с мячом. Чтобы поле перестало двигаться, нужно изменить метод Render:
Код
// Renders one frame using the Starter Kit helpers, 
void Game::Render()
{
// snip
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
m_meshModels[i]->Render(m_graphics, XMMatrixIdentity()); 
} 
}

При этом изменении преобразование применяется только к мячу. Поле отрисовывается без преобразования. Если запустить код сейчас, вы увидите, что мяч отскакивает от поля, но «проваливается» в него в нижней части. Для исправления этой ошибки нужно перенести поле на -0,5 по оси Y. Выберите поле и измените его перенос по оси Y на -0,5. Теперь при запуске приложения мяч будет отскакивать от поля, как на рис. 4.


Рисунок 4. Мяч отскакивает от поля

Задание положения камеры и мяча

Мяч расположен в центре поля, но нам он там не нужен. В этой игре мяч должен находиться на 11-метровой отметке. Следует переместить мяч по оси X, изменив перемещение мяча в методе Render в Game.cpp:
rotation *= XMMatrixTranslation(63.0, m_translation, 0);

Мяч перемещается на 63 единицы по оси X, то есть устанавливается на 11-метровую отметку. После этого изменения вы перестанете видеть мяч, поскольку он вне поля зрения камеры: камера установлена в центре поля и направлена на середину. Нужно изменить положение камеры, чтобы она была направлена на линию ворот. Это нужно сделать в CreateWindowSizeDependentResources в файле Game.cpp:
Код
m_graphics.GetCamera().SetViewport((UINT) outputSize.Width, (UINT) outputSize.Height); 
m_graphics.GetCamera().SetPosition(XMFLOAT3(25.Of, 10.0f, 0.0f)); 
m_graphics.GetCamera().SetLookAt(XMFLOAT3(100.0f, 0.0f, 0.0f)); 
float aspectRatio = outputSize.Width / outputSize.Height; 
float fovAngleY = 30.0f * XM_PI / 180.0f;
if (aspectRatio < 1.0f)
{
// Portrait or snap view
m_graphics.GetCamera().SetUpVector(XMFLOAT3(1.0f, 0.0f, 0.0f));
fovAngleY = 120.0f * XM_PI / 180.0f;
} else
{
// Landscape view.
m_graphics.GetCamera().SetUpVector(XMFLOAT3(0.0f, 1.0f, 0.0f)); 
}
m_graphics.GetCamera().SetProjection(fovAngleY, aspectRatio, 1.0f, 100.0f);

Теперь камера находится между отметкой середины поля и 11-метровой отметкой и направлена в сторону линии ворот. Новое представление показано на рис. 5.


Рисунок 5. Измененное положение мяча и новое положение камеры

Добавление штанги ворот

Чтобы добавить на поле ворота, понадобится новая трехмерная сцена с воротами. Можно создать собственную модель или использовать готовую. Эту модель следует добавить в папку Assets, чтобы ее можно было скомпилировать и использовать.
Эту модель нужно загрузить в методе CreateDeviceDependentResources в файле Game.cpp:
Код
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
); 
});

После загрузки задайте положение и отрисуйте в методе Render в Game.cpp:
Код
auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5, 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
m_meshModels[i]->Render(m_graphics, goalTransform); }

Это изменение применяет преобразование к воротам и отрисовывает их. Это преобразование является сочетанием трех преобразований: масштабированием (увеличение исходного размера в 2 раза), поворотом на 90 градусов и перемещением на 85,5 единиц по оси X и на -0,5 единиц по оси Y из-за глубины поля. После этого ворота устанавливаются лицом к полю на линии ворот. Обратите внимание, что важен порядок преобразований: если применить вращение после перемещения, то ворота будут отрисованы совсем в другом месте, и вы их не увидите.

Удар по мячу

Все элементы установлены на свои места, но мяч все еще подпрыгивает. Пора по нему ударить. Для этого нужно снова применить физические навыки. Удар по мячу выглядит примерно так, как показано на рис. 6.


Рисунок 6. Схема удара по мячу

Удар по мячу осуществляется с начальной скоростью v0 под углом α (если не помните школьные уроки физики, поиграйте немного в Angry Birds, чтобы увидеть этот принцип в действии). Движение мяча можно разложить на два разных движения: по горизонтали — это движение с постоянной скоростью (исходим из того, что отсутствует сопротивление воздуха и воздействие ветра), а также вертикальное движение — такое же, как мы использовали раньше. Уравнение движения по горизонтали:
sX = s0 + v0*cos(α)*t
Уравнение движения по вертикали:
sY = s0 + v0*sin(α)*t — 1/2*g*t2
Таким образом, у нас два перемещения: одно по оси X, другое по оси Y. Если удар нанесен под углом 45 градусов, то cos(α) = sin(α) = sqrt(2)/2, поэтому v0*cos(α) = v0*sin(a)*t. Нужно, чтобы мяч попал в ворота, поэтому дальность удара должна превышать 86 единиц (расстояние до линии ворот равно 85,5). Нужно, чтобы полет мяча занимал 2 секунды. При подстановке этих значений в первое уравнение получим:
86 = 63 + v0* cos(α) * 2 >= v0* cos(α) = 23/2 = 11,5
Если заменить значения в уравнении, то уравнение перемещения по оси Y будет таким:
sY = 0 + 11,5*t-5*t2
А по оси X — таким:
sX = 63 + 11,5*t
Уравнение для оси Y дает нам время, когда мяч снова ударится о землю. Для этого нужно решить квадратное уравнение (да, я понимаю, что вы надеялись навсегда распрощаться с ними после школьного курса алгебры, но тем не менее вот оно):
(-b ± sqrt(b0 — 4*a*c))/2*a >= (-11,5 ± sqrt(11,52 — 4 * -5 * 0)/2 * -5 >= 0 или 23/10 >= 2,3 с
Этими уравнениями можно заменить перемещение для мяча. Сначала в Game.h создайте переменные для сохранения перемещения по трем осям:
float m_translationX, m_translationY, m_translationZ;
Затем в методе Update в Game.cpp добавьте уравнения:
Код
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0 + 11.5 * totalTime;
m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime; 
}

Метод Render использует эти новые перемещения:
rotation *= XMMatrixTranslation(m_translationX, m_translationY, 0);
Если запустить программу сейчас, вы увидите, как мяч влетает в середину ворот. Если нужно, чтобы мяч двигался в других направлениях, нужно добавить горизонтальный угол удара. Для этого мы используем перемещение по оси Z.
Расстояние от 11-метровой отметки до ворот составляет 22,5 единицы, а расстояние между штангами ворот — 14 единиц. Это дает нам угол α = atan(7/22.5), то есть 17 градусов. Можно вычислить и перемещение по оси Z, но можно сделать и проще: мяч должен переместиться до линии в тот же момент, когда он достигнет штанги. Это означает, что мяч должен переместиться на 7/22,5 единицы по оси Z и на 1 единицу по оси X. Уравнение для оси Z будет таким:
sz = 11,5 * t/3,2 ≥ sz = 3,6 * t

Это перемещение до штанги ворот. У любого перемещения с меньшей скоростью угол будет меньше. Чтобы мяч достиг ворот, скорость должна составлять от -3,6 (левая штанга) до 3,6 (правая штанга). Если учесть, что мяч должен полностью попасть в ворота, максимальное расстояние составляет 6/22,5, а скорость — от 3 до -3. Имея эти цифры, можно задать угол удара в методе Update: 
Код
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0 + 11.5 * totalTime;
m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime;
m_translationZ = 3 * totalTime;
}
Перемещение по оси Z будет использовано в методе Render:
rotation *= XMMatrixTranslation(m_translationX, m_translationY, m_translationZ);
… .

Результат должен быть примерно таким, как на рис. 7.


Рисунок 7. Удар под углом

Добавление вратаря

Движение мяча уже готово, ворота на месте, теперь нужно добавить вратаря, который будет ловить мяч. В роли вратаря у нас будет искаженный куб. В папке 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#.
Основное затруднение состоит в освоении трехмерных моделей, в их движении и расположении привычным образом. Для этого потребовалось применить знания физики, геометрии, тригонометрии и математики.
Как бы то ни было, можно заключить, что разработка игры не является непосильной задачей. При наличии терпения и нужных инструментов можно создать великолепные игры с превосходной производительностью.
Tags:
Hubs:
+27
Comments4

Articles

Information

Website
www.intel.ru
Registered
Founded
Employees
5,001–10,000 employees
Location
США
Representative
Анастасия Казантаева