Navigation: 2D Sprites

Tutorial 1 - Sprites


Overview

You might find you need 2d sprites for displaying game elements. I have made a light-weight sprite class to help with this. It uses an ID3DXSprite instance to render textures to the screen.

ID3DXSprite

To render sprites using Direct3D 9 you need to create an instance of ID3DXSprite. This is done using D3DXCreateSprite(). For example:
//Declare pointer in Game class
ID3DXSprite* m_pSprite;

//Get pointer to ID3DXSprite, 
//in Game::InitializeDirect3D()
if(FAILED(D3DXCreateSprite(m_d3dDevice, &m_pSprite)))
{
	return E_FAIL;
}
You will also have to add ->OnLostDevice(), and ->OnResetDevice() calls for m_pSprite:
void Game::OnDeviceLost()
{
	try
	{
		//Add OnDeviceLost() calls for DirectX COM objects
		m_pEffect->OnLostDevice();
		m_pSprite->OnLostDevice();
		m_deviceStatus = DEVICE_NOTRESET;
	}
	catch(...)
	{
		//Error handling code
	}
}

void Game::OnDeviceGained()
{
	try
	{
		m_d3dDevice->Reset(&m_present_parameters);
		//Add OnResetDevice() calls for DirectX COM objects
		m_pEffect->OnResetDevice();
		m_pSprite->OnResetDevice();
		m_deviceStatus = DEVICE_LOST_OR_OPERATIONAL;
	}
	catch(...)
	{
		//Error handling code
	}
}
Add the following code to the Game::FreeAllResources() method:
SAFE_RELEASE(m_pSprite);
Once you have the ID3DXSprite* pointer you can draw 2d textures to the screen over some 3d world.

How to Use Sprite and Object

You can write your own Sprite handling class/method or you can use an existing one. Here I will show you how to use my toolkit sprite class to draw sprites. The Sprite class I have written supports pixel-perfect detection of points or collisions with other sprites. For example if you have an odd shaped shape and you want to know if a mouse click clicks on the shape and not somewhere else then you can use the Sprite class to detect this.

The idea is that you create objects(instances) of the Object class and assign sprites to those objects. You can then control the member variables and methods of Object to control how the object's sprite is drawn.

Since some objects may share the same sprite it makes sense to store x and y position in an instance of an object rather than in each sprite instance because the x and y of one object may differ from the x,y of another. If we stored the x,y in the sprite rather than per object then the sprite would be rendered at the same position for each object that uses that sprite, which is not what we want.

Before I explain how to use Sprite and Object there are a few modifications you need to make to your existing DirectX Application:
  • Make the Game instance globally accessible
  • Set the back buffer width and height to window dimensions
  • Update the back buffer dimensions on window resized
  • Make the Game instance globally accessible
The reason for making the instance global is so that the Window Procedure can call methods of the game instance, for example update the backbuffer when window is resized. To make it global, simply declare the instance of Game in the global scope:
class Game
{
public:
	~Game();
	bool InitGame(HINSTANCE hInstance, HWND render_window);

...

	Sprite my_img1;
	Object oBarbarian;
};

//Global game
Game game;
  • Set the back buffer width and height to window dimensions
Setting the backbuffer dimensions to window dimensions is necessary for maintaining the correct aspect ratio used to render sprites. Otherwise sprites would appear stretched.

Get the window dimensions(client area) with:
RECT rc;
GetClientRect(render_window, &rc);
Use the width and height of the client rect as the backbuffer width and height:
D3DPRESENT_PARAMETERS Game::CreatePresentParameters(HWND render_window)
{
	D3DPRESENT_PARAMETERS pp;

	RECT rc;
	GetClientRect(render_window, &rc);

	memset(&pp, 0, sizeof(D3DPRESENT_PARAMETERS));
	pp.BackBufferWidth = rc.right;
	pp.BackBufferHeight = rc.bottom;
	pp.BackBufferFormat = D3DFMT_A8R8G8B8;
	pp.BackBufferCount = 1;
	pp.MultiSampleType = D3DMULTISAMPLE_NONE;
	pp.MultiSampleQuality = 0;
	pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	pp.hDeviceWindow = render_window;
	pp.Windowed = true;
	pp.EnableAutoDepthStencil = true;
	pp.AutoDepthStencilFormat = D3DFMT_D24S8;
	pp.Flags = 0;
	pp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
	pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;

	return pp;
}
  • Update the back buffer dimensions on window resized
