Learning Windows 8 Game Development
上QQ阅读APP看书,第一时间看更新

Drawing the sprites

Now we need to get those images onto the screen. Using DirectXTK we have access to a class called SpriteBatch. If you have used XNA before, you might recognize this class and remember that it is an excellent way to render 2D images with all of the effort and optimization done for you.

For this we need to prepare the SpriteBatch inside our Game class so we can render the textures we have created, and then from there add some code to the Texture class so that we can finally draw these images.

Let's begin by defining a shared pointer to a SpriteBatch object within the Game class. Remember that DirectXTK defines all of its classes within the DirectX namespace, so you'll need to create something like the following:

std::shared_ptr<DirectX::SpriteBatch> _spriteBatch;

Once you've done that, we need to create SpriteBatch. This requires a device context so that it can create the required internal resources. Add the following line to the start of your LoadContent method within the Game class.

Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d11DeviceContext;
m_d3dContext.As(&d3d11DeviceContext);
_spriteBatch = std::make_shared<DirectX::SpriteBatch>(
    d3d11DeviceContext.Get()
);

Here we need to convert the device context we have from ID3D11DeviceContext1 to ID3D11DeviceContext. The ComPtr type allows us to do this easily, and once we have that we can create SpriteBatch.

Now we have a working SpriteBatch that can be used to render our textures. The next step is to draw the sprites. We're using SpriteBatch and, as the name implies, the sprites will be drawn batched together. This improves performance by giving the GPU a nice chunk of work to do in a minimal number of API calls, so we don't need to keep going through the API and driver layers continuously, wasting time. To do this, we need to let the batch know when it should begin and when we're done issuing the commands, so that it can do some work, batch everything together, and draw to the screen.

To do this, we need to call SpriteBatch->Begin() before we draw anything. For our purposes, we can just call Begin without any parameters; however, if you want to alter the GPU state during the batch, or change the batching mode, you would do that here.

Sorting modes

There are a few different modes available for batching, so you can choose the best one for your situation:

  • SpriteSortMode_Deferred
  • SpriteSortMode_Immediate
  • SpriteSortMode_FrontToBack
  • SpriteSortMode_BackToFront
  • SpriteSortMode_Texture

Deferred is the default option, and this takes the sprites in the order you submit them, waits until End is called, and then submits them to be drawn.

Immediate removes the benefit of batching by sending draw calls for every sprite you draw, as soon as you call Draw. This is useful in some cases where batching is giving you issues or you just want to use SpriteBatch as an easy way to do 2D.

FrontToBack sorts the sprites by their depth parameter, drawing the sprites closest to the screen (0.0f) first. BackToFront does the opposite, and draws the sprites behind first. This is where you need to look at what you're drawing and make a decision. By rendering FrontToBack you make the most of the depth culling functionality on modern GPUs. As the closest sprites are drawn first, the GPU knows that those pixels are now occluded and minimizes overdraw in those regions. BackToFront is often necessary when you are working with transparent sprites, often in particle effects. To keep using depth culling with other objects, you need to render these back to front to ensure that the particles can blend together without artifacts.

Finally, texture is a special sorting mode, where the batch will look at the textures you're drawing and determine if you're reusing the same texture anywhere. If you are, it can batch the calls together for each texture, optimizing the number of texture swaps that need to occur, which can often be a major bottleneck for texture heavy games.

Finishing the batch

Now that we have begun rendering, we can draw each texture individually. To do this, we'll add a Draw method to our Texture class that will handle the rendering for you. Inside the Texture class header, declare the following method prototype under the internal modifier:

void Draw(std::shared_ptr<DirectX::SpriteBatch> batch);

Once you've done that, implement the method in the .cpp file by adding the following code:

void Texture::Draw(std::shared_ptr<DirectX::SpriteBatch> batch)
{
  auto vPos = DirectX::XMLoadFloat2(&_pos);
  batch->Draw(_srv.Get(), vPos, nullptr);
}

The main part of this method is the call to batch->Draw(), which tells the SpriteBatch to draw the texture, or save it for later (depending on which sort mode you chose).

We need to tell the Draw call where to place the sprite on the screen. This is done through the second parameter, which asks for an FXMVECTOR from the DirectXMath library that comes with Direct3D and Windows 8. To do this we will have to load our _pos variable, which is stored as XMFLOAT2 (X, Y), into XMVECTOR, which can be passed to the Draw function.

After that, we will pass a nullptr value to the final parameter, which asks us to define which region of the texture should be drawn. Here we can define a sub-region of the texture and just draw that part; however, in our case we want to draw the whole thing, so to make that simpler we can pass a nullptr value and skip having to manually get the size of the texture and create a rectangle that defines the entire region.

Vectors

A vector can be simply described as an array of numbers, which in this case refer to a co-ordinate location on the 2D screen. In this book I will refer to vectors in the form (X, Y), where X and Y represent their respective co-ordinates.

DirectXMath provides plenty of helper functions and utilities to make working with vectors easy, and the SimpleMath library contained within DirectXTK makes this even easier.

So now we can finish rendering the sprites by calling the Draw function we just implemented for each of our sprites and then ending the batch. Return to the Game class' Draw method and add the following code after our Clear calls:

_spriteBatch->Begin();

_player->Draw(_spriteBatch);
_enemy->Draw(_spriteBatch);

_spriteBatch->End();

Here we're calling our new Draw method on each of the textures we created and loaded. After that we call End on the sprite batch, which informs it that we are done issuing commands and it can proceed with rendering.

Because we are using the default Begin method, we need to ensure that we specify the order in which these textures are drawn. One useful example is if you want to have a background in a game. If you draw the player and other objects before the background without any form of sorting, you will end up writing over the player images, and all you will see is the background. So ensure you check your draw order before actually drawing to the screen; and if you can't see some sprites that should be there, make sure you aren't hiding them accidentally.

Now run the game and you should see our two sprites rendered onto the screen, ready to come alive once we add some gameplay.