3D game development for Windows 8 using C ++ and Microsoft DirectX

Original author: Bruno Sonnino
  • Transfer


Game development is an ongoing topic: everyone likes to play games, they are willing to buy them, so it’s profitable to sell them. But when developing good games, you should pay a lot of attention to performance. No one will like a game that slows down or works jerkyly even on less powerful devices.
In this article, I will show how to develop a simple 3D football game using Microsoft DirectX and C ++, although I mainly deal with C # development. In the past, I worked quite a lot with C ++, but now this language is not so simple for me. In addition, DirectX is new to me, so this article can be considered a beginner's point of view on game development. I ask experienced developers to forgive me for possible errors.

We will use the Microsoft Visual Studio 3D Starter Kit - a natural initial resource for everyone who wants to develop games for Windows 8.1.

Microsoft Visual Studio 3D Starter Kit

After downloading the Starter Kit, you can unzip it to a folder and open the StarterKit.sln file. This solution has a ready-made C ++ project for Windows 8.1. When it starts, an image similar to Fig. 1.


Figure 1. Initial state of Microsoft Visual Studio 3D Starter Kit

This program as part of the Starter Kit shows several useful elements.
  • Five objects are animated: four figures revolve around the teapot, and the teapot, in turn, “dances”.
  • Each item is made of a separate material; some are solid, and the surface of the cube is a bitmap.
  • The light source is in the upper left corner of the scene.
  • In the lower right corner of the screen is a frame rate counter (number of frames per second).
  • Above is the score indicator.
  • If you click on an object, it stands out and the number of points increases.
  • If you right-click the game screen or swipe the screen from the bottom edge to the middle, two buttons will appear for sequentially switching the color of the teapot.


The main game loop is in the StarterKitMain.cpp file , where the page and the frame rate counter are drawn. Game.cpp contains a game loop and click verification. In this file, the Update method computes the animation, and the Render method renders all the objects. The frame rate counter is drawn in SampleFpsTextRenderer.cpp . Game objects are in the Assets folder . Teapot.fbx is a teapot, and the GameLevel.fbx file contains four figures that revolve around a dancing teapot.
Now, having familiarized yourself with the sample application in the Starter Kit, you can proceed to create your own game.

Adding resources to the game

We are developing a soccer game, so our first resource should be a soccer ball, which we will add to Gamelevel.fbx . First you need to remove four shapes from this file by highlighting each of them and pressing the Delete button. In the solution explorer , delete the CubeUVImage.png file as well , because we don’t need it: this is the texture for the cube we just deleted.
Now add the sphere to the model. Open the tools (if they are not visible, click View> Toolbox) and double-click the sphere to add it to the model. We also need a stretched texture, such as in fig. 2.


Figure 2. Texture of a soccer ball adapted to a sphere

If you do not want to create your own models in Visual Studio, you can find ready-made models on the Internet. Visual Studio supports any model in the FBX, DAE and OBJ format: just add them to the solution resources. For example, you can use a .obj file like the one shown in fig. 3 (free model from TurboSquid website ). 


Figure 3. Three-dimensional ball OBJ model

Model animation

The model is ready, now it's time to animate it. But first you need to remove the kettle, because we do not need it. In the Assets folder, delete the teapot.fbx file . Now delete its download and animation. In the Game.cpp file , models are loaded asynchronously in CreateDeviceDependentResources :
The code
// 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(

It is necessary to change the model and remove the continuation of the task so that only the ball is loaded:
The code
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 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;
	});
}

The ReleaseDeviceDependentResources method only needs to clear the grids:
The code
void Game::ReleaseDeviceDependentResources()
{
	for (Mesh* m : m_meshModels)
	{
		delete m;
	}
	m_meshModels.clear();
	m_loadingComplete = false;
}

Now you need to change the Update method so that only the ball rotates:
The code
void Game::Update(DX::StepTimer const& timer)
{
	// Rotate scene.
	m_rotation = static_cast(timer.GetTotalSeconds()) * 0.5f;
}

A multiplier (0.5f) is used to control the rotation speed. To make the ball spin faster, you just need to increase this multiplier. For every second, the ball will rotate at an angle of 0.5 / (2 * Pi) radians. The Render method draws the ball with the desired rotation angle:
The code
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) 
{
}

