Quantcast
Channel: Статьи Intel Developer Zone
Viewing all articles
Browse latest Browse all 357

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

$
0
0

Скачать PDF

Автор: Бруно Соннино (Bruno Sonnino)

Разработка игр — постоянно актуальная тема: всем нравится играть в игры, их охотно покупают, поэтому их выгодно продавать. Но при разработке хороших игр следует обращать немало внимания на производительность. Никому не понравится игра, «тормозящая» или работающая рывками даже на не самых мощных устройствах.

Для разработки игр можно применять различные языки и платформы, но если в игре для Windows* нужно добиться производительности, рецепт однозначен: 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 демонстрирует несколько полезных элементов:

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

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

Начнем с файла App.xaml и его аналогов cpp/h. При запуске приложения App.xaml запускается DirectXPage. В DirectXPage.xaml находится элемент SwapChainPanel и панель приложения. Элемент SwapChainPanel служит поверхностью для размещения графики DirectX на странице XAML. Здесь можно добавлять объекты XAML, которые будут отображаться в сцене Microsoft Direct3D* — это удобно для добавления кнопок, подписей и других объектов XAML в игру DirectX без необходимости создания собственных элементов управления с нуля. Пакет Starter Kit также включает элемент StackPanel, который мы будем использовать для подсчета очков.

В Directxpage.xaml.cpp происходит инициализация переменных, подключение обработчиков событий для изменения размера и ориентации, обработчиков событий щелчков мышью и нажатия кнопок на панели приложения. Кроме того, в этом файле содержится и цикл отрисовки. Все объекты XAML обрабатываются как любые другие программы для Windows 8. Файл также обрабатывает событие Tapped, проверяя, приходится ли касание (или щелчок мышью) на объект. Если да, то событие увеличивает счет для этого объекта.

Необходимо сообщить программе, что SwapChainPanel должен отрисовывать содержимое DirectX. Для этого, согласно документации, нужно «вызвать экземпляр SwapChainPanel для IInspectable или IUnknown, затем вызвать Querylnterface для получения ссылки на интерфейс ISwapchainPanelNative (это собственная реализация интерфейса, дополняющая SwapChainPanel и поддерживающая обмен информацией). Затем следует вызвать ISwapchainPanelNative::SetSwapChain для этой ссылки, чтобы связать реализованную цепочку с экземпляром SwapChainPanel». Это осущест¬вляется в методе CreateWindowSizeDependentResources в файле DeviceResources.cpp.

Основной цикл игры находится в файле StarterKitMain.cpp, где отрисовывается страница и счетчик количества кадров в секунду.

Game.cpp содержит игровой цикл и проверку нажатий. В этом файле в методе Update вычисляется анимация, а в методе Render происходит отрисовка всех объектов. Счетчик кадровой скорости отрисовывается в SampleFpsTextRenderer.cpp.

Объекты игры находятся в папке Assets. Teapot.fbx — это чайник, а файл GameLevel.fbx содержит четыре фигуры, которые вращаются вокруг танцующего чайника.

Теперь, ознакомившись с образцом приложения в пакете Starter Kit, можно перейти к созданию собственной игры.

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

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

Теперь добавляем сферу в модель. Откройте инструменты (если их не видно, щелкните View > Toolbox) и дважды щелкните сферу, чтобы добавить ее в модель. Если мяч кажется слишком маленьким, можно увеличить масштаб, нажав вторую кнопку на панели инструментов в верхней части окна редактора, нажав клавишу Z на клавиатуре, с помощью мыши (нажмите и перетащите к середине экрана, чтобы увеличить изображение) или с помощью клавиш со стрелками вверх и вниз. Также для управления масштабированием можно использовать колесико мыши при нажатой клавише Ctrl. Результат должен быть примерно таким, как на рис. 2.


Рисунок 2. Редактор моделей с фигурой сферы.

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


Рисунок 3.Текстура мяча в виде шестиугольной сетки: первая попытка.

Чтобы наложить текстуру на сферу, выберите ее, затем в окне свойств назначьте файл .png свойству Texture1. Идея вроде была неплохая, но результат не особенно удался, как видно на рис. 4.


Рисунок 4.Сфера с наложенной текстурой.

Шестиугольники растянуты из-за проекции точек текстуры на сферу. Нам требуется растянутая текстура, такая как на рис. 5.


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

