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.