We do not need to change the backlight of the ball, but we need to receive data on its touch. To do this, use the modified onHitobject method :
The code
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; }

If you start the project now, you will see that the ball rotates around its Y axis. Now we will set the ball in motion.

Ball movement

To move the ball, you need to move it, for example, up and down. First you need to declare a variable for the current position of the ball in Game.h :
The code
class Game
{
public:
// snip private:
// snip
float m_translation;

Then, in the Update method, you need to calculate the current position:
The code
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast(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;
}

Now the ball will rise and fall every 2 seconds. During the first second, the ball will rise; within the next second, it will fall. The Render method calculates the resulting matrix and draws the ball in a new position:
The code
void Game::Render() {
// snip
// Draw our scene models.
XMMATRIX rotation = XMMatrixRotationY(m_rotation);
rotation *= XMMatrixTranslation(0, m_translation, 0);

If you start the project now, you will see that the ball moves up and down at a constant speed. Now you need to give the ball physical properties.

Adding Ball Physics

To give the ball physical effects, you need to simulate the impact on it of a force representing gravity. If you remember the school physics course, then you know that the accelerated motion of the body is described by the following equations:
s = s 0 + v 0 t + 1 / 2at 2
v = v 0 + at

Where s is the position of the body at time t, s 0 is the initial position, v 0 - initial velocity, a - acceleration. For vertical movement, a is the acceleration of gravity (-10 m / s 2 ), and s 0 = 0 (first, the ball is on the ground, that is, at zero height). The equations turn into the following:
s = v 0 t-5t 2
v = v 0 -10t

We want to reach maximum height in 1 second. At the maximum height, the speed is 0. Therefore, the second equation allows you to find the initial speed:
0 = v 0 - 10 * 1 => v 0 = 10 m / s
This gives us the ball moving:
s = 10t - 5t 2
We need to change the Update method to use this equation:
The code
void Game::Update(DX::StepTimer consts timer) {
// Rotate scene.
m_rotation = static_cast(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
m_translation = 10*totalTime - 5 *totalTime*totalTime;
}

Now that the ball moves realistically up and down, it's time to add a soccer field.

Adding a soccer field

To add a football field, you need to create a new scene. In the Assets folder, right-click to add a new 3D scene and name it field.fbx . From the toolbox, add a plane and select it, change its size along the X axis to 107, and along the Z axis to 60. Set the properties of this plane Texture1 to the image of a football field.  
Now you can use the zoom tool (or press the Z key) to reduce the image.
Then you need to load the model into CreateDeviceDependentResources in Game.cpp :
The code
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;
});
}

When you start the program, you will see that the field jumps with the ball. For the field to stop moving, you need to change the Render method:
The code
// 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()); 
} 
}

With this change, the transformation applies only to the ball. The field is drawn without conversion. If you run the code now, you will see that the ball bounces off the field, but “falls” into it at the bottom. To correct this error, you need to transfer the field to -0.5 along the Y axis. Select the field and change its transfer along the Y axis to -0.5. Now, when the application starts, the ball will bounce off the field, as in fig. 4.


Figure 4. The ball bounces off the field.

Setting the position of the camera and the ball

The ball is located in the center of the field, but we do not need it there. In this game, the ball must be at the 11-meter mark. You should move the ball along the X axis by changing the movement of the ball in the Render method in Game.cpp :
rotation *= XMMatrixTranslation(63.0, m_translation, 0);

The ball moves 63 units along the X axis, that is, it is set at the 11-meter mark. After this change, you will no longer see the ball, because it is out of the field of view of the camera: the camera is installed in the center of the field and is aimed at the middle. It is necessary to change the position of the camera so that it is aimed at the goal line. This needs to be done in CreateWindowSizeDependentResources in the Game.cpp file :
The code
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);

Now the camera is between the midfield and the 11-meter mark and is directed towards the goal line. The new view is shown in fig. 5.


Figure 5. Changed ball position and new camera position

Adding a goal post

To add a gate to the field, you will need a new three-dimensional scene with a gate. You can create your own model or use the finished one. This model should be added to the Assets folder so that it can be compiled and used.
This model must be loaded in the CreateDeviceDependentResources method in the Game.cpp file :
The code
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
); 
});

