DirectXTutorial.com
The Ultimate DirectX Tutorial
Lesson 5:  Building a Game Display
Log In
Lesson Overview

Now that we have covered the important parts of sprites and text, let's build an actual display out of them.  There are various techniques you will need to know before you can actually build a good display, and we'll cover some of them in here.

The display we will build will have three parts.  First will be a radar screen (not a functioning one, because there aren't any enemies to show just yet).  Next there will be a health bar (manually controlled, as there aren't any enemies to kill you just yet).  Finally there will be a numerical ammo indicator (also manually controlled, as there aren't any enemies to shoot just yet).

A Too-Simple Wrapper for Sprites

Because of the complex nature of DirectX, it is probably best to build a few functions to get everything simplified.  To do otherwise will make the code completely unreadable, and that is not what we want in a tutorial, right?

I've written two functions here that will greatly cut down on the code.  There is one to load a texture file and one to render a portion of the file.


LoadTexture()

For loading we will need a two of parameters.  We'll need a place to store the texture and a filename.

void LoadTexture(LPDIRECT3DTEXTURE9* texture, LPCTSTR filename)
{
    D3DXCreateTextureFromFileEx(d3ddev, filename, D3DX_DEFAULT, D3DX_DEFAULT,
        D3DX_DEFAULT, NULL, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_DEFAULT, 
        D3DX_DEFAULT, D3DCOLOR_XRGB(255, 0, 255), NULL, NULL, texture);

    return;
}

It's easy enough.  It takes the texture location and the filename, loads the file into the texture with the hot-pink background.  It is set for alpha, so it can be used with semi-transparency.


DrawTexture()

This one aint so easy.

There are more parameters.  For each graphic drawn, we need to specify its location in the texture, its size, and its location on the screen.  We also need to specify which texture we are using.  This results in seven parameters!  Yikes!

Okay, let's cut it down a little.  We'll use a RECT to simplify the location in the texture and the size.  Now we have a RECT, two integers, and a pointer to a texture.  Four parameters.  Much better.

Wait, what about alpha?  We'll add another integer for this.  Five parameters.  Not too great, but there isn't much we can do about it, really.  We could build a complex class to take care of it, but you can do that on your own if you like.

Let's look at the function as is:

void DrawTexture(LPDIRECT3DTEXTURE9 texture, RECT texcoords, int x, int y, int a)
{
    D3DXVECTOR3 center(0.0f, 0.0f, 0.0f), position(x, y, 0.0f);
    d3dspt->Draw(texture, texcoords, &center, &position, D3DCOLOR_ARGB(a, 255, 255, 255));

    return;
}

For the rest of this lesson, we will be using these functions so that we can focus on building the display, not on managing needlessly large functions.

Constructing the Radar

Let's start looking into the actual display itself.  The first part we will look into is the radar.  The radar will require three separate sprites:

Image 1.1 - DirectGrid.png Displayed in DirectX

Image 5.1 - The Radar Sprites

The first sprite is the inside of the radar circle, and is drawn at 50% transparency.  It is made to fit exactly inside the white circle.

The second sprite is a small red dot, which is used to indicate the alleged enemies.

The third sprite is the circle, which is drawn opaque.  Notice that there is a dot in the middle, representing the player.  The player is always in the center of the radar, so we don't need to make an extra sprite for that.

Of course, each sprite is filled in by pink for color-keying.


The Code

In drawing the radar, we need to get the order of drawing exact.  If we draw it in the wrong order, some things will be covered by the wrong sprites, making the image look incorrect.  The order we want to draw our sprite in is first the transparent background, second the enemy blip, third the white circle.

We will put our code into two functions.  One to load the graphics, and one to draw them.  These functions we will call LoadDisplay() and DrawDisplay().

// globals
LPDIRECT3DTEXTURE9 DisplayTexture;
float enemyX = 60.0f, enemyY = 60.0f;

void LoadDisplay()
{
    LoadTexture(&DisplayTexture, L"DisplaySprites.png");

    return;
}

void DrawDisplay()
{
    RECT Part;

    // DRAW THE RADAR
    // display the backdrop
    SetRect(&Part, 2, 14, 169, 181);
    DrawTexture(DisplayTexture, Part, 10, 10, 127);
      
    // display the enemy
    SetRect(&Part, 341, 14, 344, 17);
    DrawTexture(DisplayTexture, Part, enemyX, enemyY, 255);

    // display the border
    SetRect(&Part, 171, 13, 340, 182);
    DrawTexture(DisplayTexture, Part, 9, 9, 255);

    return;
}

This code shows and displays all three parts on top of one another, just as they should be...except for...

Fixing the Radar

If at this point you tried changing the location of the enemy blip around, you will have noticed that the blip still shows even when it is off the radar completely.  The solution to this is to only draw the blip if it is within a certain distance of the center of the radar.

If you don't recall how to find distances, here is the formula.  For the distance between Point 1 and Point 2:

Distance = √(x1 - x2)² + (y1 - y2)²

Here, x1 and y1 are the coordinates of the center of the radar, while x2 and y2 are the coordinates of the enemy blip.  If you use this formula, you can determine whether the distance is greater than the radius of the radar.  If it is, then you don't draw it, and if it isn't you do.  Simple.

Here is the code you add to determine the distance:

// globals
LPDIRECT3DTEXTURE9 DisplayTexture;
float enemyX = 60.0f, enemyY = 60.0f;

void LoadDisplay()
{
    LoadTexture(&DisplayTexture, L"DisplaySprites.png");

    return;
}

void DrawDisplay()
{
    RECT Part;

    // DRAW THE RADAR
    // display the backdrop
    SetRect(&Part, 2, 14, 169, 181);
    DrawTexture(DisplayTexture, Part, 10, 10, 127);
      
    // if the enemy is within 84 units of the player, display the enemy
    if(sqrt((92 - enemyX) * (92 - enemyX) + (92 - enemyY) * (92 - enemyY)) < 84)
    {
        SetRect(&Part, 341, 14, 344, 17);
        DrawTexture(DisplayTexture, Part, enemyX, enemyY, 255);
    }

    // display the border
    SetRect(&Part, 171, 13, 340, 182);
    DrawTexture(DisplayTexture, Part, 9, 9, 255);

    return;
}

Now, when you run that, the radar only shows those enemies that are in range.  Of course, you'll have to get the blip moving around if you want to see it working.

Constructing the Health Bar

Now let's go onto the next item in our series: the health bar.  This item will have only two graphics.  Take a look at them here, then we'll go over what they are:

Image 1.1 - DirectGrid.png Displayed in DirectX

Image 5.2 - The Health Bar Sprites

The first, and more obvious, sprite is the bar.  The second, and less obvious one, is the filler.

The task at hand is pretty simple.  We draw the bar, then we draw repeated instances of the filler.  We keep drawing the filler until we have draw enough to indicate the level of health/mana/whatever.

The end result is this:

Image 1.1 - DirectGrid.png Displayed in DirectX

Image 5.3 - The Health Bar Pieced Together at Varying Percentages 

Now let's look at our display code with the health bar included:

// globals
LPDIRECT3DTEXTURE9 DisplayTexture;
float enemyX = 60.0f, enemyY = 60.0f;
int health = 300;
int maxhealth = 1000;


void LoadDisplay()
{
    LoadTexture(&DisplayTexture, L"DisplaySprites.png");

    return;
}

void DrawDisplay()
{
    RECT Part;

    // DRAW THE RADAR
    // display the backdrop
    SetRect(&Part, 2, 14, 169, 181);
    DrawTexture(DisplayTexture, Part, 10, 10, 127);

    // if the enemy is within 84 units of the player, display the enemy
    if(sqrt((92 - enemyX) * (92 - enemyX) + (92 - enemyY) * (92 - enemyY)) < 84)
    {
        SetRect(&Part, 341, 14, 344, 17);
        DrawTexture(DisplayTexture, Part, enemyX, enemyY, 255);
    }

    // display the border
    SetRect(&Part, 171, 13, 340, 182);
    DrawTexture(DisplayTexture, Part, 9, 9, 255);

    // DRAW THE HEALTHBAR
    // display the bar
    SetRect(&Part, 1, 1, 505, 12);
    DrawTexture(DisplayTexture, Part, 11, 456, 255);

    // display the health "juice"
    SetRect(&Part, 506, 1, 507, 12);
    for(int index = 0; index < (health * 490 / maxhealth); index++)
        DrawTexture(DisplayTexture, Part, index + 18, 456, 255);


    return;
}
Constructing the Ammo Indicator

Finally, let's take a look at the ammo indicator.  This indicator is numerical, so we will be using a font.  I will be using a boring, italic Arial for now, but you are free to search for your own font that better suits the motif.

The ammo indicator has just two graphics:

Image 1.1 - DirectGrid.png Displayed in DirectX

Image 5.4 - The Ammo Indicator Sprites

These are the obvious parts: the opaque border and the transparent background.  It is the font that makes this one tricky.  Let's study the code added.

// globals
LPDIRECT3DTEXTURE9 DisplayTexture;
float enemyX = 60.0f, enemyY = 60.0f;
int health = 300;
int maxhealth = 1000;
int ammo = 10394;
LPD3DXFONT dxfont;


void LoadDisplay()
{
    LoadTexture(&DisplayTexture, L"DisplaySprites.png");

    D3DXCreateFont(d3ddev, 12, 0, FW_NORMAL, 1, false, DEFAULT_CHARSET,
                   OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
                   L"Arial", &dxfont);


    return;
}

void DrawDisplay()
{
    RECT Part;

    // DRAW THE RADAR
    // display the backdrop
    SetRect(&Part, 2, 14, 169, 181);
    DrawTexture(DisplayTexture, Part, 10, 10, 127);

    // if the enemy is within 84 units of the player, display the enemy
    if(sqrt((92 - enemyX) * (92 - enemyX) + (92 - enemyY) * (92 - enemyY)) < 84)
    {
        SetRect(&Part, 341, 14, 344, 17);
        DrawTexture(DisplayTexture, Part, enemyX, enemyY, 255);
    }

    // display the border
    SetRect(&Part, 171, 13, 340, 182);
    DrawTexture(DisplayTexture, Part, 9, 9, 255);

    // DRAW THE HEALTHBAR
    // display the bar
    SetRect(&Part, 1, 1, 505, 12);
    DrawTexture(DisplayTexture, Part, 11, 456, 255);

    // display the health "juice"
    SetRect(&Part, 506, 14, 507, 12);
    for(int index = 0; index < (health * 490 / maxhealth); index++)
        DrawTexture(DisplayTexture, Part, index + 18, 456, 255);

    // DRAW THE AMMO INDICATOR
    // display the backdrop
    SetRect(&Part, 351, 14, 456, 40);
    DrawTexture(DisplayTexture, Part, 530, 449, 127);

    // display the border
    SetRect(&Part, 351, 45, 457, 72);
    DrawTexture(DisplayTexture, Part, 530, 449, 255);

    // display the font
    SetRect(&Part, 535, 453, 630, 470);
    static char strAmmoText[10];
    _itoa_s(ammo, strAmmoText, 10);
    dxfont->DrawTextA(NULL,
                      (LPCSTR)&strAmmoText,
                      strlen((LPCSTR) &strAmmoText),
                      &textbox,
                      DT_RIGHT,
                      D3DCOLOR_ARGB(255, 120, 120, 255));

      return;
}

Easily the scariest part of this code is the string management where we convert the integer ammo to the string strAmmoText.

However, all that really happens here is that the integer ammo is converted into strAmmoText using the _itoa_s() function, and the address of that string is passed into the char* of the DrawTextA function.

Of course, strlen() is used to determine the length of the string, so we don't get something like this 127□ÀÀÀÀÀÀ appearing on our screen when we only want 127.

The rest is just typical drawing from our sprite functions.

The Finished Program

Now let's take the fragments we've gone over and put this whole program together.

[Show Code]

This program runs on the file "DisplaySprites.png".  It contains all the sprites that make up the display panel.

If you use this image and run the code, you will get something like this:

Image 1.1 - DirectGrid.png Displayed in DirectX

Image 5.5 - The enemy is coming, and you're low on health.  Good thing there's LOTS of ammo!
Summary

This tutorial has covered the basics of building a game display.  Every game display is, of course, unique, and each is quite different from others.  The difficult task is to figure out how to create the display that will best fit your game.  And so I challenge you with these tasks:

1.  Design a rough layout your own game display.
2.  Create the graphics for your display.  Make them look cool!
3.  Write a program to show and manipulate your display.

Next Tutorial:  DirectInput

GO! GO! GO!