Box navigation, viewport and HUD
Last updated: 17-06-2006
Project: Game Programming with DirectX 9
Prerequisites: Completion of all the preceding tutorials in this project is required.
Downloads: DirectXBoxNavigation.zip
In this version of our application we are going to implement box navigation so it will be possible to control box movements by pressing the arrow keys on a keyboard. We will also build a very simple HUD (heads-up display) for our game with dialog windows and will use a DirectX viewport to render our 3D world according to the specified dimensions. The source code that comes with this tutorial (see Downloads section above) extends the previous solution. To see all the tutorials that belong to this project click on the project name above.
HUD is usually used to provide a 2D graphical user interface but also to show additional information to the user like the score for example. During the development process HUD is often useful to output the test results.
At this point we need a HUD to show the box's position in our 3D world. In order to do that we will use standard Win32 dialog window and static controls (labels).
To start with we have to add the necessary resources to our application. We assume that the reader is already experienced with the process of creating resources for a Windows application. If not, additional information can be found in the documentation that comes with Visual Studio. Each compiler has its way to add resources to the program. In Visual Studio we use the Resource Editor which simplifies the process of managing application resources to the level of entering values into the fields. The zip archive attached to this tutorial includes all the necessary resource files. See also Resources section of Adding the "Game Menu" tutorial for more information about resources.
With the Resource Editor in Visual Studio we have created the ControlPanel.rc file that includes definitions of the following application resources: 2 dialog windows (one to be used as a Control Panel to present additional information to the user and one to be used as a Viewport to render the DirectX 3D world). The control panel dialog window has a number of static controls (labels) to hold the X, Y, Z position values of the box. We provide all the resources and their elements with identifiers so we could refer to them in our code.
The identifiers we did use can be found in the header file bellow.
Extract from Resource1.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by ControlPanel.rc
//
#define ID_CONTROLPANEL                 101
#define ID_VIEWPORT                     102
#define ID_BOX_X                        1001
#define ID_BOX_Y                        1002
#define ID_BOX_Z                        1003
#define ID_BOX_X_LABEL                  1004
#define ID_BOX_Z_LABEL                  1005
#define ID_BOX_Y_LABEL                  1006

If you open the ControlPanel.rc with a text editor you will see that dialog windows are actually defined like in the .rc file bellow.
Extract from ControlPanel.rc
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

ID_CONTROLPANEL DIALOGEX 0, 170, 365, 55
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    LTEXT           "X",ID_BOX_X,24,7,65,10
    LTEXT           "Z",ID_BOX_Z,24,35,65,10
    LTEXT           "Y",ID_BOX_Y,24,21,65,10
    LTEXT           "X:",ID_BOX_X_LABEL,9,7,14,10
    LTEXT           "Z:",ID_BOX_Z_LABEL,9,35,9,10
    LTEXT           "Y:",ID_BOX_Y_LABEL,9,21,10,10
END

ID_VIEWPORT DIALOGEX 0, 0, 365, 170
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
END
The Resource1.h file and textual content of the ControlPanel.rc are generated automatically if you use a Resource Editor in Visual Studio to add the resources to your program. Alternativelly you can type the definitions yourself, which gives room for mistakes and therefore is not recommended.
Notice that the include section of our application class was extended:
#include <windows.h>
#include <atltypes.h>
#include <string>
#include <sstream> 
#include <d3d9.h> 
#include <d3dx9.h>
#include "resource.h" 
#include "resource1.h" 

using namespace std;
We need the atltypes.h header to work with CRect class which we use to obtain the dialog windows dimentions as a rectangle object to determine the size of a DirectX viewport.
The string and sstream are included so we could work with strings and perform all the necessary convertions (like for example converting double to string).
We then add the resource1.h file generated by the Resource Editor.
The "using namespace std" is declared to avoid writing the entire path when we use constants, objects or methods from this namespace.
The following constants were added to define box's movement boundaries:
const float    MAX_BOX_X = 9.0f,
        MIN_BOX_X = -9.0f,
        MAX_BOX_Y = 4.5f,
        MIN_BOX_Y = -5.5f;
As you may have already guessed our box is going to move in X and Y but not in Z axis inside those boundaries.
We define a viewport as a global variable:
D3DVIEWPORT9 viewPort;
Viewport is a DirectX object that determines the boundaries of a rendered 3D world.
Before we proceed with our source code discussion it is important to understand how Windows operating system determines the dimensions of a dialog window. You probably know that dialog size can not be defined in pixels during the design time, instead it is defined in DLU's (Dialog Units). These DLU's are converted to pixels by the operating system at runtime according to the font used by the dialog window. This is done to support the "large fonts" feature in Windows. Large fonts scale differently than regular fonts which means that a dialog window has to adjust itself.
When we create a DirectX viewport we have to provide its dimensions in pixels. In other words we need to find the way to convert from DLU's to pixels at runtime. The MapDialogRect() method defined in the included windows.h does just that. It takes a rectangle in DLUs and returns a rectangle in pixels. You can find the exact specification of this method in MSDN documentation which is usually installed together with Visual Studio or online at http://msdn2.microsoft.com.
The following global variables are defined so we will have an easy access to the dialog windows.
HWND hwndControlPanel = NULL;
HWND hwndViewPort = NULL;
We did add the isBoxMove variable to determine if we have to move the box when we draw a scene.
bool isWireframe = true,
     isBoxMove = true;