При наложении этой текстуры сфера уже больше похожа на футбольный мяч. Чтобы изображение было более реалистичным, нужно настроить некоторые свойства. Для этого нужно выбрать мяч и изменить эффект Phong в окне свойств. Модель освещения Phong включает направленное и рассеянное освещение и моделирует отражающие свойства объекта. Это шейдер, входящий в состав Visual Studio, его можно перетащить из набора инструментов. Для получения дополнительных сведений о шейдерах и об их создании с помощью конструктора шейдеров Visual Studio щелкните ссылку «Дополнительные сведения». Установите для свойств Red, Green, и Blue в разделе MaterialSpecular значение 0.2, а для свойства MaterialSpecularPower — значение 16. Теперь наш футбольный мяч выглядит лучше (рис. 6).


Рисунок 6. Готовый футбольный мяч.

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


Рисунок 7. Трехмерная 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();
	ID3D11RenderTargetView *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::ToggleHitEffect(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, &meshTransform, &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 const& timer)
{
	// Rotate scene.
	m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
	const float maxHeight = 7.0f;
	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 m/s

Это дает нам перемещение мяча:

s = 10t – 5t2

Нужно изменить метод Update, чтобы использовать это уравнение:


void Game::Update(DX::StepTimer const& 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. Теперь при запуске приложения мяч будет отскакивать от поля, как на рис. 8.


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

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

Мяч расположен в центре поля, но нам он там не нужен. В этой игре мяч должен находиться на 11-метровой отметке. Если посмотреть на редактор сцены на рис. 9, видно, что следует переместить мяч по оси X, изменив перемещение мяча в методе Render в Game.cpp:


rotation *= XMMatrixTranslation(63.0, m_translation, 0);



Мяч перемещается на 63 единицы по оси X, то есть устанавливается на 11-метровую отметку.


Figure 9.Field with Axis – X (red) and Z (blue)

После этого изменения вы перестанете видеть мяч, поскольку он вне поля зрения камеры: камера установлена в центре поля и направлена на середину. Нужно изменить положение камеры, чтобы она была направлена на линию ворот. Это нужно сделать в CreateWindowSizeDependentResources в файле Game.cpp:


m_graphics.GetCamera().SetViewport((UINT) outputSize.Width, (UINT) outputSize.Height);
m_graphics.GetCamera().SetPosition(XMFLOAT3(25.0f, 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-метровой отметкой и направлена в сторону линии ворот. Новое представление показано на рис. 10.


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

Теперь нужно добавить ворота.

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

Чтобы добавить на поле ворота, понадобится новая трехмерная сцена с воротами. Можно создать собственную модель или использовать готовую. Эту модель следует добавить в папку 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 из-за глубины поля. После этого ворота устанавливаются лицом к полю на линии ворот, как показано на рис. 11. Обратите внимание, что важен порядок преобразований: если применить вращение после перемещения, то ворота будут отрисованы совсем в другом месте, и вы их не увидите.


Рисунок 11. Поле с установленными воротами.

Удар по мячу

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


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

Удар по мячу осуществляется с начальной скоростью v0 под углом α (если не помните школьные уроки физики, поиграйте немного в Angry Birds, чтобы увидеть этот принцип в действии). Движение мяча можно разложить на два разных движения: по горизонтали — это движение с постоянной скоростью (исходим из того, что отсутствует сопротивление воздуха и воздействие ветра), а также вертикальное движение — такое же, как мы использовали раньше. Уравнение движения по горизонтали:

sX = s0 + v0*cos(α)*t

Уравнение движения по вертикали:

sY = s0 + v0*sin(α)*t – ½*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

. . . and for the x-axis:

sX = 63 + 11.5 * t

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

(−b ± sqrt(b2− 4*a*c))/2*a ≥ (−11.5 ± sqrt(11.52– 4 * −5 * 0)/2 * −5 ≥ 0 or 23/10 ≥ 2.3s

Этими уравнениями можно заменить перемещение для мяча. Сначала в Game.h создайте переменные для сохранения перемещения по трем осям:


float m_translationX, m_translationY, m_translationZ;



Затем в методе Update в Game.cpp добавьте уравнения:


void Game::Update(DX::StepTimer const& 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.

На рис. 13 видно, что расстояние от 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


Рисунок 13. Схема расстояния до ворот.

Это перемещение до штанги ворот. У любого перемещения с меньшей скоростью угол будет меньше. Чтобы мяч достиг ворот, скорость должна составлять от -3,6 (левая штанга) до 3,6 (правая штанга). Если учесть, что мяч должен полностью попасть в ворота, максимальное расстояние составляет 6/22,5, а скорость — от 3 до -3. Имея эти цифры, можно задать угол удара в методе Update:


void Game::Update(DX::StepTimer const& 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);
       ….



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


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

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

Движение мяча уже готово, ворота на месте, теперь нужно добавить вратаря, который будет ловить мяч. В роли вратаря у нас будет искаженный куб. В папке 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);
	}
}

Этот код размещает вратаря в середине ворот, как показано на рис. 15 (обратите внимание, что положение камеры на снимке экрана отличается).


Рисунок 15. Вратарь в середине ворот.

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

Движение вратаря ограничено штангами ворот, расположенными на расстоянии +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(e->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:
	Game(const std::shared_ptr<DX::DeviceResources>& deviceResources);
	void CreateDeviceDependentResources();
	void CreateWindowSizeDependentResources();
	void ReleaseDeviceDependentResources();
	void Update(DX::StepTimer const& 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);



Применив эти изменения, можно запустить игру: вы увидите, что вратарь движется вправо или влево при нажатии соответствующих клавиш со стрелками (см. рис. 16).


Рисунок 16. Игра с вратарем в нужном положении.

До сих пор мяч двигался постоянно, но это нам не нужно. Мяч должен начинать движение непосредственно после удара и останавливаться при достижении ворот. Вратарь также не должен двигаться до удара по мячу.

Необходимо объявить частное поле 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 const& 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.0f * 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.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;
	}
	else if (key == Windows::System::VirtualKey::Space)
		m_isKick = true;
}

Когда m_isKick имеет значение true, в методе Update запускается анимация:


void Game::Update(DX::StepTimer const& 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.0f * 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)));
…



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