After loading, set the position and draw in the Render method in Game.cpp :
The code
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); }

This change applies the transform to the gate and draws it. This transformation is a combination of three transformations: scaling (increasing the initial size by 2 times), rotating it by 90 degrees and moving by 85.5 units along the X axis and -0.5 units along the Y axis due to the depth of field. After that, the gates are set facing the field on the goal line. Please note that the order of transformations is important: if you apply rotation after moving, the gates will be drawn in a completely different place, and you will not see them.

Kick the ball

All elements are in place, but the ball is still bouncing. It's time to hit him. To do this, again apply physical skills. Kicking the ball looks something like the one shown in fig. 6.


Figure 6. The scheme of hitting the ball

Hitting the ball is carried out with an initial speed v 0 at an angle α (if you do not remember school physics lessons, play a little Angry Birds to see this principle in action). The movement of the ball can be decomposed into two different movements: horizontal - this is movement at a constant speed (we assume that there is no air resistance and wind), as well as vertical movement - the same as we used before. The equation of horizontal movement:
s X = s 0 + v 0* cos (α) * t
The equation of vertical movement:
s Y = s 0 + v 0 * sin (α) * t - 1/2 * g * t 2
Thus, we have two movements: one along the X axis, the other along the Y axis. If the strike is applied at an angle of 45 degrees, then cos (α) = sin (α) = sqrt (2) / 2 , so v 0 * cos (α) = v 0 * sin (a) * t . It is necessary for the ball to hit the goal, so the strike range must exceed 86 units (the distance to the goal line is 85.5). It is necessary that the flight of the ball takes 2 seconds. Substituting these values ​​in the first equation, we obtain:
86 = 63 + v 0 * cos (α) * 2> = v 0 * cos (α) = 23/2 = 11.5
If you replace the values ​​in the equation, then the equation of movement along the Y axis will be:
s Y = 0 + 11.5 * t-5 * t 2
A along the X axis - like this:
s X = 63 + 11.5 * t
The equation for the axis Y gives us time when the ball hits the ground again. To do this, you need to solve the quadratic equation (yes, I understand that you hoped to say goodbye to them forever after the school course of algebra, but nevertheless, here it is):
(-b ± sqrt (b 0 - 4 * a * c)) / 2 * a> = (-11.5 ± sqrt (11.52 - 4 * -5 * 0) / 2 * -5> = 0 or 23/10> = 2.3 with
These equations you can replace the displacement for the ball. First in Game.h, create variables to keep moving along three axes:
float m_translationX, m_translationY, m_translationZ;
Then in the Update method in Game.cpp add the equations:
The code
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast(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; 
}

The Render method uses these new moves:
rotation * = XMMatrixTranslation (m_translationX, m_translationY, 0);
If you start the program now, you will see how the ball flies into the middle of the goal. If you want the ball to move in other directions, you need to add a horizontal angle of impact. To do this, we use the movement along the Z axis.
The distance from the 11-meter mark to the goal is 22.5 units, and the distance between the goal bars is 14 units. This gives us the angle α = atan (7 / 22.5), i.e. 17 degrees. It is possible to calculate the movement along the Z axis, but it can be made simpler: the ball must move to the line at the same moment when it reaches the bar. This means that the ball must move 7 / 22.5 units along the Z axis and 1 unit along the X axis. The equation for the Z axis will be:
s z = 11.5 * t / 3.2 ≥ s z = 3.6 * t

This is the movement to the goal post. Any movement with a lower speed will have a smaller angle. For the ball to reach the goal, the speed must be from -3.6 (left bar) to 3.6 (right bar). Given that the ball must completely hit the goal, the maximum distance is 6 / 22.5, and the speed is from 3 to -3. Having these numbers, you can set the angle of impact in the Update method : 
The code
void Game::Update(DX::StepTimer consts timer)
{
// Rotate scene.
m_rotation = static_cast(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);
… .

The result should be approximately the same as in fig. 7.


Figure 7. Impact at an angle

Adding a goalkeeper

The movement of the ball is already ready, the goal is in place, now you need to add a goalkeeper who will catch the ball. In the role of goalkeeper, we will have a distorted cube. In the Assets folder, add a new element (a new three-dimensional scene) and name it goalkeeper.fbx .
Add a cube from the toolbox and select it. Set the scale: 0.3 on the X axis, 1.9 on the Y axis, and 1 on the Z axis. Set the MaterialAmbient property to 1 for red and 0 for blue and green to make the object red. 
Change the value of the Red property in the MaterialSpecular section to 1 and the value of the MaterialSpecularPower property to 0.2.
Load a new resource in the methodCreateDeviceDependentResources :
The code
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
); 
});