Now that game is a global instance, the Window Procedure can talk to it. You will also have to make OnDeviceLost(), and OnDeviceGained() public in the Game class. Modifications to the WndProc are as follows:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	//Handle window creation and destruction events
	switch(msg)
	{
	case WM_CREATE:
		break;
	case WM_CLOSE:
		PostQuitMessage(0);
		break;
	//Window resized event:
	case WM_SIZE:
		game.OnDeviceLost();
		game.OnDeviceGained();
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}

	//Handle all other events with default procedure
	return DefWindowProc(hWnd, msg, wParam, lParam);
}
You will also need to modify the Game::OnDeviceGained() function as well to set the backbuffer width and height to the new window dimensions if the window is resized:
void Game::OnDeviceGained()
{
	try
	{
		m_present_parameters = CreatePresentParameters(m_mainWindow);
		m_d3dDevice->Reset(&m_present_parameters);

		//Add OnResetDevice() calls for DirectX COM objects
		m_pEffect->OnResetDevice();
		m_pSprite->OnResetDevice();
		m_deviceStatus = DEVICE_LOST_OR_OPERATIONAL;
	}
	catch(...)
	{
		//Error handling code
	}
}
I have also added a return statement to the Game::Update() function to ensure no updates to the game occur if the device is in a lost state, which I should have done in previous tutorials.
if(coop != D3D_OK)
{
	if(coop == D3DERR_DEVICELOST)
	{
		if(m_deviceStatus == DEVICE_LOST_OR_OPERATIONAL)
			OnDeviceLost();		
	}
	else if(coop == D3DERR_DEVICENOTRESET)
	{
		if(m_deviceStatus == DEVICE_NOTRESET)
			OnDeviceGained();
	}

	return;
}

Sprite

To load a sprite image into a game using the Sprite class, first declare an instance of a Sprite(A good place might be the Game class):
Sprite my_img1;
Call my_img1.Load() in Game::InitGame():
my_img1.Load(hInstance, m_d3dDevice, L"barbarian_front.png", NULL, 1, NULL);
The fourth parameter is also an image filename like the 3rd parameter; but it is a mask rather than something visible to the player. The mask is necessary if you want pixel-perfect detection. I've set it to NULL for now, so no mask is used.

Note: You need to include the Sprite.h header wherever you use Sprite

Note: Make sure you call my_img1.Release() when you are done with it.

Object

Now create an instance of an Object(You can do this in the Game class):
...

Sprite my_img1;
Object oBarbarian;

...
After my_img1 is loaded, assign it to oBarbarian:
oBarbarian.SetSprite(&my_img1);
oBarbarian.x = 20.0f;
oBarbarian.y = 12.0f;
Render the oBarbarian sprite to the screen in Game::RenderWorld():
void Game::RenderWorld()
{
	if(m_deviceStatus == DEVICE_LOST_OR_OPERATIONAL)
	{
		//TODO: Add Device lost handling.
		m_d3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff000022, 1.0f, 0);

		if(SUCCEEDED(m_d3dDevice->BeginScene()))
		{
			//Call UpdateMatrices() to recalculate the view and
			//projection matrices with current camera position etc.
			//We may have moved the camera or resized the window, 
			//so we need to recalculate
			//the view and projection matrices with UpdateMatrices()

			...

			oBarbarian.Draw(m_pSprite);

			m_d3dDevice->EndScene();
		}

		// Swap buffers.
		m_d3dDevice->Present(NULL, NULL, NULL, NULL);
	}
}
Now you should see the image rendered to the screen if you got this far!

That's how to use Object and Sprite together.

The last thing is to perform a click-mask detection. There are of course other functions of Object and Sprite you may find useful but I don't want to go into great detail; I leave it to the reader to explore them. The click detection is done through the Object interface, oBarbarian in this case.

To use a mask with a sprite modify the my_img1 Load method to:
my_img1.Load(hInstance, m_d3dDevice, 
		L"test_sprite2.png", 
		L"test_sprite2.bmp",
		1, NULL);
Note: The mask has to be a bmp

test_sprite2.png and test_sprite2.bmp looks like this:


Figure 1.

The white pixels are ignored if a collision-sprite test is made, and if the mouse clicks a black pixel, then there is a collision with the mouse.

To test this out you can have an Alert box if a black pixel is clicked(Put the following code in the Game::Update() function):
if(mouse.LeftPressed())
{
	POINT p = mouse.GetMousePos(m_mainWindow);

	//Has the user clicked the sprite mask?
	if(oBarbarian.AtPlace(p.x, p.y)){
		MessageBox(m_mainWindow, L"Clicked!", L"Message", MB_OK);
	}
}
I have made a simple mouse class that the above code uses. You can get the files here:

Mouse.h
Mouse.cpp

Just declare a Mouse instance in the Game class, or a suitable place to use it:
class Game
{
public:
	~Game();
	bool InitGame(HINSTANCE hInstance, HWND render_window);

	...

private:
	//Helper Functions.
	D3DPRESENT_PARAMETERS CreatePresentParameters(HWND render_window);

private:
	Mouse mouse;

	Camera camera1;
	Mesh platform_mesh;

	...
};

Conclusion

You can now incorporate 2d sprites into your game and perform click-sprite detection using a sprite mask bmp.

Download Project Files