The boxMove variable is defined as a WPARAM so we could easily send it as a message parameter to a target dialog window.
WPARAM boxMove;
D3DXVECTOR3 posBox = D3DXVECTOR3(0.0f, -3.37f, 0.0f);
We also need the posBox vector to hold the current box position in our 3D world.
The following function helps converting from double to string:
string CStr(double dVal) { stringstream ssVal; ssVal << dVal; return ssVal.str(); } 
The drawBoxMove() method provided bellow deserves a special attention. As the name suggests each box move controlled by the arrow keys from a keyboard is rendered here.
First of all we define the x, y, z local variables and initialize them from the values of a global posBox vector. The reason we have made posBox a global variable is to allow us access the box's position from other methods we are going to discuss later in this project.
Then we define the direction vector and position vectors A and B that signify the departure position and the destination position of the box respectively. The last definition is a matrix to hold the box's transformation in space.
Before we can calculate the box's destination position we have to know in which direction box is going to move. This is where we use the boxMove variable defined as WPARAM holding the value of an arrow key pressed on a keyboard which is very convenient for us. Notice that we also check that the movement is still in the allowed boundaries (see constants declarations above). According to the direction of the movement we increase of decrease y of x box's position value with the timeDelta (amount of movement).
Now we are ready to initialize our vectorB with the new position values. Notice that Z value is never modified because our box does not move on Z axis.
void drawBoxMove(){
    static float    x = posBox.x,
                    y = posBox.y,
                    z = posBox.z;

    static D3DXVECTOR3 direction = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 vectorA, vectorB;
    D3DXMATRIX matrix;

    vectorA = D3DXVECTOR3(x, y, z);

    if(boxMove == VK_UP && y < MAX_BOX_Y){ // box move up
        y += timeDelta;
    }
    else if(boxMove == VK_DOWN && y > MIN_BOX_Y) { // box move down
        y -= timeDelta;
    }
    else if(boxMove == VK_RIGHT && x < MAX_BOX_X){ // box move right
        x += timeDelta;
    }
    else if(boxMove == VK_LEFT && x > MIN_BOX_X){ // box move left
        x -= timeDelta;
    }

    vectorB = D3DXVECTOR3(x, y, z); 
    
    
    direction = vectorA - vectorB;
    D3DXVec3Normalize(&direction, &direction);
    posBox = vectorA + (direction * timeDelta);

    D3DXMatrixTranslation(&matrix, posBox.x, posBox.y, posBox.z);
    directXDevice->SetTransform(D3DTS_WORLD, &matrix);

    // draw the object using the previously created world matrix.
    meshBox->DrawSubset(0);

    // send message to the label
    string strX = CStr(posBox.x);
    string strY = CStr(posBox.y);
    string strZ = CStr(posBox.z);

    SendMessage(hwndControlPanel, WM_COMMAND, ID_BOX_X, (LPARAM)strX.c_str());
    SendMessage(hwndControlPanel, WM_COMMAND, ID_BOX_Y, (LPARAM)strY.c_str());
    SendMessage(hwndControlPanel, WM_COMMAND, ID_BOX_Z, (LPARAM)strZ.c_str());
}
The direction of the vector is calculated by substacting vectorB from vectorA. After that operation the direction vector is normalized and a new posBox vector is calculated. Now we can perform the matrix transformation by providing the matrix to hold the return value and new X, Y, Z position values stored in posBox vector.
After the matrix transformation was calculated we inform the directXDevice that we have performed a matrix transformation by calling SetTransform() method and passing the D3DTS_WORLD constant and the transformed matrix as parameters.
Finally we can draw the box in its new position in 3D space by calling the DrawSubset(0) method of meshBox. We use 0 because meshBox has only 1 subset to draw.
We also convert the new X, Y, Z position values to strings and send messages to update the labels at hwndControlPanel dialog window.
Because all the job is done by the drawBoxMove() method our drawScene() method can now be simplified:
void drawScene(){
    directXDevice->SetViewport(&viewPort);
    directXDevice->Clear(0, NULL, D3DCLEAR_TARGET, 
    0x00000000, 1.0f, 0);  // 0x00000000 = black
    directXDevice->BeginScene();
    // begin scene

    //
    // draw your objects here
    //


    // draw box
    if(isBoxMove) drawBoxMove();

    // end scene
    directXDevice->EndScene();
    directXDevice->Present(NULL, NULL, NULL, NULL);

}

