HIGHLIGHTS
In this article, we show you how to develop a soccer penalties shootout game. We’ll be using the Microsoft Visual Studio 3D Starter Kit, a logical starting point for those who want to develop games for Windows 8.1.
Game development is a perennial hot topic: everybody loves to play games, and they are the top sellers on every list. But when you talk about developing a good game, performance is always a requirement. No one likes to play a game that lags or has glitches, even on low-end devices.
You can use many languages and frameworks to develop a game, but when you want performance for a Windows* game, nothing beats the real thing: Microsoft DirectX* with C++. With these technologies, you’re close to the bare metal, you can use the full capabilities of the hardware, and you get excellent performance.
I decided to develop such a game, even though I’m mostly a C# developer. I have developed a lot in C++ in the past, but the language now is far from what it used to be. In addition, DirectX is a new subject for me, so this article is about developing games from a newbie standpoint. Experienced developers will have to excuse my mistakes.
In this article, I show you how to develop a soccer penalties shootout game. The game kicks the ball, and the user moves the goalkeeper to catch it. We won’t be starting from scratch. We’ll be using the Microsoft Visual Studio* 3D Starter Kit, a logical starting point for those who want to develop games for Windows 8.1.
The Microsoft Visual Studio* 3D Starter Kit
After you download the Starter Kit, you can extract it to a folder and open the StarterKit.sln file. This solution has a Windows 8.1 C++ project ready to run. If you run it, you will see something like Figure 1.
Figure 1. Microsoft Visual Studio* 3D Starter Kit initial state
This Starter Kit program demonstrates several useful concepts:
- Five objects are animated: four shapes turning around the teapot and the teapot “dancing.”
- Each element has a different material: some are solid colors, and the cube has a bitmap material.
- Lighting comes from the top left of the scene.
- The bottom right corner contains a frames-per-second (FPS) counter.
- A score indicator is positioned at the top.
- Clicking an object highlights it, and the score is incremented.
- Right-clicking the game or swiping from the bottom calls up a bottom app bar with two buttons to cycle the color of the teapot.
You can use these features to create any game, but first you want to look at the files in the kit.
Let’s start with App.xaml and its cpp/h counterparts. When you start the App.xaml application, it runsDirectXPage. In DirectXPage.xaml, you have a SwapChainPanel and the app bar. TheSwapChainPanel is a hosting surface for DirectX graphics on an XAML page. There, you can add XAML objects that will be presented with the Microsoft Direct3D* scene—this is convenient for adding buttons, labels, and other XAML objects to a DirectX game without having to create your own controls from scratch. The Starter Kit also adds a StackPanel, which you will use as a scoreboard.
DirectXPage.xaml.cpp has the initialization of the variables, hooking the event handlers for resizing and changing orientation, the handlers for the Click event of app bar buttons, and the rendering loop. All the XAML objects are handled like any other Windows 8 program. The file also processes the Tapped event, checking whether a tap (or mouse click) hits an object. If it does, the event increments the score for that object.
You must tell the program that the SwapChainPanel should render the DirectX content. To do that, according to the documentation, you must “cast the SwapChainPanel instance to IInspectable orIUnknown, then call QueryInterface to obtain a reference to the ISwapChainPanelNative interface (this is the native interface implementation that is the complement to the SwapChainPanel and enables the interop bridge). Then, call ISwapChainPanelNative::SetSwapChain on that reference to associate your implemented swap chain with the SwapChainPanel instance.” This is done in theCreateWindowSizeDependentResources method in DeviceResources.cpp.
The main game loop is in StarterKitMain.cpp where the page and the FPS counter are rendered.
Game.cpp has the game loop and hit testing. It calculates the animation in the Update method and draws all objects in the Render method. The FPS counter is rendered in SampleFpsTextRenderer.cpp.
The objects of the game are in the Assets folder. Teapot.fbx has the teapot, and GameLevel.fbx has the four shapes that spin around the dancing teapot.
With this basic knowledge of the Starter Kit sample app, you can start to create your own game.
Add Assets to the Game
You’re developing a soccer game, so the first asset you need is a soccer ball, which you add toGamelevel.fbx. First, remove the four shapes from this file by selecting each and pressing Delete. In the Solution Explorer, delete CubeUVImage.png as well because you won’t need it; it’s the texture used to cover the cube, which you just deleted.
The next step is to add a sphere to the model. Open the toolbox (if you don’t see it, click View > Toolbox) and double-click the sphere to add it to the model. If the ball seems too small, you can zoom in by clicking the second button in the editor’s top toolbar, pressing Z to zoom with the mouse (dragging to the center increases the zoom), or tapping the up and down arrows. You can also press Ctrl and use the mouse wheel to zoom. You should have something like Figure 2.
Figure 2. Model editor with a sphere shape
This sphere has just a white color with some lighting on it. It needs a soccer ball texture. My first attempt was to use a hexagonal grid, like the one shown in Figure 3.
Figure 3. Hexagonal grid for the ball texture: first attempt
To apply the texture to the sphere, select it and, in the Properties window, assign the .png file to theTexture1 property. Although that seemed like a good idea, the result was less than good, as you can see in Figure 4.
Figure 4. Sphere with texture applied
The hexagons are distorted because of the projections of the texture points in the sphere. You need a distorted texture, like the one in Figure 5.
Figure 5. Soccer ball texture adapted to the sphere
When you apply this texture, the sphere starts to look more like a soccer ball. You just need to adjust some properties to make it more real. To do that, select the ball and edit the Phong effect in the Properties window. The Phong lighting model includes directional and ambient lighting and simulates the reflective properties of the object. This is a shader included with Visual Studio that you can drag from the toolbox. If you want to know more about shaders and how to design them using the Visual Studio shader designer, click the link in “For More Information.” Set the Red, Green, and Blue properties under MaterialSpecular to 0.2 and MaterialSpecularPower to 16. Now you have a better-looking soccer ball (Figure 6).
Figure 6. Finished soccer ball
If you don’t want to design your own models in Visual Studio, you can grab a premade model from the Web. Visual Studio accepts any model in the FBX, DAE, and OBJ formats: you just need to add them to your assets in the solution. As an example, you can use an .obj file like the one in Figure 7 (a free model downloaded from http://www.turbosquid.com).
Figure 7. Three-dimensional .obj ball model
Animate the Model
With the model in place, it’s time to animate it. Before that, though, I want to remove the teapot since it won’t be needed. In the Assets folder, delete teapot.fbx. Next, delete its loading and animation. InGame.cpp, the loading of the models is done asynchronously in CreateDeviceDependentResources:
01 | // Load the scene objects. |
02 | auto loadMeshTask = Mesh::LoadFromFileAsync( |
03 | m_graphics, |
04 | L"gamelevel.cmo", |
05 | L"", |
06 | L"", |
07 | m_meshModels) |
08 | .then([this]() |
09 | { |
10 | // Load the teapot from a separate file and add it to the vector of meshes. |
11 | return Mesh::LoadFromFileAsync( |
You must change the model and remove the continuation of the task, so only the ball is loaded:
01 | void Game::CreateDeviceDependentResources() |
03 | m_graphics.Initialize(m_deviceResources->GetD3DDevice(), m_deviceResources->GetD3DDeviceContext(), m_deviceResources->GetDeviceFeatureLevel()); |
05 | // Set DirectX to not cull any triangles so the entire mesh will always be shown. |
06 | CD3D11_RASTERIZER_DESC d3dRas(D3D11_DEFAULT); |
07 | d3dRas.CullMode = D3D11_CULL_NONE; |
08 | d3dRas.MultisampleEnable = true; |
09 | d3dRas.AntialiasedLineEnable = true; |
11 | ComPtr<ID3D11RasterizerState> p3d3RasState; |
12 | m_deviceResources->GetD3DDevice()->CreateRasterizerState(&d3dRas, &p3d3RasState); |
13 | m_deviceResources->GetD3DDeviceContext()->RSSetState(p3d3RasState.Get()); |
15 | // Load the scene objects. |
16 | auto loadMeshTask = Mesh::LoadFromFileAsync( |
24 | (loadMeshTask).then([this]() |
26 | // Scene is ready to be rendered. |
27 | m_loadingComplete = true; |
Its counterpart, ReleaseDeviceDependentResources, needs only to clear the meshes:
01 | void Game::ReleaseDeviceDependentResources() |
03 | for (Mesh* m : m_meshModels) |
09 | m_loadingComplete = false; |
The next step is to change the Update method so that only the ball is rotated:
1 | void Game::Update(DX::StepTimer const& timer) |
4 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
You manipulate the speed of the rotation through the multiplier (0.5f). If you want the ball to rotate faster, just make this multiplier larger. This means that the ball will rotate at an angle of 0.5/(2 * pi) radians every second. The Render method renders the ball at the desired rotation:
03 | // Loading is asynchronous. Only draw geometry after it's loaded. |
04 | if (!m_loadingComplete) |
09 | auto context = m_deviceResources->GetD3DDeviceContext(); |
11 | // Set render targets to the screen. |
12 | auto rtv = m_deviceResources->GetBackBufferRenderTargetView(); |
13 | auto dsv = m_deviceResources->GetDepthStencilView(); |
14 | ID3D11RenderTargetView *const targets[1] = { rtv }; |
15 | context->OMSetRenderTargets(1, targets, dsv); |
17 | // Draw our scene models. |
18 | XMMATRIX rotation = XMMatrixRotationY(m_rotation); |
19 | for (UINT i = 0; i < m_meshModels.size(); i++) |
21 | XMMATRIX modelTransform = rotation; |
23 | String^ meshName = ref new String(m_meshModels[i]->Name()); |
25 | m_graphics.UpdateMiscConstants(m_miscConstants); |
27 | m_meshModels[i]->Render(m_graphics, modelTransform); |
ToggleHitEffect won’t do anything here; the ball won’t change the glow if it’s touched:
1 | void Game::ToggleHitEffect(String^ object) |
Although you don’t want the ball to change the glow, you still want to report that it’s been touched. To do that, use this modified OnHitObject:
01 | String^ Game::OnHitObject(int x, int y) |
03 | String^ result = nullptr; |
07 | m_graphics.GetCamera().GetWorldLine(x, y, &point, &dir); |
10 | XMMATRIX worldMat = XMMatrixRotationY(m_rotation); |
11 | XMStoreFloat4x4(&world, worldMat); |
13 | float closestT = FLT_MAX; |
14 | for (Mesh* m : m_meshModels) |
16 | XMFLOAT4X4 meshTransform = world; |
18 | auto name = ref new String(m->Name()); |
21 | bool hit = HitTestingHelpers::LineHitTest(*m, &point, &dir, &meshTransform, &t); |
22 | if (hit && t < closestT) |
Now you can run the project and see that the ball is spinning on its y-axis. Now, let’s make the ball move.
Move the Ball
To move the ball, you need to translate it, for example, make it move up and down. The first thing to do is declare the variable for the current position in Game.h:
Then, in the Update method, calculate the current position:
1 | void Game::Update(DX::StepTimer const& timer) |
4 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
5 | const float maxHeight = 7.0f; |
6 | auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f); |
7 | m_translation = totalTime > 1.0f ? |
8 | maxHeight – (maxHeight * (totalTime – 1.0f)) : maxHeight *totalTime; |
This way, the ball will go up and down every 2 seconds. In the first second, it will move up; in the next, down. The Render method calculates the resulting matrix and renders the ball at the new position:
5 | // Draw our scene models. |
6 | XMMATRIX rotation = XMMatrixRotationY(m_rotation); |
7 | rotation *= XMMatrixTranslation(0, m_translation, 0); |
If you run the project now, you will see that the ball is moving up and down at a constant speed. Now, you need to add some physics to the ball.
Add Physics to the Ball
To add some physics to the ball, you must simulate a force on it, representing gravity. From your physics lessons (you do remember them, don’t you?), you know that an accelerated movement follows these equations:
s = s0 + v0t + 1/2at2
v = v0 + at
where s is the position at the instant t, s0 is the initial position, v0 is the initial velocity, and a is the acceleration. For the vertical movement, a is the acceleration caused by gravity (−10 m/s2) and s0 is 0 (the ball starts on the floor). So, the equations become:
s = v0t -5t2
v = v0 -10t
You want to reach the maximum height in 1 second. At the maximum height, the velocity is 0. So, the second equation allows you to find the initial velocity:
0 = v0 – 10 * 1 => v0 = 10 m/s
And that gives you the translation for the ball:
s = 10t – 5t2
You modify the Update method to use this equation:
1 | void Game::Update(DX::StepTimer const& timer) |
4 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
5 | auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f); |
6 | m_translation = 10*totalTime – 5 *totalTime*totalTime; |
Now that the ball moves up and down realistically it’s time to add the soccer field.
Add the Soccer Field
To add the soccer field, you must create a new scene. In the Assets folder, right-click to add a new three-dimensional (3D) scene and name it field.fbx. From the toolbox, add a plane and select it, changing its scale X to 107 and Z to 60. Set its Texture1 property to a soccer field image. You can use the zoom tool (or press Z) to zoom out.
Then, you must load the mesh in CreateDeviceDependentResources in Game.cpp:
01 | void Game::CreateDeviceDependentResources() |
05 | // Load the scene objects. |
06 | auto loadMeshTask = Mesh::LoadFromFileAsync( |
14 | return Mesh::LoadFromFileAsync( |
20 | false // Do not clear the vector of meshes |
24 | (loadMeshTask).then([this]() |
26 | // Scene is ready to be rendered. |
27 | m_loadingComplete = true; |
If you run the program, you’ll see that the field bounces with the ball. To stop the field from moving, change the Render method:
01 | // Renders one frame using the Starter Kit helpers. |
06 | for (UINT i = 0; i < m_meshModels.size(); i++) |
08 | XMMATRIX modelTransform = rotation; |
10 | String^ meshName = ref new String(m_meshModels[i]->Name()); |
12 | m_graphics.UpdateMiscConstants(m_miscConstants); |
14 | if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0) |
15 | m_meshModels[i]->Render(m_graphics, modelTransform); |
17 | m_meshModels[i]->Render(m_graphics, XMMatrixIdentity()); |
With this change, the transform is applied only to the ball. The field is rendered with no transform. If you run the code now, you will see that the ball bounces in the field, but it “enters” it at the bottom. Fix this bug by translating the field by −0.5 in the y-axis. Select the field and change its translation property on the y-axis to −0.5. Now, when you run the app, you can see the ball bouncing in the field, like Figure 8.
Figure 8. Ball bouncing in the field
Set the Camera and Ball Position
The ball is positioned at the center of the field, but you don’t want it there. For this game, the ball must be positioned at the penalty mark. If you look at the scene editor in Figure 9, you can see that to do that, you must translate the ball in the x-axis by changing the translation of the ball in the Render method inGame.cpp:
1 | rotation *= XMMatrixTranslation(63.0, m_translation, 0); |
The ball is translated by 63 units in the x-axis, which positions it at the penalty mark.
Figure 9. Field with Axis – X (red) and Z (blue)
With this change, you won’t see the ball anymore because the camera is positioned in another direction—at the middle of the field, looking at the center. You need to reposition the camera so that it points toward the goal line, which you do in CreateWindowSizeDependentResources in Game.cpp:
01 | m_graphics.GetCamera().SetViewport((UINT) outputSize.Width, (UINT) outputSize.Height); |
02 | m_graphics.GetCamera().SetPosition(XMFLOAT3(25.0f, 10.0f, 0.0f)); |
03 | m_graphics.GetCamera().SetLookAt(XMFLOAT3(100.0f, 0.0f, 0.0f)); |
04 | float aspectRatio = outputSize.Width / outputSize.Height; |
05 | float fovAngleY = 30.0f * XM_PI / 180.0f; |
07 | if (aspectRatio < 1.0f) |
09 | // Portrait or snap view |
10 | m_graphics.GetCamera().SetUpVector(XMFLOAT3(1.0f, 0.0f, 0.0f)); |
11 | fovAngleY = 120.0f * XM_PI / 180.0f; |
16 | m_graphics.GetCamera().SetUpVector(XMFLOAT3(0.0f, 1.0f, 0.0f)); |
18 | m_graphics.GetCamera().SetProjection(fovAngleY, aspectRatio, 1.0f, 100.0f); |
The position of the camera is between the center mark and the penalty mark, looking at the goal line. The new view is similar to Figure 10.
Figure 10. Ball repositioned with new camera position
Now, you need to add the goal.
Add the Goal Post
To add the goal to the field, you need a new 3D scene with the goal. You can design your own, or you can get a model ready to use. With the model in place, you must add it to the Assets folder so that it can be compiled and used.
The model must be loaded in the CreateDeviceDependentResources method in Game.cpp:
01 | auto loadMeshTask = Mesh::LoadFromFileAsync( |
09 | return Mesh::LoadFromFileAsync( |
15 | false // Do not clear the vector of meshes |
19 | return Mesh::LoadFromFileAsync( |
25 | false // Do not clear the vector of meshes |
Once loaded, position and draw it in the Render method in Game.cpp:
01 | auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5, 0); |
03 | for (UINT i = 0; i < m_meshModels.size(); i++) |
05 | XMMATRIX modelTransform = rotation; |
07 | String^ meshName = ref new String(m_meshModels[i]->Name()); |
09 | m_graphics.UpdateMiscConstants(m_miscConstants); |
11 | if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0) |
12 | m_meshModels[i]->Render(m_graphics, modelTransform); |
13 | else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0) |
14 | m_meshModels[i]->Render(m_graphics, XMMatrixIdentity()); |
16 | m_meshModels[i]->Render(m_graphics, goalTransform); |
This change applies a transform to the goal and renders it. The transform is a combination of three transforms: a scale (multiplying the original size by 2), a rotation of 90 degrees, and a translation of 85.5 units in the x-axis and −0.5 units in the y-axis because of the displacement that you gave to the field. That way, the goal is positioned facing the field, at the goal line, as in Figure 11. Note that the order of the transforms is important: if you apply the rotation after the translation, the goal will be rendered in a completely different position, and you won’t see anything.
Figure 11. Field with goal positioned
Shooting the Ball
All the elements are positioned, but the ball is still jumping. It’s time to kick it. To do that, you must sharpen your physics skills again. The kick of the ball looks something like Figure 12.
Figure 12. Schematics of a ball kick
The ball is kicked with an initial velocity of v0, with an α angle (if you don’t remember your physics classes, just play a bit of Angry Birds* to see this in action). The movement of the ball can be decomposed in two different movements: the horizontal movement is a movement with constant velocity (I admit that there is neither air friction nor wind effects), and the vertical movement is like the one used before. The horizontal movement equation is:
sX = s0 + v0*cos(α)*t
. . . and the vertical movement is:
sY = s0 + v0*sin(α)*t – ½*g*t2
Now you have two translations: one in the x-axis and other in the y-axis. Considering that the kick is at 45 degrees, cos(α) = sin(α) = sqrt(2)/2, so v0*cos(α) = v0*sin(α)*t. You want the kick to enter the goal, so the distance must be greater than 86 (the goal line is at 85.5). You want the ball flight to take 2 seconds, so when you substitute these values in the first equation, you get:
86 = 63 + v0 * cos(α) * 2 ≥ v0 * cos(α) = 23/2 = 11.5
Replacing the values in the equations, the translation equation for the y-axis is:
sY = 0 + 11.5 * t – 5 * t2
. . . and for the x-axis:
sX = 63 + 11.5 * t
With the y-axis equation, you know the time the ball will hit the ground again, using the solution for the second degree equation (yes, I know you thought you would never use it, but here it is):
(−b ± sqrt(b2 − 4*a*c))/2*a ≥ (−11.5 ± sqrt(11.52 – 4 * −5 * 0)/2 * −5 ≥ 0 or 23/10 ≥ 2.3s
With these equations, you can replace the translation for the ball. First, in Game.h, create variables to store the translation in the three axes:
1 | float m_translationX, m_translationY, m_translationZ; |
Then, in the Update method in Game.cpp, add the equations:
1 | void Game::Update(DX::StepTimer const& timer) |
4 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
5 | auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f); |
6 | m_translationX = 63.0 + 11.5 * totalTime; |
7 | m_translationY = 11.5 * totalTime – 5 * totalTime*totalTime; |
The Render method uses these new translations:
1 | rotation *= XMMatrixTranslation(m_translationX, m_translationY, 0); |
If you run the program now, you will see the goal with the ball entering the center of it. If you want the ball to go in other directions, you must add a horizontal angle for the kick. You do this with a translation in thez-axis.
Figure 13 shows the distance between the penalty mark and the goal is 22.5, and the distance between goal posts is 14. That makes α = atan(7/22.5), or 17 degrees. You could calculate the translation in the z-axis, but to make it simpler: the ball must travel to the goal line at the same time it reaches the goal post. That means it must travel 7/22.5 units in the z-axis while the ball travels 1 unit in the x-axis. So, the equation for the z-axis is:
sz = 11.5 * t/3.2 ≥ sz = 3.6 * t
Figure 13. Schematics of the distance to the goal
This is the translation to reach the goal post. Any translation with a lower velocity will have a smaller angle. So to reach the goal, the velocity must be between −3.6 (left post) and 3.6 (right post). If you consider that the ball must enter the goal entirely, the maximum distance is 6/22.5, and the velocity range is between 3 and −3. With these numbers, you can set an angle for the kick with this code in the Update method:
1 | void Game::Update(DX::StepTimer const& timer) |
4 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
5 | auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f); |
6 | m_translationX = 63.0 + 11.5 * totalTime; |
7 | m_translationY = 11.5 * totalTime – 5 * totalTime*totalTime; |
8 | m_translationZ = 3 * totalTime; |
The translation in the z-axis will be used in the Render method:
1 | rotation *= XMMatrixTranslation(m_translationX, m_translationY, m_translationZ); |
You should have something like Figure 14.
Figure 14. Kick at an angle
Add a Goalkeeper
With the ball movement and goal in place, you now need to add a goalkeeper to catch the ball. The goalkeeper will be a distorted cube. In the Assets folder, add a new item—a new 3D scene—and call itgoalkeeper.fbx.
Add a cube from the toolbox and select it. Set its scale to 0.3 in the x-axis, 1.9 in the y-axis, and 1 in the z-axis. Change its MaterialAmbient property to 1 for the Red and 0 for the Blue and Green properties to make it Red. Change the Red property in MaterialSpecular to 1 and MaterialSpecularPower to 0.2.
Load the new resource in the CreateDeviceDependentResources method:
01 | auto loadMeshTask = Mesh::LoadFromFileAsync( |
09 | return Mesh::LoadFromFileAsync( |
15 | false // Do not clear the vector of meshes |
19 | return Mesh::LoadFromFileAsync( |
25 | false // Do not clear the vector of meshes |
29 | return Mesh::LoadFromFileAsync( |
35 | false // Do not clear the vector of meshes |
The next step is to position and render the goalkeeper in the center of the goal. You do this in the Rendermethod of Game.cpp:
05 | auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5f, 0); |
06 | auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.4f, 0); |
08 | for (UINT i = 0; i < m_meshModels.size(); i++) |
10 | XMMATRIX modelTransform = rotation; |
12 | String^ meshName = ref new String(m_meshModels[i]->Name()); |
14 | m_graphics.UpdateMiscConstants(m_miscConstants); |
16 | if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0) |
17 | m_meshModels[i]->Render(m_graphics, modelTransform); |
18 | else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0) |
19 | m_meshModels[i]->Render(m_graphics, XMMatrixIdentity()); |
20 | else if (String::CompareOrdinal(meshName, L"Cube_Node") == 0) |
21 | m_meshModels[i]->Render(m_graphics, goalkeeperTransform); |
23 | m_meshModels[i]->Render(m_graphics, goalTransform); |
With this code, the goalkeeper is positioned at the center of the goal, as shown in Figure 15 (note that the camera position is different for the screenshot).
Figure 15. The goalkeeper at the center of the goal
Now, you need to make the keeper move to the right and the left to catch the ball. The user will use the left and right arrow keys to change the keeper’s movement.
The movement of the goalkeeper is limited by the goal posts, positioned at +7 and −7 units in the z-axis. The goalkeeper has 1 unit in both directions, so it can be moved 6 units on either side.
The key press is intercepted in the XAML page (DirectXPage.xaml) and will be redirected to the Gameclass. You add a KeyDown event handler in DirectXPage.xaml:
2 | x:Class="StarterKit.DirectXPage" |
5 | xmlns:local="using:StarterKit" |
8 | mc:Ignorable="d" KeyDown="OnKeyDown"> |
The event handler in DirectXPage.xaml.cpp is:
1 | void DirectXPage::OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e) |
3 | m_main->OnKeyDown(e->Key); |
m_main is the instance of the StarterKitMain class, which renders the game and the FPS scenes. You must declare a public method in StarterKitMain.h:
01 | class StarterKitMain : public DX::IDeviceNotify |
04 | StarterKitMain(const std::shared_ptr<DX::DeviceResources>& deviceResources); |
07 | // Public methods passed straight to the Game renderer. |
08 | Platform::String^ OnHitObject(int x, int y) { |
09 | return m_sceneRenderer->OnHitObject(x, y); } |
10 | void OnKeyDown(Windows::System::VirtualKey key) { |
11 | m_sceneRenderer->OnKeyDown(key); } |
This method redirects the key to the OnKeyDown method in the Game class. Now, you must declare theOnKeyDown method in Game.h:
04 | Game(const std::shared_ptr<DX::DeviceResources>& deviceResources); |
05 | void CreateDeviceDependentResources(); |
06 | void CreateWindowSizeDependentResources(); |
07 | void ReleaseDeviceDependentResources(); |
08 | void Update(DX::StepTimer const& timer); |
10 | void OnKeyDown(Windows::System::VirtualKey key); |
This method processes the key pressed and moves the goalkeeper with the arrows. Before creating the method, you must declare a private field in Game.h that stores the goalkeeper’s position:
8 | float m_goalkeeperPosition; |
The goalkeeper position is initially 0 and will be incremented or decremented when the user presses an arrow key. If the position is larger than 6 or smaller than −6, the goalkeeper’s position won’t change. You do this in the OnKeyDown method in Game.cpp:
01 | void Game::OnKeyDown(Windows::System::VirtualKey key) |
03 | const float MaxGoalkeeperPosition = 6.0; |
04 | const float MinGoalkeeperPosition = -6.0; |
05 | if (key == Windows::System::VirtualKey::Right) |
06 | m_goalkeeperPosition = m_goalkeeperPosition >= MaxGoalkeeperPosition ? |
07 | m_goalkeeperPosition : m_goalkeeperPosition + 0.1f; |
08 | else if (key == Windows::System::VirtualKey::Left) |
09 | m_goalkeeperPosition = m_goalkeeperPosition <= MinGoalkeeperPosition ? |
10 | m_goalkeeperPosition : m_goalkeeperPosition – 0.1f; |
The new goalkeeper position is used in the Render method of Game.cpp, where the goalkeeper transform is calculated:
1 | auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.40f, m_goalkeeperPosition); |
With these changes, you can run the game and see that the goalkeeper moves to the right and the left when you press the arrow keys (Figure 16).
Figure 16. Game with the goalkeeper in position
Until now, the ball keeps moving all the time, but that’s not what you want. The ball should move just after it’s kicked and stop when it reaches the goal. Similarly, the goalkeeper shouldn’t move before the ball is kicked.
You must declare a private field, m_isAnimating in Game.h, so the game knows when the ball is moving:
This variable is used in the Update and Render methods in Game.cpp so the ball moves only whenm_isAnimating is true:
01 | void Game::Update(DX::StepTimer const& timer) |
05 | m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f; |
06 | auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f); |
07 | m_translationX = 63.0f + 11.5f * totalTime; |
08 | m_translationY = 11.5f * totalTime – 5.0f * totalTime*totalTime; |
09 | m_translationZ = 3.0f * totalTime; |
17 | XMMATRIX modelTransform; |
20 | modelTransform = XMMatrixRotationY(m_rotation); |
21 | modelTransform *= XMMatrixTranslation(m_translationX, |
22 | m_translationY, m_translationZ); |
25 | modelTransform = XMMatrixTranslation(63.0f, 0.0f, 0.0f); |
The variable modelTransform is moved from the loop to the top. The arrow keys should only be processed in the OnKeyDown method when m_isAnimating is true:
01 | void Game::OnKeyDown(Windows::System::VirtualKey key) |
03 | const float MaxGoalkeeperPosition = 6.0f; |
07 | auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ? |
10 | m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= MaxGoalkeeperPosition ? |
11 | m_goalkeeperPosition : |
12 | m_goalkeeperPosition + goalKeeperVelocity; |
The next step is to kick the ball. This happens when the user presses the space bar. Declare a new private field, m_isKick, in Game.h:
Set this field to true in the OnKeyDown method in Game.cpp:
01 | void Game::OnKeyDown(Windows::System::VirtualKey key) |
03 | const float MaxGoalkeeperPosition = 6.0f; |
07 | auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ? |
10 | m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= MaxGoalkeeperPosition ? |
11 | m_goalkeeperPosition : |
12 | m_goalkeeperPosition + goalKeeperVelocity; |
14 | else if (key == Windows::System::VirtualKey::Space) |
When m_isKick is true, the animation starts in the Update method:
01 | void Game::Update(DX::StepTimer const& timer) |
05 | m_startTime = static_cast<float>(timer.GetTotalSeconds()); |
11 | auto totalTime = static_cast<float>(timer.GetTotalSeconds()) – m_startTime; |
12 | m_rotation = totalTime * 0.5f; |
13 | m_translationX = 63.0f + 11.5f * totalTime; |
14 | m_translationY = 11.5f * totalTime – 5.0f * totalTime*totalTime; |
15 | m_translationZ = 3.0f * totalTime; |
The initial time for the kick is stored in the variable m_startTime (declared as a private field in Game.h), which is used to compute the time for the kick. If it’s above 2.3 seconds, the game is reset (the ball should have reached the goal). You declare the ResetGame method as private in Game.h:
4 | m_goalkeeperPosition = 0; |
This method sets m_isAnimating to false and resets the goalkeeper’s position. The ball doesn’t need to be repositioned because it will be drawn in the penalty mark if m_isAnimating is false. Another change you need to make is the kick angle. This code fixes the kick near the right post:
1 | m_translationZ = 3.0f * totalTime; |
You must change it so that the kick is somewhat random and the user doesn’t know where it will be. You must declare a private field m_ballAngle in Game.h and initialize it when the ball is kicked in the Updatemethod:
01 | void Game::Update(DX::StepTimer const& timer) |
05 | m_startTime = static_cast<float>(timer.GetTotalSeconds()); |
08 | m_ballAngle = (static_cast <float> (rand()) / |
09 | static_cast <float> (RAND_MAX) -0.5f) * 6.0f; |
Rand()/RAND_MAX results in a number between 0 and 1. Subtract 0.5 from the result so the number is between −0.5 and 0.5 and multiply per 6, so the final angle is between −3 and 3. To use different sequences every game, you must initialize the generator by calling srand in theCreateDeviceDependentResources method:
1 | void Game::CreateDeviceDependentResources() |
3 | srand(static_cast <unsigned int> (time(0))); |
To call the time function, you must include ctime. You use m_ballAngle in the Update method to use the new angle for the ball:
1 | m_translationZ = m_ballAngle * totalTime; |
Most of the code is now in place, but you must know whether the goalkeeper caught the ball or the user scored a goal. Use a simple method to find out: when the ball reaches the goal line, you check whether the ball rectangle intersects the goalkeeper rectangle. If you want, you can use more complex methods to determine a goal, but for our needs, this is enough. All the calculations are made in the Update method:
01 | void Game::Update(DX::StepTimer const& timer) |
05 | m_startTime = static_cast<float>(timer.GetTotalSeconds()); |
08 | m_isGoal = m_isCaught = false; |
09 | m_ballAngle = (static_cast <float> (rand()) / |
10 | static_cast <float> (RAND_MAX) -0.5f) * 6.0f; |
14 | auto totalTime = static_cast<float>(timer.GetTotalSeconds()) – m_startTime; |
15 | m_rotation = totalTime * 0.5f; |
19 | m_translationX = 63.0f + 11.5f * totalTime; |
20 | m_translationY = 11.5f * totalTime – 5.0f * totalTime*totalTime; |
21 | m_translationZ = m_ballAngle * totalTime; |
25 | // if ball is caught, position it in the center of the goalkeeper |
26 | m_translationX = 83.35f; |
27 | m_translationY = 1.8f; |
28 | m_translationZ = m_goalkeeperPosition; |
30 | if (!m_isGoal && !m_isCaught && m_translationX >= 85.5f) |
32 | // ball passed the goal line – goal or caught |
33 | auto ballMin = m_translationZ – 0.5f + 7.0f; |
34 | auto ballMax = m_translationZ + 0.5f + 7.0f; |
35 | auto goalkeeperMin = m_goalkeeperPosition – 1.0f + 7.0f; |
36 | auto goalkeeperMax = m_goalkeeperPosition + 1.0f + 7.0f; |
37 | m_isGoal = (goalkeeperMax < ballMin || goalkeeperMin > ballMax); |
38 | m_isCaught = !m_isGoal; |
Declare two private fields in Game.h: m_isGoal and m_IsCaught. These fields tell you whether the user scored a goal or the goalkeeper caught the ball. If both are false, the ball is still travelling. When the ball reaches the goalkeeper, the program calculates the ball and the goalkeeper’s bounds and determines whether the bounds of the ball overlap with the bounds of the goalkeeper. If you look at the code, you will see that I added 7.0 f to every bound. I did this because the bounds can be positive or negative, and that would complicate the overlapping calculation. By adding 7.0 f, you ensure that all numbers are positive, which simplifies the calculation. If the ball is caught, the ball position is set to the center of the goalkeeper.m_isGoal and m_IsCaught are reset when there is a kick. Now, it’s time to add the scoreboard to the game.
Add Scorekeeping
In a DirectX game, you can render the score with Direct2D, but when you’re developing a Windows 8 game, you have another way to do it: using XAML. You can overlap XAML elements in your game and create a bridge between the XAML elements and your game logic. This is an easier way to show information and interact with the user, as you won’t have to deal with element positions, renders, and update loops.
The Starter Kit comes with an XAML scoreboard (the one used to record hits on the elements). You simply need to modify it to keep the goal score. The first step is to change DirectXPage.xaml to change the scoreboard:
01 | <SwapChainPanel x:Name="swapChainPanel" Tapped="OnTapped" > |
02 | <Border VerticalAlignment="Top" HorizontalAlignment="Center" Padding="10" Background="Black" |
04 | <StackPanel Orientation="Horizontal" > |
05 | <TextBlock x:Name="ScoreUser" Text="0" Style="{StaticResource HudCounter}"/> |
06 | <TextBlock Text="x" Style="{StaticResource HudCounter}"/> |
07 | <TextBlock x:Name="ScoreMachine" Text="0" Style="{StaticResource HudCounter}"/> |
While you’re here, you can remove the bottom app bar, as it won’t be used in this game. You have removed all hit counters in the score, so you just need to remove the code that mentions them in theOnTapped hander in DirectXPage.xaml.cpp:
1 | void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e) |
You can also remove OnPreviousColorPressed, OnNextColorPressed, and ChangeObjectColor from the cpp and h pages because these were used in the app bar buttons that you removed.
To update the score for the game, there must be some way to communicate between the Game class and the XAML page. The game score is updated in the Game class, while the score is shown in the XAML page. One way to do that is to create an event in the Game class, but this approach has a problem. If you add an event to the Game class, you get a compilation error: “a WinRT event declaration must occur in a WinRT class.” This is because Game is not a WinRT (ref) class. To be a WinRT class, you must define the event as a public ref class and seal it:
1 | public ref class Game sealed |
You could change the class to do that, but let’s go in a different direction: create a new class—in this case, a WinRT class—and use it to communicate between the Game class and the XAML page. Create a new class and name it ViewModel:
2 | ref class ViewModel sealed |
In ViewModel.h, add the event and the properties needed to update the score:
01 | #pragma once |
02 | namespace StarterKit |
03 | { |
04 | ref class ViewModel sealed |
05 | { |
06 | private: |
07 | int m_scoreUser; |
08 | int m_scoreMachine; |
09 | public: |
10 | ViewModel(); |
11 | event Windows::Foundation::TypedEventHandler<Object^, Platform::String^>^ PropertyChanged; |
12 | |
13 | property int ScoreUser |
14 | { |
15 | int get() |
16 | { |
17 | return m_scoreUser; |
18 | } |
19 | |
20 | void set(int value) |
21 | { |
22 | if (m_scoreUser != value) |
23 | { |
24 | m_scoreUser = value; |
25 | PropertyChanged(this, L"ScoreUser"); |
26 | } |
27 | } |
28 | } |
29 | |
30 | property int ScoreMachine |
31 | { |
32 | int get() |
33 | { |
34 | return m_scoreMachine; |
35 | } |
36 | |
37 | void set(int value) |
38 | { |
39 | if (m_scoreMachine != value) |
40 | { |
41 | m_scoreMachine = value; |
42 | PropertyChanged(this, L"ScoreMachine"); |
43 | } |
44 | } |
45 | }; |
46 | }; |
47 | |
48 | } |
Declare a private field of type ViewModel in Game.h (you must include ViewModel.h in Game.h). You should also declare a public getter for this field:
1 | class Game |
2 | { |
3 | public: |
4 | // snip |
5 | StarterKit::ViewModel^ GetViewModel(); |
6 | rivate: |
7 | StarterKit::ViewModel^ m_viewModel; |
This field is initialized in the constructor of Game.cpp:
1 | Game::Game(const std::shared_ptr<DX::DeviceResources>& deviceResources) : |
2 | m_loadingComplete(false), |
3 | m_deviceResources(deviceResources) |
4 | { |
5 | CreateDeviceDependentResources(); |
6 | CreateWindowSizeDependentResources(); |
7 | m_viewModel = ref new ViewModel(); |
8 | } |
The getter body is:
1 | StarterKit::ViewModel^ Game::GetViewModel() |
2 | { |
3 | return m_viewModel; |
4 | } |
When the current kick ends, the variables are updated in ResetGame in Game.cpp:
1 | void Game::ResetGame() |
2 | { |
3 | if (m_isCaught) |
4 | m_viewModel->ScoreUser++; |
5 | if (m_isGoal) |
6 | m_viewModel->ScoreMachine++; |
7 | m_isAnimating = false; |
8 | m_goalkeeperPosition = 0; |
9 | } |
When one of these two properties changes, the PropertyChanged event is raised, which can be handled in the XAML page. There is still one indirection here: the XAML page doesn’t have access to Game (a non-ref class) directly but instead calls the StarterKitMain class. You must create a getter for theViewModel in StarterKitMain.h:
1 | class StarterKitMain : public DX::IDeviceNotify |
2 | { |
3 | public: |
4 | // snip |
5 | StarterKit::ViewModel^ GetViewModel() { return m_sceneRenderer->GetViewModel(); } |
With this infrastructure in place, you can handle the ViewModel’s PropertyChanged event in the constructor of DirectXPage.xaml.cpp:
01 | DirectXPage::DirectXPage(): |
02 | m_windowVisible(true), |
03 | m_hitCountCube(0), |
04 | m_hitCountCylinder(0), |
05 | m_hitCountCone(0), |
06 | m_hitCountSphere(0), |
07 | m_hitCountTeapot(0), |
08 | m_colorIndex(0) |
09 | { |
10 | // snip |
11 | |
12 | m_main = std::unique_ptr<StarterKitMain>(new StarterKitMain(m_deviceResources)); |
13 | m_main->GetViewModel()->PropertyChanged += ref new |
14 | TypedEventHandler<Object^, String^>(this, &DirectXPage::OnPropertyChanged); |
15 | m_main->StartRenderLoop(); |
16 | } |
The handler updates the score (you must also declare it in DirectXPage.xaml.cpp.h):
01 | void StarterKit::DirectXPage::OnPropertyChanged(Platform::Object ^sender, Platform::String ^propertyName) |
02 | { |
03 | |
04 | if (propertyName == "ScoreUser") |
05 | { |
06 | auto scoreUser = m_main->GetViewModel()->ScoreUser; |
07 | Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreUser]() |
08 | { |
09 | ScoreUser->Text = scoreUser.ToString(); |
10 | })); |
11 | } |
12 | if (propertyName == "ScoreMachine") |
13 | { |
14 | auto scoreMachine= m_main->GetViewModel()->ScoreMachine; |
15 | Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreMachine]() |
16 | { |
17 | ScoreMachine->Text = scoreMachine.ToString(); |
18 | })); |
19 | } |
20 | |
21 | } |
Now the score gets updated every time the user scores a goal or the goalkeeper catches the ball (Figure 17).
Figure 17. Game with score updating
Using Touch and Sensors in the Game
The game is working well, but you can still add flair to it. New Ultrabook™ devices have touch input and sensors that you can use to enhance the game. Instead of using the keyboard to kick the ball and move the goalkeeper, a user can kick the ball by tapping the screen and move the goalkeeper by tilting the screen to the right or the left.
To kick the ball with a tap on the screen, use the OnTapped event in DirectXPage.cpp:
1 | void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e) |
2 | { |
3 | m_main->OnKeyDown(VirtualKey::Space); |
4 | } |
The code calls the OnKeyDown method, passing the space key as a parameter—the same code as if the user had pressed the space bar. If you want, you can enhance this code to get the position of the tap and only kick the ball if the tap is on the ball. I leave that as homework for you. As a starting point, the Starter Kit has code to detect whether the user has tapped an object in the scene.
The next step is to move the goalkeeper when the user tilts the screen. For that, you use the inclinometer, which detects all the movement for the screen. This sensor returns three readings: pitch, roll, and yaw, corresponding to the rotations around the x-, y-, and z-axes, respectively. For this game, you only need the roll reading.
To use the sensor, you must obtain the instance for it, which you do using the GetDefault method. Then, you set its reporting interval, like this code in void Game::CreateDeviceDependentResources inGame.cpp:
01 | void Game::CreateDeviceDependentResources() |
02 | { |
03 | m_inclinometer = Windows::Devices::Sensors::Inclinometer::GetDefault(); |
04 | if (m_inclinometer != nullptr) |
05 | { |
06 | // Establish the report interval for all scenarios |
07 | uint32 minReportInterval = m_inclinometer->MinimumReportInterval; |
08 | uint32 reportInterval = minReportInterval > 16 ? minReportInterval : 16; |
09 | m_inclinometer->ReportInterval = reportInterval; |
10 | } |
11 | } |
m_inclinometer is a private field declared in Game.h. In the Update method, reposition the goalkeeper:
1 | void Game::Update(DX::StepTimer const& timer) |
2 | { |
3 | // snip |
4 | SetGoalkeeperPosition(); |
5 | if (totalTime > 2.3f) |
6 | ResetGame(); |
7 | } |
8 | } |
SetGoalkeeperPosition repositions the goalkeeper, depending on the inclinometer reading:
01 | void StarterKit::Game::SetGoalkeeperPosition() |
02 | { |
03 | |
04 | if (m_isAnimating && m_inclinometer != nullptr) |
05 | { |
06 | Windows::Devices::Sensors::InclinometerReading^ reading = |
07 | m_inclinometer->GetCurrentReading(); |
08 | auto goalkeeperVelocity = reading->RollDegrees / 100.0f; |
09 | if (goalkeeperVelocity > 0.3f) |
10 | goalkeeperVelocity = 0.3f; |
11 | if (goalkeeperVelocity < -0.3f) |
12 | goalkeeperVelocity = -0.3f; |
13 | m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 6.0f ? |
14 | m_goalkeeperPosition : m_goalkeeperPosition + goalkeeperVelocity; |
15 | } |
16 | } |
With this change, you can move the goalkeeper by tilting the screen. You now have a finished game.
Performance Measuring
With the game running well on your development system, you should now try it on a less powerful mobile device. It’s one thing to develop on a powerhouse workstation, with a top-of-the-line graphics processor and 60 FPS. It’s completely different to run your game on a device with an Intel® Atom™ processor and a built-in graphics card.
Your game should perform well on both machines. To measure performance, you can use the tools included in Visual Studio or the Intel® Graphics Performance Analyzers (Intel® GPA), a suite of graphics analyzers that can detect bottlenecks and improve the performance of your game. Intel GPA provides a graphical analysis of how your game is performing and can help you make it run faster and smoother.
Conclusion
Finally, you’ve reached the end of your journey. You started with a dancing teapot and ended with a DirectX game, with keyboard and sensor input. With the languages becoming increasingly similar, C++/CX was not too difficult to use for a C# developer.
The major difficulty is mastering the 3D models, making them move, and positioning them in a familiar way. For that, you had to use some physics, geometry, trigonometry, and mathematics.
The bottom line is that developing a game isn’t an impossible task. With some patience and the right tools, you can create great games that have excellent performance.
Special Thanks
I’d like to thank Roberto Sonnino for his writing tips and the technical review of this article.
About the Author
Bruno Sonnino is a Microsoft Most Valuable Professional (MVP) located in Brazil. He is a developer, consultant, and author of five Delphi books published in Portuguese by Pearson Education Brazil and numerous articles for Brazilian and American magazines and websites.
For more such windows resources and tools from Intel, please visit the Intel® Developer Zone