m_translationZ = m_ballAngle * totalTime;



Теперь почти весь код готов, но нужно понять, поймал ли вратарь мяч, или же пользователь забил гол. Это можно определить простым способом: проверить, пересекается ли прямоугольник мяча с прямоугольником вратаря в момент достижения мячом линии ворот. Разумеется, для определения забитых голов можно использовать и более сложные методики, но для нашего случая описанного способа вполне достаточно. Все вычисления осуществляются в методе 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_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 сбрасываются при ударе. Теперь добавим в игру счет.

Добавление счета

В игре DirectX можно отрисовывать счет с помощью Direct2D, но поскольку мы разрабатываем игру для Windows 8, то можно использовать и XAML. Можно подключать элементы XAML в игре и создавать связи между элементами XAML и игровой логикой. Это удобный способ отображения информации и взаимодействия с пользователем, поскольку не придется иметь дело с положениями элементов, отрисовщиками и циклами обновления.

В состав Starter Kit входит табло результатов XAML (то самое, которое используется для подсчета нажатий на элементы в сцене с чайником). Нужно просто изменить его, чтобы на нем отображался футбольный счет. Сначала нужно изменить DirectXPage.xaml, чтобы изменить табло счета:

<SwapChainPanel x:Name="swapChainPanel" Tapped="OnTapped"><Border VerticalAlignment="Top" HorizontalAlignment="Center" Padding="10" Background="Black"
          Opacity="0.7"><StackPanel Orientation="Horizontal"><TextBlock x:Name="ScoreUser" Text="0" Style="{StaticResource HudCounter}"/><TextBlock Text="x" Style="{StaticResource HudCounter}"/><TextBlock x:Name="ScoreMachine" Text="0" Style="{StaticResource HudCounter}"/></StackPanel></Border></SwapChainPanel>

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


void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{

}



Также можно удалить OnPreviousColorPressed, OnNextcolorPressed и ChangeObjectColor из cpp- и h-страниц, поскольку они использовались в удаленных кнопках панели приложения.

Для обновления счета в игре должен быть какой-либо способ обмена информацией между классом Game и страницей XAML. Игровой счет обновляется в классе Game, тогда как счет отображается на странице XAML. Это можно сделать, создав событие в классе Game, но у этого подхода есть недостаток. При добавлении события в класс Game возникает ошибка компиляции: «объявление события WinRT должно быть включено в класс WinRT». Причина заключается в том, что Game не является классом WinRT(ref). Чтобы использовать класс WinRT, нужно определить это событие как публичный класс ref и запечатать его:


public ref class Game sealed



Для этого можно было бы изменить класс, но мы пойдем другим путем: создадим новый класс — в данном случае класс WinRT — и будем использовать его для обмена информацией между классом Game и страницей XAML. Создайте новый класс и назовите его ViewModel:


#pragma once
ref class ViewModel sealed
{
public:
	ViewModel();
};



В файле ViewModel.h добавляем событие и свойства, необходимые для обновления результатов:


#pragma once
namespace StarterKit
{
	ref class ViewModel sealed
	{
	private:
		int m_scoreUser;
		int m_scoreMachine;
	public:
		ViewModel();
		event Windows::Foundation::TypedEventHandler<Object^, Platform::String^>^ PropertyChanged;

		property int ScoreUser
		{
			int get()
			{
				return m_scoreUser;
			}

			void set(int value)
			{
				if (m_scoreUser != value)
				{
					m_scoreUser = value;
					PropertyChanged(this, L"ScoreUser");
				}
			}
		};

		property int ScoreMachine
		{
			int get()
			{
				return m_scoreMachine;
			}

			void set(int value)
			{
				if (m_scoreMachine != value)
				{
					m_scoreMachine = value;
					PropertyChanged(this, L"ScoreMachine");
				}
			}
		};
	};

}



Объявите частное поле типа ViewModel в Game.h (нужно включить ViewModel.h в Game.h). Также нужно объявить публичный метод получения для этого поля:


class Game
{
public:
       // snip
       StarterKit::ViewModel^ GetViewModel();
private:
	StarterKit::ViewModel^ m_viewModel;



Это поле инициализируется в конструкторе Game.cpp:


Game::Game(const std::shared_ptr<DX::DeviceResources>& deviceResources) :
m_loadingComplete(false),
m_deviceResources(deviceResources)
{
	CreateDeviceDependentResources();
	CreateWindowSizeDependentResources();
	m_viewModel = ref new ViewModel();
}

Код метода получения:


StarterKit::ViewModel^ Game::GetViewModel()
{
	return m_viewModel;
}



Когда текущий удар завершается, переменные обновляются в ResetGame в файле Game.cpp:


void Game::ResetGame()
{
	if (m_isCaught)
		m_viewModel->ScoreUser++;
	if (m_isGoal)
		m_viewModel->ScoreMachine++;
	m_isAnimating = false;
	m_goalkeeperPosition = 0;
}

При изменении одного из этих двух свойств возникает событие PropertyChanged, которое можно обработать на странице XAML. Здесь есть одна недоработка: у страницы XAML нет прямого доступа к классу Game (это не ref-класс), но вместо этого осуществляется вызов StarterKitMain. Нужно создать метод получения для ViewModel в StarterKitMain.h:


class StarterKitMain : public DX::IDeviceNotify
{
public:
	// snip
	StarterKit::ViewModel^ GetViewModel() { return m_sceneRenderer->GetViewModel(); }

При наличии этой инфраструктуры можно обрабатывать событие PropertyChanged для ViewModel в конструкторе DirectXPage.xaml.cpp:


DirectXPage::DirectXPage():
	m_windowVisible(true),
	m_hitCountCube(0),
	m_hitCountCylinder(0),
	m_hitCountCone(0),
	m_hitCountSphere(0),
	m_hitCountTeapot(0),
	m_colorIndex(0)
{
	// snip

	m_main = std::unique_ptr<StarterKitMain>(new StarterKitMain(m_deviceResources));
	m_main->GetViewModel()->PropertyChanged += ref new
           TypedEventHandler<Object^, String^>(this, &DirectXPage::OnPropertyChanged);
	m_main->StartRenderLoop();
}

Этот обработчик обновляет счет (его также нужно объявить в DirectXPage.xaml.cpp.h):


void StarterKit::DirectXPage::OnPropertyChanged(Platform::Object ^sender, Platform::String ^propertyName)
{

		if (propertyName == "ScoreUser")
		{
			auto scoreUser = m_main->GetViewModel()->ScoreUser;
			Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreUser]()
			{
				ScoreUser->Text = scoreUser.ToString();
			}));
		}
		if (propertyName == "ScoreMachine")
		{
			auto scoreMachine= m_main->GetViewModel()->ScoreMachine;
			Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreMachine]()
			{
				ScoreMachine->Text = scoreMachine.ToString();
			}));
		}

}



Теперь счет обновляется каждый раз, когда пользователь забивает гол и когда вратарь ловит мяч (рис. 17):


Рисунок 17. Игра с обновлением счета.

Использование сенсорного управления и датчиков в игре

Игра уже вполне работоспособна, но почему бы ее не улучшить? Новые Ultrabook™ оборудованы датчиками и сенсорными экранами, с помощью которых можно расширить возможности игры. Вместо использования клавиатуры для ударов по мячу и перемещения вратаря пользователь может ударить по мячу, коснувшись экрана, и перемещать вратаря, наклоняя экран вправо или влево.

Чтобы ударять по мячу касанием экрана, используйте событие OnTapped в DirectXPage.cpp:


void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{
	m_main->OnKeyDown(VirtualKey::Space);
}

