.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.
Last updated: 10:55 AM Monday October 2008
View the errata, additions and corrections to this tutorial
Important!
If you are using the DirectX 9 Summer Update version of the SDK, this tutorial will not build. Between DirectX9b and the Summer Update, Microsoft radically changed the ID3DXSprite interface. This means that both this tutorial and the next will not function correctly. When you attempt to build the code, you will receive errors stating that ID3DXSprite::Draw() does not take 7 parameters. This is unfortunately unavoidable. The principles shown in this tutorial are still valid, so please carry on!
We will be publishing an update tutorial that solves this problem ASAP!
![]()
Now we're going to start producing some neat code. The last tutorial showed you how to blit a bitmap to the back buffer, and we came to the conclusion that it wasn't really ideal for a number of reasons, not least of which was due to the lack of working colour keying functionality. So it's about time we developed a nice sprite class to fix this. Our sprite class will be the base for a couple of future classes, so it's got to be fairly generic, but at the least it should support colour keying to fix the issues with CD3DBitmap. So, here's a little preview of what we're going to create, a neat sprite class that allows for movement, rotation and scaling of a sprite:
Fig 2.2: CD3DSprite in action
Neat, eh! Thanks to Krayz for the little spaceship sprite - check his site for some excellent artwork. Note the nice colour keying in there, it's all working great. So, as usual lets take a look at the code:[ Download complete VC6 workspace as .zip ]
SpriteDemo.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
CD3DSprite.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .h file ]
CD3DSprite.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
First up, here's a brief description of each of the class methods. A lot of them are similar to the CD3DBitmap class, and with good reason. Uniform code makes our life easier.
CD3DSprite::Initialise()One call initialisation for the class. It takes all the parameters required to set up the class members, and then calls the Load() method. It's overloaded to accept either a filename to load a new image from disk, or a pointer to an existing surface - useful if we want to create multiple copies of the same sprite.
CD3DSprite::Load()
The meaty part of the class. This method does the actual loading of the graphic, and creation of the sprite. Again, it's overloaded to accept either a filename or a pointer to an existing surface.
CD3DSprite::Render()
Unsurprisingly does the actual drawing of the sprite on-screen.
CD3DSprite::Get/SetSourceRect()
Allows you to select a rectangular area of the source surface to be used as a sprite, instead of the entire surface.
CD3DSprite::Get/SetRotation()
Allows you to set an angle to rotate the sprite by. Eg: passing 90 to the Set function will rotate your sprite 90 degrees clockwise.
CD3DSprite::Get/SetRotationPoint()
Allows you to set the "hotspot" that the sprite is rotated by. Eg: setting it to 0,0 will rotate the sprite by it's top left corner.
CD3DSprite::Get/SetScale()
Allows you to scale the sprite independantly in the x,y axis. Note that a scale of 1.0, 1.0 keeps the sprite's original size.
CD3DSprite::Get/SetTranslation()
Represents the x,y location on screen that the sprite will be displayed at.
CD3DSprite::Get/SetColourKey()
You should know what this is for by now! Sets the colour to make transparent.
CD3DSprite::Get/SetModulateColour()
A handy function that allows you to modulate the sprite with an RGB colour. For example, if you have an all white sprite, and you modulate it with green, the sprite will appear with a green hue on screen. A nice way of making it look like you have far more sprite images than you really do!
I've made a couple of minor changes to this tutorial's source to update it from DirectX 8 to DirectX 9, however the changes were pretty much just style and ordering changes so you don't need to worry about them. Updating this tutorial to DX9 was nice and easy!
It looks a lot more than it actually is. Because we're using the API, we can take full advantage of it's features with very little code. In fact, all of the Get/Set methods that deal with scaling, rotation etc have implementations that are provided by the API. In other words, we get all this extra functionality for free! Lets take a look at the class members, so we can see just what we require for a sprite class:LPDIRECT3DTEXTURE9 m_pSpriteTexture; // The texture surface holding the sprite image
LPD3DXSPRITE m_pSprite; // The actual D3D sprite
RECT m_SourceRect; // The source rectangle containing the sprite
BOOL m_bLoadedFromFile; // TRUE if loaded from file, FALSE if using an existing texture
char* m_pszPathname; // Filename of the sprite image
float m_fRotation; // Angle of rotation in radians
D3DXVECTOR2 m_vRotationPoint; // The center of rotation
D3DXVECTOR2 m_vScale; // Vector to scale sprite by (1.0, 1.0 retains original size)
D3DXVECTOR2 m_vTranslation; // Vector to translate the sprite by
D3DCOLOR m_ColourKey; // Colourkey to make transparent for the sprite image
D3DCOLOR m_ModulateColour; // Colour to modulate the sprite with
float m_fTranslateX; // Float value to move the sprite on X axis by on next Render()
float m_fTranslateY; // same, for Y axis
The comments explain the purpose of each member pretty well. As usual, we require a surface to hold our image for the sprite. What is interesting to note is that instead of an LPDIRECT3DSURFACE9 member, we now have a LPDIRECT3DTEXTURE9 member, which represents a "texture surface". I'm going to hold off discussing this for the moment, as it leads into some more advanced coding, so until then just accept that a texture surface (or simply "texture") is a bitmap that has the sole purpose of being displayed on screen (as opposed to a surface, which you can draw manually to). Next, we have an LPD3DXSPRITE member, which is a pointer to the ID3DXSprite interface, and represents our actual sprite. The rest of the members are pretty uninteresting and will be discussed as we go through the methods. However, lets just look at the D3DXVECTOR2 type. We're going to be seeing a lot more of this one soon, and before we go much further we'll have to have a little discussion of 3D concepts, principles and types. For the moment though, just treat this type as a handy way of storing 2 float values, whether it's an x,y location on screen or a scaling factor.
I mentioned it above, I'm mentioning it here and I'm going to say it again later. You must have a scaling factor of (1.0, 1.0) to keep the sprite at it's original size. Using a scaling factor of (0.0, 0.0) will shrink your sprite down to nothing, and will cause you much pain and debugging. This seems a bit illogical until you realise that instead of just 2 random floats, you're actually specifying a scale of (1:1, 1:1).
So lets move onto the class methods. Again, we'll be ignoring the Get/Set methods, as they are self explanatory. First up are the 2 CD3DSprite::Initialise() methods. I've overloaded these methods so that between them they will accept either a pointer to an existing surface to use as a sprite image, or load a new image off disk. This is nice and useful because, if you think about it, most games have a lot of sprites that are totally identical. Think of a game such as R-Type, which had wave after wave of enemy ships flying at you. If we had to create a new surface for every ship, we'd rapidly run out of memory. Using a pointer to an existing surface means we can load the surface once, and share it amongst many instances of the CD3DSprite class. Anyway, these overloaded CD3DSprite::Initialise() methods call the appropriate overloaded CD3DSprite::Load() method. First of all, lets examine the Load() method that loads a file from disk.
HRESULT CD3DSprite::Load(LPDIRECT3DDEVICE9 pDevice, char* Pathname)
{
if(!pDevice)
return E_FAIL;
if(m_pSprite)
m_pSprite->Release();
if(m_pSpriteTexture)
{
m_pSpriteTexture->Release();
m_pSpriteTexture=NULL;
}
First of all, we check to make sure we have a valid pointer to the IDirect3DDevice9 interface. Next, we check if our m_pSprite member already holds a valid pointer to a sprite, as it's possible we've decided to load a brand new sprite to an existing CD3DSprite instance (object re-use is always good for memory). If it does hold a valid pointer, we remember our COM rules and release it (and you thought that section on COM back in Series 1 was useless!). Next, we do exactly the same for the texture surface. We need to release both these pointers as we're about to create brand new ones.
// Create the texture surface from the file
HRESULT rslt=D3DXCreateTextureFromFileEx(pDevice, Pathname, D3DX_DEFAULT, D3DX_DEFAULT,
D3DX_DEFAULT, D3DUSAGE_RENDERTARGET, D3DFMT_UNKNOWN,
D3DPOOL_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT,
m_ColourKey, NULL, NULL, &m_pSpriteTexture);
if(FAILED(rslt))
{
::OutputDebugString("Could not create a texture surface from the following file:\n");
::OutputDebugString(Pathname);
::OutputDebugString("\n");
return E_FAIL;
}
Next, we use D3DXCreateTextureFromFileEx() to create a texture surface from a bitmap stored on disk. This works in kind of the same way as the D3DXLoadSurfaceFromFile() method we saw in tutorial 1. It has one advantage over that method though, which is that it calculates the appropriate surface dimensions and format for us. Here's it's prototype:
HRESULT D3DXCreateTextureFromFileEx(LPDIRECT3DDEVICE9 pDevice, LPCTSTR pSrcFile, UINT Width, UINT Height, UINT MipLevels, DWORD Usage, D3DFORMAT Format, D3DPOOL Pool, DWORD Filter, DWORD MipFilter, D3DCOLOR ColorKey, D3DXIMAGE_INFO* pSrcInfo, PALETTEENTRY* pPalette, LPDIRECT3DTEXTURE9* ppTexture);
Monster function :). Parameters one and two are self explanatory, taking a pointer to the IDirect3DDevice9 interface, and a string for the filename to load as a texture respectively. Params 3 and 4 are the width and height of the texture surface. We specify D3DX_DEFAULT for both of these, which tells D3D to figure out the dimensions for itself. Parameter 5 deals with mipmapping, which tries to make textures look as nice up close as they do when far away. This is an advanced 3D graphics concept and we don't need it for our sprite engine, so we use D3DX_DEFAULT again to tell D3D to sort it out for itself. Parameter 6 specifies the type of surface we wish to create, which can either be a render target (as we've specified), or a dynamic texture surface. Again, we have no need for a dynamic surface, so a "standard" render target surface suits us just fine. Parameter 7 is the format of the surface, specifying D3DFMT_UNKNOWN tells D3D to work it out for itself from the image file. Parameter 8 is the type of memory the surface should be created in, for example system memory, AGP memory etc. However, because we've specified D3DUSAGE_RENDERTARGET for our surface type in parameter 6, the only valid parameter we can use here is D3DPOOL_DEFAULT, which tells D3D to pick the best location. Parameters 9 & 10 define how D3D should act when loading the graphic to the surface, allowing you to specify linear filtering, mirroring and other effects. These directly relate to more advanced texturing techniques, and we'll be discussing them in a later tutorial. Meanwhile we're OK to just specify D3DX_DEFAULT to take the default options. Parameter 11 specifies the colour key to make transparent, which by now you should know exactly what it does! Parameter 12 takes a pointer to a D3DXIMAGE_INFO struct, which is filled out with information about the graphic that has been loaded to the surface - it's width, height etc. As we should already know this info, we don't need to store it. Parameter 13 takes a pointer to a PALETTEENTRY struct, which is filled out with information about the graphic's palette (the colours in the image). This is useful if we're operating in reduced bit depth modes (ie: less colours), or if we want to tightly control the palette in our application. Finally, parameter 14 is a pointer to our LPDIRECT3DTEXTURE9 var, which will be set to the address of the newly created texture.
Without wishing to digress too far from the topic, it's time to make you aware of one important "feature" of texture surfaces. A lot of graphics cards in use at the moment only support what is commonly referred to as "POW2" textures. This means that your texture graphic sizes have to be sized to the power of 2 in their width and height dimensions. For example 16*16, 32*16 and 128*256 are all valid sizes for an image to be used for a texture, as each dimension is a power of 2. However 23*45, 14*14 or 328*141 are not valid sizes for an image. If you do use an image that is not sized to a power of 2, D3D will scale your graphic up to the next available power of 2, for example 23*14 would become 32*16. With small differences it's not too noticeable, but when you get up into bigger textures the scaling becomes very obvious and you begin to see artifacts (faults in the image). As an exercise, load the spaceship.bmp from the .zip file with this tutorial image into a paint package, and resize the canvas (ie: the entire image size) to anything that is not a power of 2. You'll immediately see the scaling taking place when you run the app. Note that it is perfectly acceptable to stick a picture measuring 110*97 in the top left of an image, make the canvas size 128*128 and leave the remaining space blank. This is one of the reasons for the CD3DSprite::Get/SetSourceRect() method, as you are able to specify a source rect of 110*97 and blit only the sprite image.
There are caveats to this (like most things in D3D!), such as cards that support textures that aren't sized to pow2, but we will be talking about texturing in detail in a later tutorial. For now, just accept that your canvas size must be a power of 2 for reasons of compatibility.
As per usual, it's a big function with a lot of parameters set to their defaults. Pretty straightforward when you know what each parameter means! Lets finish up the method.
// Set this member so that if the code asks change the colour key, we know whether we own
// the surface or not (therefore if we can reload it)
m_bLoadedFromFile=TRUE;
// Store the filename of the image used for this sprite source
if(m_pszPathname)
delete [] m_pszPathname;
m_pszPathname=new char[(lstrlen(Pathname)+1)];
lstrcpy(m_pszPathname, Pathname);
// Next, create the sprite
rslt=D3DXCreateSprite(pDevice, &m_pSprite);
if(FAILED(rslt))
{
::OutputDebugString("Could not create the sprite\n");
return E_FAIL;
}
return S_OK;
}
Our next two actions are to save us trouble in the future. It's possible that during the life of our sprite we would want to change which colour we make transparent. To do this, we have to reload the image from disk. But as you will recall, we overloaded the CD3DSprite::Load() method to allow us to either load an image from disk, or to use an existing image in memory. If we used an existing image in memory for this sprite, and we tried to set the colour key we would have 2 problems. First, we would be changing a property of a surface the class didn't "own", which may produce (in true Microsoft speak) undefined behaviour - ie: who knows what might happen, half our sprites might suddenly dissappear. Secondly, we wouldn't be able to change the property anyway, as changing the colour key requires a reload of the image from disk. As we didn't load the image in the first place, we sure don't know how to reload it. QED, we can't change the colour key on a sprite with an image we did not load.We track this by way of a simple BOOL - m_bLoadedFromFile. If this var is set to TRUE, we loaded the image from disk, therefore we own it, therefore we can change the colourkey. If it's FALSE, we can't do anything. And if we do need to reload it, we need to keep track of the filename, which we store in m_pszPathname.
In case you're wondering what all these "m_bXXX" and "m_pszXXX" prefixes to the variable names are, it's my little variation on the Microsoft "Hungarian notation". Basically, a long time back Microsoft created a standard way to identify what type each variable is. For example, just by looking at it you can tell that "m_bDidItWork" is a boolean class member - class member because of the "m_", and boolean because of the "b". There's lots of different prefixes, such as "psz" for "pointer to null terminated string", "sz" for "null terminated string", or "n" for "integer". To be honest, I use a couple of the basic ones and make the rest up as I go. For example, an MFC CString variable that is a class member I prefix with "m_cstr". Whatever floats your boat, as long as it makes sense to you and anyone who may read your code. Remember, code readability is a good thing.
Finally we call the library function D3DXCreateSprite() to make our sprite, and fill out our member variable m_pSprite with a pointer to the ID3DXSprite COM interface. If we've done everything else right this should never fail, but good practice is to check the return value and act appropriately.Now, thats the CD3DSprite::Load() for loading a graphic from disk, but what about the overloaded method that takes an existing surface? There is one simple difference - we don't need to worry about the loading. Because we're using an existing surface, we know the graphic is already loaded in memory and valid for use, so we can just grab a pointer and use that. Ah, the miracle of COM. Here's the code:
HRESULT CD3DSprite::Load(LPDIRECT3DDEVICE9pDevice, LPDIRECT3DTEXTURE9 pTexture)
{
if(!pDevice)
return E_FAIL;
if(!pTexture)
return E_FAIL;
// We're using an existing texture surface, so as per COM rules, add a reference to the surface
pTexture->AddRef();
m_pSpriteTexture=pTexture;
m_bLoadedFromFile=FALSE;
Nice and simple. First, we make sure that the 2 pointers we were passed are valid. Next, we call the IUnknown::AddRef() method, to add a reference count to the texture surface COM object. If you're unsure why we need to do this, have another read of this tutorial in Series 1. Note that I add the reference count before I make a copy of the pointer. If we copy the pointer and then increment the reference count, it is possible that under some conditions the time between the copy and the call to AddRef() allows another process to Release() it's pointer, destroying the COM object and thus making your pointer invalid. It's unlikely, but calling AddRef() before copying prevents this possibility. So, we add a reference count to the texture surface object, copy the pointer and then finally set our m_bLoadedFromFile member var to FALSE to enable us to track ownership of the surface, as discussed above.Next up is the CD3DSprite::Render() method.
HRESULT CD3DSprite::Render(LPDIRECT3DDEVICE9 pDevice, LPDIRECT3DSURFACE9 pRenderSurface)
{
// Even though we know the destination surface, we'll still do these checks to make sure
// there is a valid device and surface.
if(!pDevice)
return E_FAIL;
if(!pRenderSurface)
return E_FAIL;
// Add in any single frame translations
m_vTranslation.x+=m_fTranslateX;
m_vTranslation.y+=m_fTranslateY;
m_fTranslateX=0.0f;
m_fTranslateY=0.0f;
Again, we check that the pointers passed to the method are valid. Let me explain the comment above them though. The reason we know the destination surface is, as you will see, a ID3DXSprite object can only be drawn between a call to IDirect3DDevice9::BeginScene() and IDirect3DDevice9::EndScene(). You may be wondering (again) why we pass 2 parameters we don't need. This will become apparent very soon, so just trust me!Next, we add in single frame translations. This enables us to set a member variable to a float, and have the sprite's current position moved by that same factor in either the X or Y axis in this one iteration only. As I've done in the main Render() function in SpriteDemo.cpp, this is a quick way to move a sprite across the screen relative to it's current location. If we only used the CD3DSprite::Get/SetTranslation() methods to move the sprite, we'd require an extra var to hold the current position, then we'd need to increment it and pass it back. I think you'll agree it's much quicker to do small frame-by-frame movements relative to the sprite's current position this way.
// Draw the sprite. If the source rect corners equal 0, use the entire surface
if(m_SourceRect.bottom==0 && m_SourceRect.top==0 && m_SourceRect.left==0 && m_SourceRect.right==0)
{
rslt=m_pSprite->Draw(m_pSpriteTexture, NULL, &m_vScale, &m_vRotationPoint,
m_fRotation, &m_vTranslation, m_ModulateColour);
}
else
{
rslt=m_pSprite->Draw(m_pSpriteTexture, &m_SourceRect, &m_vScale, &m_vRotationPoint,
m_fRotation, &m_vTranslation, m_ModulateColour);
}
Finally, we need to draw the sprite. First we check to see if all the corners of m_SourceRect are equal to 0 - if they are we'll use the whole surface for the sprite, if not then we'll use the source rect. Depending on this check, we call the ID3DXSprite9::Draw() method with the appropriate parameters. Here's it's prototype:
HRESULT Draw(LPDIRECT3DTEXTURE9 pSrcTexture, CONST RECT* pSrcRect, CONST D3DXVECTOR2* pScaling, CONST D3DXVECTOR2* RotationCenter, FLOAT Rotation, CONST D3DVECTOR2* pTranslation, D3DCOLOR Color)
Hopefully you can see exactly how our parameters drop into this prototype. The first parameter takes our pointer to the texture surface holding our sprite image. The second parameter is the source rectangle on that surface to use for the sprite. As we discussed before, if you have a sprite which is smaller than the texture surface (which is sized to the power of 2, remember?), you use this parameter to specify exactly the dimensions of the image on the surface that you wish to blit. Parameter 3 is the factor to scale the image by. Parameter 4 is an x,y location on the texture surface to use as the center of rotation. For example, specifying 0,0 here will cause any rotations to rotate the sprite by it's top left corner. Parameter 5 is the angle in radians to rotate the sprite by, counterclockwise. Parameter 6 is the translation to apply to the sprite, in other words the x,y location that it should be displayed at on screen. Parameter 7 is the modulation colour, which allows you to tint the sprite with a colour of your choosing.And that is pretty much that! The remainder of the method just checks return values and exits, and is not worth reproducing here. Which leaves us with the rest of the class, the Get/Set methods. Most of these are standard C++ and not D3D related, so I won't discuss them here. Lets just take a quick look at the CD3DSprite::SetColourKey() method though:
HRESULT CD3DSprite::SetColourKey(LPDIRECT3DDEVICE9 pDevice, D3DCOLOR colour)
{
// First, check if the texture surface has been loaded. If it hasn't, we can just set the
// colour key, and it will be picked up by the next call to load the surface.
if(!m_pSpriteTexture)
{
m_ColourKey=colour;
return S_OK;
}
// But if the texture surface is a valid pointer, we need to reload the surface using the new
// colour key. First, we have to make sure we loaded the texture from file - we can't go
// fiddling with surfaces loaded by other components.
if(!m_bLoadedFromFile)
{
::OutputDebugString("Cannot set a colour key on a texture this class does not own\n");
return E_FAIL;
}
// Now that we've established we own the surface, reload it using the new colour key
m_ColourKey=colour;
return Load(pDevice, m_pszPathname);
}
I've heavily commented this method so it's easy to follow the logic. First, we check to see if the class member m_pSpriteTexture is a valid pointer. If it's not, we know that a texture surface hasn't been assigned to the class yet (wether it's a copy of a surface or a new one loaded from disk). In that case, we can just pre-emptively set the m_ColourKey member so that it is picked up on the next call to CD3DSprite::Load() or Initialise(). However, if m_pSpriteTexture is a valid pointer, we need to check the m_bLoadedFromFile member. If it's FALSE, we didn't load the texture surface ourselves, therefore we can't set a colour key on it. In that case, we output an error message and return. But if we pass all these checks and we did load the texture surface ourselves, we can set the colour key and call CD3DSprite::Load() to reload the texture with it. In case you are wondering why we don't use the D3DXLoadSurfaceFromSurface() method that we used in our CD3DBitmap class, it's because the function doesn't support texture surfaces. As always, there are ways round this which you'll see in a later tutorial.All that's left to do now is to see how we use our class in our main code. First up we create a global instance of our class for the spaceship sprite, called g_SpriteSpaceship. Next, in our GameInit() function, we set up the class:
// Create our sprite, setting the colour key and translation vector
g_SpriteSpaceship.SetColourKey(g_pDevice, D3DCOLOR_XRGB(243,0,0));
g_SpriteSpaceship.SetTranslation(D3DXVECTOR2(20.0f, 50.0f));
rslt=g_SpriteSpaceship.Load(g_pDevice, "spaceship.bmp");
if(FAILED(rslt))
{
return rslt;
}
If you take a look at the spaceship.bmp file included in the complete workspace zip (at the top of this tutorial), you'll see that the spaceship graphic is in the top left of the image, and it's on a red background. So, first we call our CD3DSprite::SetColourKey() method to set the colour key to the RGB value of the red we used as a background. Next we call CD3DSprite::SetTranslation() to set the sprite's initial x,y location on screen. Finally we call CD3DSprite::Load() to load the sprite image from disk. And that's all we have to do, because our class has handled the rest of the setup for us. So, in the Render() function we only need the following code to display our sprite on screen and move it about:
// Get the current x,y location of the sprite on screen
D3DXVECTOR2 vShipLocation;
g_SpriteSpaceship.GetTranslation(&vShipLocation);
// If it's x location is less than backbuffer width-100, translate it by +0.5 on the X axis
if(vShipLocation.x < g_D3DSettings.m_nDeviceWidth - 100)
g_SpriteSpaceship.m_fTranslateX+=0.5f;
g_SpriteSpaceship.Render(g_pDevice, g_pBackSurface);
We add our render code in between the IDirect3DDevice9::BeginScene() and IDirect3DDevice9::EndScene() calls. To make sure we only move the spaceship within the bounds of the screen, we call CD3DSprite::GetTranslation() to find out whereabouts on screen the sprite is currently. If it's less than the backbuffer width - 100, we add 0.5 to the m_fTranslateX member to make the sprite move across screen. Finally, we call the CD3DSprite::Render() method to draw the sprite to screen, and we're done!You now have your very own working and functional sprite engine! I really encourage you to play around with the code, calling the Get/Set methods of the class to see what each one does. I've commented out some sample code to demonstrate the source rectangle clipping. At the least you should play with the rotation and colour modulation, the more you play the more you learn.
You may not have realised it, but this chapter has introduced you to some complex theories and techniques. In series 3 of the DirectX Basics set we'll be going into the 3D pipeline in detail and discussing topics such as primitives, texture mapping, and 3D maths. Radians, D3DVECTORs and translations will all be coming back in much more detail soon! If you're still with us and confident you understand everything so far, you're doing well. The rest of this series is easy by comparison. I'll see you in the next tutorial, where we get to make our own custom font writer based on this sprite class. Enjoy playing with the code!
Click here to send feedback, bug reports, comments and ideas etc!
Corrections, Additions, Errata
24/10/2002: Tony pointed out a semantics/coding bug in the Get/SetRotation() class methods. Whereas my text above (and the var names) say that the parameter to this method is the angle in degrees you wish to rotate the sprite, it is in fact the angle in radians you wish to rotate the sprite. To convert degrees to radians, use the D3DXToRadian() macro. For example:
g_Sprite.SetRotation(D3DXToRadian(90));
Thanks Tony!
27/02/2003: Thomas en Niels Rot found a nasty little bug in the structure of the sprite code in this tutorial. I'd mistakenly forgotten to reset the m_pszPathname member var to NULL in the class constructor, meaning that under certain circumstances the code would try to delete a string at an invalid address which would cause a nasty crash. The fix was simply to assign the variable to NULL in the constructor. Thanks Thomas!