The only thing we do here is to check if the value of isBoxMove variable is true which means that we have to draw the box movement. If it is false box remains in its current position. Notice that box moves only if you press the arrow keys. Pressing any other key (except the [ESC] key that exits the application) will stop the movement, fixing the box in its current position.
We add the following method to initialize the dialog window hosting our DirectX viewport.
BOOL CALLBACK DlgProcViewPort(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_INITDIALOG:
        {
            CRect    rectPixels(0,0,0,0);
            // returns a rectangle of hwnd in pixels
            if(GetWindowRect(hwnd, rectPixels)){
                viewPort.X = 0;
                viewPort.Y = 0;
                viewPort.Width = rectPixels.Width();
                viewPort.Height = rectPixels.Height();
                viewPort.MinZ = 0;
                viewPort.MaxZ = 1;
            }
        }
        break;

        default:
            return FALSE;
    }
    return TRUE;
}
This is where we use the GetWindowRect() method described in the beginning of this tutorial to convert DLU's coordinates to pixels. This is done by first defining a rectangle with initial values (in DLU's) and then passing this rectangle to the GetWindowRect() method as parameter. The return value will be stored at rectPixels rectangle with dimensions converted to pixels. If this is successful we can initialize the DirectX viewport's dimensions to the obtained from rectPixels width and height.
What is also important to realize at this point is that if your viewport dimensions are not calculated correctly due to the wrong conversion from DLU's to pixels you might end up with something that looks like that:
In this example a dialog window defined in DLU's with Resource Editor in Visual Studio was clearly larger then the DirectX viewport generated in pixels and the space not occupied by the viewport was filled with whatever makes sense to your graphical card at this point of time. If you see something like that, you will have to take care that viewport dimensions in pixels are equal to the dialog window dimensions after being correctly converted from DLU's at runtime.
Now we can go back to our code discussion. The DlgProcControlPanel() method bellow receives the WM_COMMAND messages to update the labels showing the X, Y, Z box's position values.
BOOL CALLBACK DlgProcControlPanel(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_COMMAND:
            switch(LOWORD(wParam))
            {
                case ID_BOX_X:
                    SetDlgItemText(hwnd, ID_BOX_X, (LPCSTR)lParam);
                break;

                case ID_BOX_Y:
                    SetDlgItemText(hwnd, ID_BOX_Y, (LPCSTR)lParam);
                break;

                case ID_BOX_Z:
                    SetDlgItemText(hwnd, ID_BOX_Z, (LPCSTR)lParam);
                break;

            }
        break;

        default:
            return FALSE;
    }
    return TRUE;
}
Inside the WinProc() method the following cases were added:
case WM_CREATE:
{
    hwndControlPanel = CreateDialog(GetModuleHandle(NULL), 
        MAKEINTRESOURCE(ID_CONTROLPANEL),
        hwnd, DlgProcControlPanel);
    if(hwndControlPanel != NULL){
        ShowWindow(hwndControlPanel, SW_SHOW);
    }
    
    hwndViewPort = CreateDialog(GetModuleHandle(NULL),
        MAKEINTRESOURCE(ID_VIEWPORT),
        hwnd, DlgProcViewPort);
    if(hwndViewPort != NULL)
    {    
        ShowWindow(hwndViewPort, SW_SHOW);
    }
}
break;

case WM_SYSKEYDOWN:
case WM_KEYDOWN:
    switch(wParam){
        case VK_ESCAPE:
            DestroyWindow(hwnd);
        break;

        case VK_LEFT:
        case VK_RIGHT:
        case VK_UP:
        case VK_DOWN:
            boxMove = wParam;
            isBoxMove = true;
        break;

        default:
            boxMove = wParam;
            isBoxMove = true;
        break;
    }            
break;
When the main window is being created (case WM_CREATE) we create our dialog windows and initialize the hwndControlPanel and hwndViewPort global variables to point to them.
If [ESC] key is pressed we call DestroyWindow() method to exit the application.
If left, right, up or down key is pressed we initialize the boxMove to the value of wParam and set the value of isBoxMove to true.
If any other key is pressed then isBoxMove variable will be set to false, fixing the box in its current position in space.
Because we have modified the structure of our GUI we need to modify the following inside the WinMain():
initDevice(hwndViewPort);
Here we pass the hwndViewPort to the initDevice() method instead of the hwnd as in the previous tutorial.
The last touch will be a slight modification of the time interval calculation formula so everything on screen will move a little bit faster then in the previous tutorial:
timeDelta = (currTime - lastTime)*0.002f;
In the next tutorial we are going to simulate the shooting of fireballs.