Этот код вызывает метод OnKeyDown, передавая в качестве параметра клавишу пробела — такой же код, как если бы пользователь нажал клавишу пробела. Если хотите, можно усовершенствовать этот код: получать положение касания и запускать удар по мячу только в том случае, если касание приходится на мяч. Пусть это будет вашим заданием на дом. В качестве начальной точки Starter Kit содержит код, чтобы обнаруживать касание пользователем объекта в сцене.

Теперь нужно сделать, чтобы вратарь перемещался, когда пользователь наклоняет экран. Для того нужно использовать измеритель угла наклона, определяющий все движения экрана. Этот датчик возвращает три значения: показатели поворота вокруг поперечной оси (X), продольной оси (Y) и вертикальной оси (Z). Для этой игры нужно считывать только поворот вокруг продольной оси.

Чтобы использовать этот датчик, нужно получить его экземпляр, для чего применяется метод GetDefault. Затем нужно задать интервал считывания, как в этом коде в void Game::CreateDeviceDependentResources в файле Game.cpp:


void Game::CreateDeviceDependentResources()
{
	m_inclinometer = Windows::Devices::Sensors::Inclinometer::GetDefault();
	if (m_inclinometer != nullptr)
	{
		// Establish the report interval for all scenarios
		uint32 minReportInterval = m_inclinometer->MinimumReportInterval;
		uint32 reportInterval = minReportInterval > 16 ? minReportInterval : 16;
		m_inclinometer->ReportInterval = reportInterval;
	}
...

m_inclinometer — частное поле, объявляемое в Game.h. В методе Update изменяем положение вратаря:


void Game::Update(DX::StepTimer const& timer)
{
	// snip
		SetGoalkeeperPosition();
		if (totalTime > 2.3f)
			ResetGame();
	}
}

SetGoalkeeperPosition изменяет положение вратаря в зависимости от значения, которое выдает датчик угла наклона:


void StarterKit::Game::SetGoalkeeperPosition()
{

	if (m_isAnimating && m_inclinometer != nullptr)
	{
		Windows::Devices::Sensors::InclinometerReading^ reading =
                    m_inclinometer->GetCurrentReading();
		auto goalkeeperVelocity = reading->RollDegrees / 100.0f;
		if (goalkeeperVelocity > 0.3f)
			goalkeeperVelocity = 0.3f;
		if (goalkeeperVelocity < -0.3f)
			goalkeeperVelocity = -0.3f;
		m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 6.0f ?
                 m_goalkeeperPosition : m_goalkeeperPosition + goalkeeperVelocity;
	}
}

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

Оценка производительности

Игра хорошо работает в системе разработки, но нужно попробовать ее и на менее мощном устройстве. Одно дело — разрабатывать игру на полноценном настольном компьютере с современным графическим процессором и 60 кадрами в секунду. Совсем другое дело — запускать игру на устройстве с процессором Intel® Atom™ и встроенным графическим адаптером.

Ваша игра должна показать хорошую производительность на обоих устройствах. Для измерения производительности можно использовать средства, входящие в состав Visual Studio или пакета Intel® Graphics Performance Analyzers (Intel® GPA), содержащего анализаторы производительности графической подсистемы, помогающие выявлять узкие места и повышать производительность игр. В Intel GPA можно получить наглядный анализ производительности игры и добиться повышения кадровой скорости.

Заключение

Итак, дело сделано. От танцующего чайника мы пришли к игре на DirectX с сенсорным управлением и управлением с помощью клавиатуры. Языки программирования становятся все более похожими, поэтому использование C++/DX не вызвало особых затруднений у разработчика, привыкшего пользоваться C#.

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

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

Благодарности

Большое спасибо Роберто Соннино (Roberto Sonnino) за его советы и техническое рецензирование этой статьи.

Изображения

Дополнительные сведения

Об авторе

Бруно Соннино — сотрудник корпорации Майкрософт уровня Most Valuable Professional (MVP), работает в Бразилии. Он выполняет обязанности разработчика и консультанта, а также является автором пяти книг про Delphi, опубликованных на португальском языке издательством Pearson Education Brazil, и ряда статей для бразильских и американских журналов и веб-сайтов.

 

Intel®Developer Zone offers tools and how-to information for cross-platform app development, platform and technology information, code samples, and peer expertise to help developers innovate and succeed.  Join our communities for the Internet of Things, Android*, Intel® RealSense™ Technology and Windows* to download tools, access dev kits, share ideas with like-minded developers, and participate in hackathons, contests, roadshows, and local events.

 

Intel, the Intel logo, Intel Atom, and Ultrabook are trademarks of Intel Corporation in the U.S. and/or other countries.
*Other names and brands may be claimed as the property of others.
Copyright © 2014. Intel Corporation. All rights reserved.


Viewing all articles
Browse latest Browse all 357

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>