Understanding the game loop
The core of our game happens in the game loop. This is where all of the logic and subsystems are processed, and the frame is rendered, many times per second. The game loop consists of two major actions: updating and drawing.
Updating the simulation
Now we can get to the action. A game is a simulation of a world, and just like a movie simulates motion by displaying multiple frames per second, games simulate a living world by advancing the simulation many times per second, each time stopping to draw a view into the current state of the game world.
The simplest update will just advance the timer, which is provided to us through the BasicTimer
class. The timer keeps track of the amount of time that has elapsed since the previous frame, or since the timer was started—usually the start of the game. Keeping track of the time is important because we need the
frame delta (time since the last frame) to correctly advance the simulation by just the right amount. We also work at a millisecond level, sometimes even a fraction of a millisecond, so we need to make use of the floating point data types to appropriately track these values.
Once the time has been updated, most games will accept and parse player input, as this often has the most important effect on the world. Doing this before the rest of the processing means you can act on input immediately rather than delaying by a frame. The amount of time between an input by the player and a reaction in the game world appearing on screen is called latency. Many games that require fast input need to reduce or eliminate latency to ensure the player has a great experience. High latency can make the game feel sluggish or laggy, and frustrate players.
Once we've processed the time and input, we need to process everything else required to make the game world seem alive. This can include (but is not limited to) the following:
- Physics
- Networking
- Artificial Intelligence
- Gameplay
- Pathfinding
- Audio
Drawing the world
Once we have an up-to-date game world, we need to display a view of the world to the players so that they can receive some kind of feedback for their actions. This is done during the draw stage, which can either be the cheapest or most expensive part of the frame, depending on what you want to render.
The Draw()
method (sometimes also called the
Render()
method) is commonly broken down into three sections: clearing the screen, drawing the world, and presenting the frame to the monitor for display.
Clearing the screen
During the rendering of each frame, the same render target is reused. Just like any data, it remains there unless we clear it away before trying to make use of the texture. Clearing the screen is paramount if you use a depth buffer. If you do not clear the depth buffer, the old data will be used and can prevent certain visuals from rendering when they should, as the GPU believes that something has already been drawn in front.
Clearing the render target and depth buffer allows us to reinitialize the data in each pixel to a clean value, ready for use in the new frame.
To clear both the buffers we need to issue two commands, one for each. This is the first time we will use the views that we created earlier. Using our ID3D11DeviceContext
, we will call both the
ClearRenderTargetView()
and ClearDepthStencilView()
methods to clear the buffers. For the first method you need to pass a color that will be set across the buffer. In most cases setting black (0, 0, 0, 1 in RGBA) will be enough; however, you may want to set a different color for debug purposes, which you can do here with a simple float array.
Clearing the depth just needs a single floating point value, in this case 1.0f
, which represents the farthest distance from the camera. Data in the depth buffer is represented by values between 0
and 1
, with 0
being the closest to the camera. We also need to tell the command which parts of the depth buffer we want to clear. We won't use the stencil buffer, so we will just clear the depth buffer using D3D11_CLEAR_DEPTH
, and leave the default of 0
for the stencil value.
The auto
keyword is a new addition to C++ in the C++11 specifi cation. It allows the compiler to determine the data type, instead of requiring the programmer to specify it explicitly.
auto rtvs = m_renderTargetView.Get(); m_d3dContext->OMSetRenderTargets( 1, &rtvs, m_depthStencilView.Get() ); const float clearCol[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; m_d3dContext->ClearRenderTargetView( rtvs, clearCol ); m_d3dContext->ClearDepthStencilView( m_depthStencilView.Get(), D3D11_CLEAR_DEPTH, 1.0f, 0);
You'll notice that aside from clearing the depth buffer, we're also setting some render targets at the start. This is because in Windows 8 the render target is unbound when we present the frame, so at the start of the new frame we need to rebind the render target so that the GPU has somewhere to draw to.
Now that we have a clean slate, we can start rendering the world. We'll get into this in the next chapter, but this is where we will draw our textures onto the screen to create the world. The user interface is also drawn at this point, and once this stage is complete you should end up with the finished frame, ready for display.
Presenting the back buffer
Once we have a frame ready to display, we need to tell Direct3D that we are done drawing and it can flip the back buffer with the front buffer. To do this we tell the API to present the frame.
When we present the frame, we can indicate to DXGI that it should wait until the next vertical retrace before swapping the buffers. The vertical retrace is the period of time where the monitor is not refreshing the screen. It comes from the days of CRT monitors where the electron beam would return to the top of the screen to start displaying a new frame.
We previously looked at tearing, and how it can impact the visual quality of the game. To fix this issue we use VSync. Try turning off VSync in a modern game and watch for lines in the display where the frame is broken by the new frame.
Another thing we can do when we present is define a region that has changed, so that we do not waste power updating all of the screen when only part of it has changed. If you're working on a game you probably won't need this; however, many other Direct3D applications only need to update part of the screen and this can be a useful optimization in an increasing mobile and low-power world.
To get started, we need to define a DXGI_PRESENT_PARAMETERS
structure in which we will define the region that we want to present, as follows:
DXGI_PRESENT_PARAMETERS parameters = {0}; parameters.DirtyRectsCount = 0; parameters.pDirtyRects = nullptr; parameters.pScrollRect = nullptr; parameters.pScrollOffset = nullptr;
In this case we want to clear the entire screen, so Direct3D lets us indicate that by presenting with zero dirty regions.
Now we can commit by using the Present1()
method in the swap chain:
m_swapChain->Present1(1, 0, ¶meters);
The first parameter defines the sync interval, and this is where you would enable or disable VSync. The interval can be any positive integer, and refers to how many refreshes should complete before the swap occurs. A value of zero here will result in VSync being disabled, while a value of one would lock the presentation rate to the monitor refresh rate. You can also use a value above one, which will result in the refresh rate being divided by the provided interval. For example, if you have a monitor with a 60 Hz refresh rate, a value of one would present at 60 Hz, while a value of two would present at 30 Hz.
At this point, we've done everything we need to initialize and render a frame; however, you'll notice the generated code adds some more lines, as shown in the following code snippet:
m_d3dContext->DiscardView(m_renderTargetView.Get()); m_d3dContext->DiscardView(m_depthStencilView.Get());
These two lines allow the driver to apply some optimizations by hinting that we will not be using the contents of the back buffer after this frame. You can get away with not including these lines if you want, but it doesn't hurt to add them and maybe reap the benefits later.