In this chapter we will start our project, create a window, initialize DirectX
in our window, and then print some text using DirectX. As we do this, we will
be creating a framework in which to run our game. I'm going to start slow and
cover a little of the visual studio usage and C++ class structure, so if you
are unfamiliar with either one at least you will not be totally lost.
Topics Covered in this Chapter:
Creating an Empty Project
Creating a window
Initializing DirectX
Creating a main for our game
Printing text
The first thing we need to do to get started is create a new project in Visual
Studio. 'New project' is on the file menu under new. In the dialog box that
comes up select Visual C++ Projects, and then Win32 Application. You also require
a title for your project. I've gone ahead and labeled mine breakout, because
that is what we will be programming. Once the project has been selected and
named, click next to continue.
On the next screen click application settings and under additional options
check 'Empty project'. Click finish, and you now have an empty C++ project.
On the right of the screen there should be a side bar currently labeled Properties.
At the bottom of the box are 3 tabs, Properties, Solution Explorer, and Class
View - Select Solution Explorer. The Solution Explorer shows you all of the
Source and Header files your project contains, as well as other resources. Right
now our project does not contain anything, so the first thing we want to do
is create the entry point to our application. Add a new source file to the project
by right clicking on the Source Files folder, and selecting 'add new item' from
the add menu. Select new C++ Source File, and name it 'winmain'. This file is
going to be the entry point to our application, it will setup our window and
start our game. Lets take a look at the winmain.cpp in the chapter 2 code.
This is the top of the winmain. It includes the basic windows functions, and
creates an instance handle for the application and for the window. This, and
the code that will follow, is fairly standard windows initialization code. In
fact this is just a modified (stripped down) version of what you get if you
don't tell Visual Studio you want an empty project.
#include <windows.h>
HINSTANCE hInst; // holds the instance for this app
HWND wndHandle; // global window handle
// Link in the DirectX libraries required to compile
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"dxerr9.lib")
#pragma comment(lib,"dinput8.lib")
#pragma comment(lib,"d3dx9.lib")
This is the WinMain function. If you are familiar
with console application programming, you know the entry point for your program
is main(). This is the windows equivalent; it is where our program starts, it
contains the main message loop, and it will be where our program ends. Notice
the comment before the message loop starts, as where we will be creating our
main game object, and after the message loop ends as where we will be destroying
our game object. When writing a windows application the message loop handles
messages passed to the window, such as menu clicks, etc. It is important to
keep the message loop flowing, because if your program stops handling messages,
windows will report your program as not responding. To keep it simple, our message
loop only cares about the quit message (WM_QUIT) and when it has no other messages
pending, it will pass control over to our game.
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE
hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
// call our function to init and create our window
if (!initWindow(hInstance))
{
MessageBox(NULL, "Unable to create window", "ERROR", MB_OK);
return false;
}
// This is where we will Create our main game object
// Main message loop:
// Enter the message loop
MSG msg;
ZeroMemory( &msg, sizeof(msg) );
while( msg.message!=WM_QUIT )
{
// check for messages
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// if there are no messages waiting update the game
else
{
//update call here
}
}
//this is where we will delete the game object
return (int) msg.wParam;
}
The next function is initWindow. initWindow
registers a window class, and then creates our window.
The last function in the winmain class is WndProc.
It's the windows procedure, and the last function we require for a functioning
windows program. The windows procedure handles events from the system that relate
to our application. However we aren't going to need to worry about anything
other than quit.
That concludes the winmain file, you should now be able to compile and run
your project. It will produce an empty window.
Now that we have a window, we need to initialize DirectX to use our window.
Initializing DirectX to use our display, requires us to instantiate 2 objects,
the Direct3D object, and the Direct3D Device object. The Direct3D object takes
one parameter (D3D_SDK_VERSION), this gives you access to Direct3D with the
current version of the SDK you have installed. My understanding is you could
pass previous versions of DirectX 9 if you needed to use them for some reason.
The Direct3D Device object handles communication with your video card. Unlike
the Direct3D object, it is not a simple object to create. Let's take a look
at the dxMgr class, and the creation of these two objects.
The dxMgr class, short for DirectX Manager class, consists of 2 files, the
header dxMgr.h and the main C++ file dxMgr.cpp. Now I am assuming you know some
C++, but I'll go over this anyway just in case. The header file contains the
class definition - The name of your class and its functions and variables. Where
the main C++ file contains the actual code for the functions. Lets start by
taking a look at dxMgr.h from the chapter 2 code.
The following is the header for the dxMgr class. It shows we are going to have
6 functions, and 2 private variables. As we go through the main file dxMgr.cpp
you will see the header was just a forward declaration of what was going to be
in the main file.
#include <d3d9.h>
class dxMgr
{
public:
dxMgr(void); //Constructor
~dxMgr(void); //deconstructor
bool init(HWND hwnd, int width, int height, bool fullscreen); //initializes
our interface
void beginRender(void); //starts the scene render
void endRender(void); //ends the scene render and presents to the screen
LPDIRECT3DDEVICE9 getD3DDevice(); //returns the device
private:
LPDIRECT3D9 direct3d; //This is the Direct3D object
LPDIRECT3DDEVICE9 direct3dDevice; //This is our device object
};
At the top of the dxMgr.cpp we include the header file.
#include ".\dxMgr.h"
Next we have the constructor. The constructor is the first function called
when you create an instance of an object, often it is used to zero out memory
that is going to be used. In this case, we are nulling our Direct3D object and
our Device object.
Next is the deconstructor. The deconstructor called when an object is deleted,
it is used to free up any memory used by an object. In this case we are freeing
our Direct3D object and our Device object.
Now on to the more complicated part, actually initializing DirectX. The init
function takes 4 parameters: hwnd - our window, width - the width of the display
we are going to create, height - the height of the display, and fullscreen -
true or false, if we want the display to be fullscreen or not. This is my simplified
init, as you will see there is a lot more we could
have put into the init, but for now that's all
we are going to worry about. The width and height are the width and height of
the backbuffer, in a window you can set this to any size you like, however you
will get the best picture with a buffer that is the size of your window. If
fullscreen is true, the DirectX device will take over the display, in this case
the width and height must be a valid supported resolution. Fullscreen also requires
the BackBufferFormat to be specifed. The BackBufferFormat specifies how color
information is going to be stored, common formats are 16 and 32 bit. I have
coded it to D3DFMT_R5G6B5, which is a 16 bit format, with 5 Red bits, 6 Green
bits and 5 Blue bits. If you would like to have a higher color format, you can
use D3DFMT_X8R8G8B8, which is a 32 bit format, with 8 bits for each color. Keep
in mind, going from a 16 bit format to a 32 format means your buffers are going
to take twice as much memory; on older graphics cards this can mean quite a
hit in performance.
//Decide if windowed or fullscreen
bool windowed;
if (fullscreen){
windowed = false;
}
//Setup the present parameters
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory( &d3dpp, sizeof(d3dpp) );
d3dpp.Windowed = windowed; //Windowed or Fullscreen
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; //discards the previous frames
d3dpp.BackBufferFormat = D3DFMT_R5G6B5; //The display format
d3dpp.BackBufferCount = 1; //Number of back buffers
d3dpp.BackBufferHeight = height; //height of the backbuffer
d3dpp.BackBufferWidth = width; //width of the backbuffer
d3dpp.hDeviceWindow = hwnd; //handle to our window
d3dpp.AutoDepthStencilFormat = D3DFMT_D16; //The stencil format
d3dpp.EnableAutoDepthStencil = TRUE;
d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
//Create the Video Device
hr = direct3d->CreateDevice( D3DADAPTER_DEFAULT, //The default adapter is the
primary display adapter
D3DDEVTYPE_HAL, //the HAL (hardware accelerated layer) uses your 3d accelerator
card
hwnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING, //sets the graphic card to do the hardware
vertexprocessing
&d3dpp, //The present parameters we created above
&direct3dDevice
);
if( FAILED(hr)){
return false;
}
direct3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, TRUE); //this normalizes
the normal values (this is important for how lighting effects your models)
return true;
}
The functions that follow are functions that let us use the device we have
created. The next function is beginRender, its
purpose is to tell the Device that we are starting rendering. It starts by making
sure we have a device to render to. Then we clear the device, and tell the device
to begin the scene.
void dxMgr::beginRender()
{
if( NULL == direct3dDevice )
return;
// Clear the backbuffer to a black color
direct3dDevice->Clear( 0, //number of rectangles to clear, 0 for all
NULL, //rects to be cleared, NULL for entire buffer
D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, //the buffers to clear
D3DCOLOR_XRGB(55,55,55), //the Color to fill the buffer with
1.0f, //depth for the z-buffer to clear
0 //stencil buffer clear
);
direct3dDevice->BeginScene();
}
Logically following beginRender is endRender.
Endrender is fairly simple, we tell the device
we have finished rendering by calling EndScene(),
and then we present the buffer we created during rendering to the screen with
Present. Present takes
a number of parameters for setting how much of the buffer we wish to present
and where we wish to present it on the screen. In this function Present(
NULL, NULL, NULL, NULL ) just presents the whole buffer to the screen.
There is one last function in the dxMgr class, and that is the
getD3DDevice function. getD3DDevice simply
returns the device we created. As we go along you will see that many of the
useful built-in DirectX functions need the device. So we defiantly need to be
able to access it.
That concludes the dxMgr class. If you have been following the code and typing
it in as you go, you should have added the header, dxMgr.h, and the main file,
dxMgr.cpp, to your project. Although we haven't included or called our dxMgr
yet compiling your project is still a good idea to see if we have made any mistakes.
If your project compiles and runs we know we at least haven't made any syntax
errors in our dxMgr.
Now that we have a Window and a DirectX Manager we are going to tie them together
with a class called GameMain. In the same way that the winmain handles setting
up your window and looping through the windows messages, the GameMain will handle
setting up our window to be a game window (by initializing DirectX), and will
handle our game update and render loop. Let's start by looking at the GameMain
header file. Notice there are 2 class definitions, the top one, class dxMgr,
is a forward declaration for the dxMgr class, it lets the compiler know we are
going to be using a dxMgr object somewhere in the header. We need to do this
if we want to declare a variable of type dxMgr, or if we want to pass a dxMgr
as a parameter, or have it as a return type. The second class definition is
the GameMain, and it contains the function forward definitions and variables
as we would expect.
class dxMgr;
class GameMain
{
public:
GameMain(void);
~GameMain(void);
bool init(HWND wndHandle);
void update(void);
private:
dxMgr* dxManager;
};
Next on to the main file for GameMain. At the top of GameMain are the includes
we need, the DirectX manager include, and the GameMain header file.
The first function in GameMain that has anything in it is
init, (both the constructor and deconstructor are currently empty), init
only takes one argument, the handle to our window. Currently the only thing
init does is create a new instance of our DirectX
manager, and then initialize it. Looking at the init
call to the DirectX manager we see we are calling it with the window handle
passed to the GameMain init, a buffer width and
height of 800x600 respectively and in windowed mode (fullscreen is false).
bool GameMain::init(HWND wndHandle)
{
dxManager = new dxMgr(); //create our direct Manager
dxManager->init(wndHandle, 800, 600, false); //Initialize our DirectX Manager
if (!dxManager){
return false;
}
return true;
}
The next, and last function in GameMain is update.
The update function takes no parameters, and will
be called by our winmain whenever there are no messages pending. The update
functions purpose is to call the update and render functions in game objects.
Currently we have no game objects, so we have an empty update
function with comments reminding us where we need to make our calls.
void GameMain::update(void)
{
//call our update function
// prepare our render device for rendering to
dxManager->beginRender();
//game render calls go here
//end the render and present the buffer to the screen
dxManager->endRender();
}
That completes our GameMain framework, now we need to call it from our winmain.
Going back to the winmain.cpp file we need to included the GameMain class, create
and initialize a GameMain instance, and call update in the appropriate location.
At the top of winmain, we need to included the GameMain header.
#include <windows.h>
#include "GameMain.h"
Next we need to create our object, and initialize it. Once it is initialized,
we place an update call into the no message pending part of the message loop.
Lastly, we add a delete call to free up the memory our object was using when
the message loop is broken with an exit message. (Changes are in Bold)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE
hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
// call our function to init and create our window
if (!initWindow(hInstance))
{
MessageBox(NULL, "Unable to create window", "ERROR", MB_OK);
return false;
}
// Create the game main
GameMain *game = new GameMain();
// Initialize the game object
if (game->init(wndHandle) == false){
return 0;
}
// Main message loop:
// Enter the message loop
MSG msg;
ZeroMemory( &msg, sizeof(msg) );
while( msg.message!=WM_QUIT )
{
// check for messages
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// if there are no messages waiting update the game
else
{ game->update();
}
}
// Delete the game object
if (game){
delete game;
}
return (int) msg.wParam;
}
Now that we have added the GameMain object and needed calls to winmain we are
ready to test our project again. Compile and run your project. If you have inputted
everything correctly you will be presented with an empty window with a dark
gray background. You have now created a window and initialized Direct3d in it.
Alright, so now we have a DirectX window, and its all ready to go, but right
now there is nothing in it. We are going to change that by drawing some text
to it. There is a lot more to drawing text than you might think. So we are going
to create a text class to handle drawing text. The class I created is called
dxText, lets start by looking at its header. Note there are two new includes,
d3dx9tex.h, which is needed to use the directx text functions, and string, which
is needed to use the standard C++ string object. In the private variables we
create an ID3DXFont object to be our font.
#include <d3d9.h>
#include <d3dx9tex.h> //needed for text
#include <string> //need for using std:string
class dxText
{
public:
dxText(void); //Constructor
~dxText(void); //deconstructor
bool init(DWORD size, LPDIRECT3DDEVICE9 device); //initializes our interface
void drawText(std::string text, int x, int y, int width, int height); //draw
a text string
private:
ID3DXFont *g_font; //font
};
The first function in the main file for the dxText class we are going to look
at is init.
The init function currently only takes 2 arguments,
the size of the font, and the Direct3d Device. Inits purpose is to create a
font to use for rendering text. Looking over the CreateFont function you can
see it would not be hard to setup the init to also take parameters for bold
or italic, or the particular font you wished to use. This text class will mainly
be used for debugging, so we won't worry about setting up nice looking fonts
quite yet.
Once we have created a font, we need a function to draw our text. The drawText
function draws the text we give it at the location we specify. It takes 5 parameters:
the text we wish to output, the starting coordinates x and y, and the width
and height of the rectangle which we are drawing the text. The font_rect is
a rectangle that specifies where we want the text to be clipped. When we specify
a width and height we generally want to make it big enough that our text doesn't
get clipped. SetRect positions our font_rect on the screen, and DrawText does
just what you would think, and actually draws the text to the buffer.
void dxText::drawText(std::string text, int
x, int y, int width, int height){
RECT font_rect = {0,0,width,height}; //sets the size of our font surface
rect
//set the destination for our text, by moving the font rect.
SetRect(&font_rect, //our font rect
x, //Left
y, //Top
width, //width
height //height
);
That completes our dxText class. The last thing we are going to do in this
chapter is add test our text class by calling it from our GameMain and drawing
some text. In order to use it in our GameMain we need to first include it, create
a variable to instantiate it to, and then call it in our render loop with some
text to draw. As I did above, I will mark the additions in bold. Starting with
the GameMain header, we include dxText.h, and declare a variable to hold our
object.
class dxMgr; class dxText;
class GameMain
{
public:
GameMain(void);
~GameMain(void);
bool init(HWND wndHandle);
void update(void);
private:
dxMgr* dxManager; dxText* textManager;
};
In the main file, we create and initialize the text class in GameMain's init
function
bool GameMain::init(HWND wndHandle)
{
dxManager = new dxMgr(); //create our direct Manager
dxManager->init(wndHandle, 800, 600, false); //Initialize our DirectX Manager
if (!dxManager){
return false;
}
textManager = new dxText(); //create our text Manager
textManager->init(32,dxManager->getD3DDevice()); //initialize our text
Manager
if (!textManager){
return false;
}
return true;
}
Lastly, we add a call to render some text in GameMain's update function .
void GameMain::update(void)
{
//call our update function
// begin rendering
dxManager->beginRender();
//game render calls go here textManager->drawText("Hello World", 30, 30, 300, 300);
//end render
dxManager->endRender();
}
That's everything, compile and run your project. Your previously blank window
should now be graced with the words "Hello World".
Summing Up - Chapter 2
In this chapter we have Created our window, and three useful classes - the
DirectX Manager, the GameMain framework, and the Text Manager. Before moving
on, you might want to play with some of the settings we called the classes with.
Run it fullscreen instead of in a window, or adjust the size of your window
and buffer. When we finish the project we may wish to add the option of selecting
windowed or fullscreen think about how we might do that. An important note about
fullscreen however is it makes debugging with breakpoints awkward because visual
studio doesen't pull itself to the front, so you have to alt-tab between them
which can be quite slow.