Now you need to place the goalkeeper in the middle of the goal and draw it. This needs to be done in the Render method in Game.cpp :
The code
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); 
} 
}

This code places the goalkeeper in the middle of the goal. Now you need to make sure that the goalkeeper can move left and right to catch the ball. To control the goalkeeper’s movement, the user will press the left and right arrow keys.
Goalkeeper movement is limited by goal bars located at a distance of +7 and -7 units along the Z axis. The goalkeeper's width is 1 unit in each direction, so it can move 6 units left or right.
A keystroke is intercepted on the XAML page ( Directxpage.xaml ) and redirected to the Game class . Add a KeyDown event handler to Directxpage.xaml :
The code

Event handler in DirectXPage.xaml.cpp :
The code
void DirectXPage::OnKeyDown(Platform::ObjectΛ sender, 
Windows::UI::Xaml::Input::KeyRoutedEventArgsΛ e) 
{
m_main->OnKeyDown( ->Key);
}

m_main is an instance of the StarterKitMain class that renders game scenes and a frame rate counter. You need to declare a public method in StarterKitMain.h :
The code
class StarterKitMain : public DX::IDeviceNotify
{
public:
StarterKitMain(const std::shared_ptr& 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); }
… .

This method redirects the key to the OnKeyDown method in the Game class . Now we need to declare a method OnKeyDown file Game.h :
The code
class Game
{ public:
Gamefconst std::shared_ptr& deviceResources);
void	CreateDeviceDependentResources();
void	CreateWindowSizeDependentResources();
void	ReleaseDeviceDependentResources();
void	Update(DX::StepTimer consts timer);
void	Render();
void	OnKeyDown(Windows::System::VirtualKey key);
…

This method handles keystrokes and moves the goalkeeper in the appropriate direction. Before creating this method, you need to declare a private field in the Game.h file to save the goalkeeper position:
The code
class Game {
// snip
private:
// snip
float m_goalkeeperPosition;

Initially, the goalkeeper takes position 0. This value will increase or decrease when the user presses the arrow key. If the position is greater than 6 or less than -6, the position of the goalkeeper does not change. This needs to be done in the OnKeyDown method in Game.cpp :
The code
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;
}

The new goalkeeper position is used in the Render method of the Game.cpp file , where the goalkeeper's movement is calculated:
auto goalkeeperTransform = XMMatrixTranslation (85.65f, 1.40f, m_goalkeeperPosition);
Applying these changes, you can start the game: you will see that the goalkeeper moves to the right or left when you press the corresponding arrow keys (see. Fig. 8).


Figure 8. Playing with the goalkeeper in the right position

Until now, the ball has been moving constantly, but we do not need this. The ball should begin to move immediately after a strike and stop when it reaches the goal. The goalkeeper must also not move before hitting the ball.
You need to declare a private field m_isAnimating file Game.hso that the game “knows” when the ball moves:
The code
class Game
{
public:
// snip
private:
// snip
bool m_isAnimating;

This variable is used in the Update and Render methods in Game.cpp , so the ball only moves when m_isAnimating is true:
The code
void Game::Update(DX::StepTimer consts timer) 
{
if (m_isAnimating) 
{
m_rotation = static_cast(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);
… .

The modelTransform variable moves from the loop to the beginning. Arrow keystrokes should be processed in the OnKeyDown method only when m_isAnimating is true:
The code
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;
} 
}

Now you need to hit the ball. This happens when the user presses the spacebar. Declare a new private field m_isKick file Game.h :
The code
class Game
{
public:
// snip
private:
// snip
bool m_isKick;

Set this field to true in the OnKeyDown method in Game.cpp :
The code
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;
}

When m_isKick is true, the animation starts in the Update method :
The code
void Game::Update(DX::StepTimer consts timer)
{
if (m_isKick)
{
m_startTime = static_cast(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
}
if (m_isAnimating)
{
auto totalTime = static_cast(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(); 
} 
}

The initial strike time is stored in the variable m_startTime (declared as a private field in the Game.h file ), which is used to calculate the strike time. If it exceeds 2.3 seconds, the game is reset (during which time the ball should already have reached the goal). The ResetGame method is declared private in Game.h :
The code
void Game::ResetGame() 
{
m_isAnimating = false;
m_goalkeeperPosition = 0; 
}

This method sets m_isAnimating to false and resets the goalkeeper. The position of the ball does not need to be changed: the ball will be drawn at the 11-meter mark if m_isAnimating is false. You also need to change the angle of impact. This code directs the blow near the right boom:
m_translationZ = 3.0f * totalTime;
This approach needs to be changed so that the hits are random and the user does not know where the next strike will be directed. You need to declare a private field m_ballAngle file Game.h and initialize it when you hit the ball in the method of the Update :
The code
void Game::Update(DX::StepTimer const& timer)
{
	if (m_isKick)
	{
		m_startTime = static_cast(timer.GetTotalSeconds());
		m_isAnimating = true;
		m_isKick = false;
		m_ballAngle = (static_cast  (rand()) / 
			static_cast  (RAND_MAX) -0.5f) * 6.0f; 
	}
… .

Rand () / RAND_MAX gives a result from 0 to 1. Subtract 0.5 from the result to get a number from -0.5 to 0.5, and then multiply by 6 to get the final angle to -3 to 3. To in each game to use different sequences, you need to initialize the generator by calling srand in the CreateDeviceDependentResources method :
The code
void Game::CreateDeviceDependentResources()
{
	srand(static_cast  (time(0)));
… .

To call the time function, you need to enable ctime . To use the new angle for the ball, you need to use m_ballAngle in the Update method :
m_translationZ = m_ballAngle * totalTime;
Now almost the entire code is ready, but you need to understand if the goalkeeper caught the ball, or the user scored a goal. This can be determined in a simple way: check whether the rectangle of the ball intersects with the rectangle of the goalkeeper when the ball reaches the goal line. Of course, more sophisticated methods can be used to determine goals scored, but for our case the described method is quite enough. All calculations are performed in the Update method : 
The code
void Game::Update(DX::StepTimer consts timer) {
if (m_isKick) {
m_startTime = static_cast(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
m_isGoal = m_isCaught = false;
m_ballAngle = (static_cast  (rand()) /
static_cast  (RAND_MAX) -0.5f) * 6.0f;
}
if (m_isAnimating)
{
auto totalTime = static_cast(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(); 
} 
}

We declare two private fields in the Game.h file : m_isGoal and m_IsCaught . These fields tell us what happened: the user scored a goal or the goalkeeper caught the ball. If both fields are false, the ball is still flying. When the ball reaches the goalkeeper, the program calculates the boundaries of the ball and the goalkeeper and determines whether the boundaries of the ball are superimposed on the goalkeeper's borders. If you look into the code, you will see that I added 7.0 f to each border. I did this because the borders can be positive or negative, and this will complicate the calculation of the overlay. By adding 7.0 f, I made sure that all the values ​​became positive in order to simplify the calculation. If the ball is caught, its position is set in the center of the goalkeeper. m_isGoal and m_IsCaughtare reset upon impact.

So, we got a very working version of the game. Those wishing to continue to improve it, in particular, add account management or touch control, we refer to the source material on the IDZ .

Conclusion

So the thing is done. From the dancing teapot we came to the game on DirectX. Programming languages ​​are becoming more and more similar, so using C ++ / DX did not cause any particular difficulties for a developer who was accustomed to using C #.
The main difficulty is the development of three-dimensional models, in their movement and location in the usual way. This required the use of knowledge of physics, geometry, trigonometry and mathematics.
Be that as it may, we can conclude that the development of the game is not an impossible task. With the patience and the right tools, you can create great games with superior performance.

Also popular now: