Page1, Page 2, Page 3, Page 4, Page 5, Page 6, Page 7

Game Programming Tutorial (Page 2)

This is page 2 of step by step guide to programming a 2D side-scrolling game with some 3D. Here' I'll show you how to build the skeleton of the game. We already finished the headers -- now it's time to move on to the .cpp files where we place most of our code/functions.   

STEP 10:

View your Solution Explorer and right click on Source Files>>Add>>New Item and select C++ File(.cpp) and call it Main and add the following includes(always good to save as you add stuff):

#include "headers.h"
#include "resource.h"
#include <Dxerr.h>
#include <atlbase.h>
#include "debug.h"

headers.h is where we'll add most of our headers just to keep things short and simple. Resource.h as you know is for the dialog box when the program quits and debug.h contains our error-to-file reporting definitions.

First we'll add MAIN which is the starting point of the program:

//------------------
// M A I N
//------------------
int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {
    
//We'll insert code here
return 0;
}//--------------------------------end main-------------------------------------------------------------

This is pretty much the standard way of starting your main cpp file function. It takes the handle for the program's instance from the Windows OS and possible command line and show commands. INIT_WINDOW definition will set our global g_instance to hInstC and call the init_window function - so we'll add this:

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {
    INIT_WINDOW;
    
//we'll insert more code here
    END_WINDOW;
return 0;
}

In order for this to actually work - we first must create our startup.cpp file which will contain the functions init_window and end_window... so we'll save this and start working on startup.cpp and come back to main.cpp after.

STEP 11:

View solution explorer and right click Source Files>>Add>>New Item and chose C++ File(.cpp) and name it Startup and add the following code (and save):

#include <stdio.h>
#include <tchar.h>
#include <d3d9.h>
#include <D3DX9.h>
#include "startup.h"
#define USE_HARDWARE_DEVICE

The above includes are needed for some of the functions below and startup.h we created which contains some definitions and prototypes used in here (and in main) for the compiler so it can figure out how to build the machine code
USE_HARDWARE_DEVICE - is defined so that if we compile the program - it knows to compile the version of code that specifies graphics hardware to be used -- if we comment it out: //#define USE_HARDWARE_DEVICE - it will compile so that the graphics processing is handled by the CPU and stored on normal RAM. We prefer to use the hardware cuz it's very fast and we'll be adding some 3D stuff eventually.

#define WM_MOUSEWHEEL 0x020A
#define WHEEL_DELTA 120

We might not use the above 2 definitions - but they come in handy if we want to handle the mouse wheel input.

//#define WINDOWED_MODE
//#define USING_LOCKABLE_BACKBUFFER

Yep - these are commented out - cuz we are not using windowed mode and are not using a lockable backbuffer. Full-screen mode is most ppl's preference for gaming so we'll stick to that -- our other code may need to be modified a bit if we switch to windowed mode - which eventually I might find time to show you.
Not using a lockable backbuffer just makes the graphics process slightly faster - though if done right, a lockable backbuffer won't cause noticable slowdown (it can come in handy if you want to make a haze effect or something).
[The backbuffer is basically the memory space on the GPU that holds the rendered image. When it's done rendering the image to the backbuffer - it flips it to the frontbuffer and starts making the next backbuffer image -- this way the animation is smooth and if slowdown were to happen - you wouldn't notice any weird drawing artifacts as it makes each frame(like little black lines flickering through the screen). ]

#define FatalError(p) {Problem=p;return false;}

I just added this locally because a fatal error here would simply mean returning from a function with a value of false to show that something went wrong. What went wrong is stored in Problem. ie: Problem="OMG this isn't working";

//EXTERNS
extern "C" char * Problem;
extern int g_WindowReactivate;
extern bool g_exit_game;
extern HHOOK g_hKeyboardHook; //for windows key disable:
extern BOOL g_bFullscreen;
extern BOOL g_bWindowActive;
extern bool left_mouse_clicked;
extern bool find_new_joysticks;

Note that the above all must be initialized in main_defs.h cuz they are extern... we did that already actually - and I already covered what these are for -- so let us move on...

//VARS
char * g_wnd_class_name = "My Game Company Name";
char * g_app_name = "Name of My Game";

^^We'll use these when creating our window that will contain our game.

STEP 12:

Let's make the function that actually creates the type of window we want for a full screen game:

//---------------------
// I N I T W I N D O W
//---------------------

bool init_window(HWND &g_hwnd,HINSTANCE &g_instance) {
ULONG window_width, window_height;
WNDCLASS window_class;
DWORD style;
    ZeroMemory(&window_class, sizeof(WNDCLASS));

This function takes the window handle and program instance handle (which are used when creating the actual window) and builds for as a full screen window where our game will exist. First we make our window class which describes to windows what kind of window we're building. We use ZeroMemory to clear the structure/class so all it's components are initially set to = 0.

#ifdef WINDOWED_MODE
   
 window_class.style = CS_HREDRAW | CS_VREDRAW;
    window_class.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

#else
    window_class.style = CS_OWNDC;
#endif
    window_class.cbClsExtra = 0; window_class.cbWndExtra = 0;
    window_class.hInstance = g_instance;
    window_class.hCursor = LoadCursor(NULL,IDC_ARROW);
    window_class.lpszMenuName = NULL;
    window_class.lpszClassName = g_wnd_class_name;
    window_class.lpfnWndProc = default_window_proc;

Since we're not using windowed mode - it only compiles the part after #else -- sets the class style to CS_OWNDC which is suitable for full screen mode. Make sure to set .hInstance to g_instance.. No menu is used - initial cursor is the arrow thingy - and we set the class name to what it was above "My Game Company Name" or whatever.. Important to set the WndProc - we will program it below - we called it default_window_proc -- it's a callback function that gets windows messages and decides how to handle them.

Now we need to register this window_class with windows -- basically just tell windows how we want this window to work. Also we store the window dimensions (to be used locally - in this .cpp file)

    
if(!RegisterClass(&window_class)) FatalError("Error registering window class");
    window_width=GetSystemMetrics(SM_CXSCREEN); window_height=GetSystemMetrics(SM_CYSCREEN);

GetSystemMetrics(SM_CXSCREEN) basically gets the current width of the display. (SM_CYSCREEN for height)
Now we'll set up some variables for creating the actual window.

#ifdef WINDOWED_MODE
    
style=WS_OVERLAPPEDWINDOW;
#else
    style=WS_EX_TOPMOST | WS_POPUP | WS_VISIBLE;
#endif

We'll be using a window that covers anything and everything for full screen mode.

#ifdef WINDOWED_MODE
    
RECT clientArea = {0, 0, window_width, window_height};
    AdjustWindowRect(&clientArea, style, false);
    g_hwnd=CreateWindow(g_wnd_class_name, g_app_name, style,         CW_USEDEFAULT, CW_USEDEFAULT, clientArea.right-clientArea.left,
        clientArea.bottom-clientArea.top,NULL,NULL,g_instance,NULL);
    if(!g_hwnd) {Problem="Error opening window";return false;}

#else
    RECT clientArea = {
0, 0, window_width, window_height};
    AdjustWindowRect(&clientArea, style, false);
    g_hwnd=CreateWindow(g_wnd_class_name, g_app_name, style,
0,0, window_width, window_height,
        NULL,NULL,g_instance,NULL);
    
if(!g_hwnd) {Problem="Error opening window";return false;}
#endif
    ShowWindow(g_hwnd,SW_MAXIMIZE); UpdateWindow(g_hwnd);SetFocus(g_hwnd);
return true;
}
//INIT_WINDOW

If we were using windowed mode - we'd get width by subtracting client area sides.. but since we're not - we just use 0,0 to window_width, window_height -- if it didn't work - we return false with an error message.. (should work)
Last - we make sure the window is shown and has focus -- focus refers to which window the user is using(ie: input)

STEP 13:

We'll also need something to shut down our window when we're done with it:

//---------------------
// E N D W I N D O W
//-------------------
--
void end_window(HWND &g_hwnd,HINSTANCE &g_instance) {
    
if(g_hwnd){
       
 if(!DestroyWindow(g_hwnd)) {
            OutputDebugString(
" Failed to DestroyWindow\n");
            MessageBox(NULL,
"Destroy Window Failed",g_app_name,MB_OK|MB_ICONERROR|MB_TOPMOST);
        } else {
            MSG msg;while(PeekMessage(&msg, NULL, 0, 0,PM_REMOVE)){DispatchMessage(&msg);}
        }
        g_hwnd=NULL;
    }

    
if(!UnregisterClass(g_wnd_class_name,g_instance)) {OutputDebugString("Failed to Unregister Window\n");
        MessageBox(NULL,
"Unregister Failed",g_app_name,MB_OK|MB_ICONERROR|MB_TOPMOST);
    }
}
//END_WINDOW

If the window exists we are free to destroy it - if it succeeds (else), then we remove any remaining messages from the queue. Should always set an object to NULL when it's no longer valid.
Now we unregister the class/style and we're done... er wait - I guess we still need to program the default_window_proc which we promised windows would exist for accepting windows messages for processing.. Let's do this now:

(STEP 12.5):

This function receives messages in msg and wparam and lparam -- which we can react to... observe my awesome power...........................................
// D E F A U L T  W I N D O W  P R O C
LRESULT CALLBACK default_window_proc(HWND hwnd,UINT msg,WPARAM wparam,LPARAM lparam) {
switch(msg) {
    
case WM_SYSCOMMAND: {
        
switch (wparam & 0xFFF0) {
            
case SC_SCREENSAVE: return 0; //prevent it from happening
            case SC_MONITORPOWER: return 0;
        }
        
break;
    }
Here we prevent screensavers or monitor power management from messing with our game^^

    
case WM_LBUTTONDOWN:
        left_mouse_clicked=
true;
   
 break;
    
case WM_LBUTTONUP:
        left_mouse_clicked=
false;
    
break;
    
case WM_DEVICECHANGE:
        find_new_joysticks=
true;
    
break;

Here we notify functions in the main loop through extern variables(cuz it's in another module) that the left mouse has been clicked (so check what it clicked on) - or that a new device has been attached to the computer (like a game controller - so check out what it was and set it up if need be)

    
case WM_CLOSE: REPORT("close"); g_exit_game=true; PostQuitMessage(0); return 0; break;
    
case WM_DESTROY: REPORT("destroy"); g_exit_game=true; PostQuitMessage(0); return 0; break;

A message has been dispatched to shut down our game and close the window.

    
case WM_ACTIVATEAPP:
       
 if( wparam == TRUE ) {
            
if (!g_bWindowActive) g_WindowReactivate=1; //begin
            g_bWindowActive =
true;
        }
    
    else {
       
     if (g_bWindowActive) g_WindowReactivate=2; //stop
            g_bWindowActive =
false;
        }
    
break;

If the window has just activated - then we set a global extern variable which will notify code in main that the window has reactivated -- so reload any lost graphics or music and reset everything and unpause the game..
If it is unactivating - then we set it to 2 which tells the main code to pause all game activity and hang out for a while until the user resumes the game (this usually happens when another program tries to take over or if the user hits Alt-tab -- maybe to change a track to listen to on their CD)


    }
return (DefWindowProc(hwnd,msg,wparam,lparam)); //don't ask...
}
//default_window_proc
void message_pump(void){
    MSG msg;
   
 if(PeekMessage(&msg, NULL, 0, 0,PM_REMOVE)){TranslateMessage(&msg); DispatchMessage(&msg);}
}
The message pump just checks for new messages and sends info to the default_window_proc for processing...

Make sure to save this file (Startup.cpp)..

STEP 14:

Before we go back to Main.cpp to add INIT_D3D and END_D3D, let's stay in startup.cpp and add the functions that make these things happen:

#define NN(p1,p2) {if (!p1) FatalError(p2);}
#define NF(p1,p2) {hr=p1; if(FAILED(hr)) FatalError(p2);}
//-----------------------------------
// I N I T D 3 D - init_d3d_simple
//-----------------------------------

bool init_d3d_simple(LPDIRECT3D9 &g_d3d, HWND &g_hwnd, HINSTANCE &g_instance,
    int &g_width, int &g_height, DWORD &g_bpp, D3DFORMAT &g_common_format,
    LPDIRECT3DDEVICE9 &g_gpu, LPDIRECT3DSURFACE9 &g_zbuffer,
    D3DPRESENT_PARAMETERS &d3dpp) {

INIT_D3D define passes all the variables needed in this function - the d3d interface, the window handle, the program instance, the variables that will hold the width and height of the display (note that by using & symbol - any asignment to a variable here is actually effecting the original variable) the var that holds bits per pixel, the color format, the pointer to the GPU class, the pointer to the z-buffer, and the var that will hold the d3d present parameters structure.. (I explain more in a bit)

    HRESULT hr;
    UINT screen_width=
800;  // you can adjust these to other valid screen dimensions if you like
    UINT screen_height=
600;

Above I initialize screen_width and screen_height - but we could also make a function to find valid screen resolutions and let the user decide - or just let the computer select an optimal resolution for the game... We'll see about maybe adding code for that later -- but this is ideal for most 2D games right now so we'll just use 800x600 for now..

    if (g_d3d==NULL) g_d3d=Direct3DCreate9(D3D_SDK_VERSION); NN(g_d3d,"Error getting Direct3D")

Create the direct3D object (if not already existing).

    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.BackBufferCount = 1;
    d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE;

One backbuffer is fine - we don't need a chain

#ifdef WINDOWED_MODE
    
d3dpp.Windowed = TRUE;
#else
    d3dpp.Windowed = FALSE;
#endif
#ifdef USING_LOCKABLE_BACKBUFFER
    
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIP;
    d3dpp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;

#else
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
#endif

Not using windowed mode - nor lockable backbuffer - so using discard (supposed to be faster)

    d3dpp.hDeviceWindow=g_hwnd;
    d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
// set the back buffer format to 32-bit
    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
    d3dpp.BackBufferWidth = screen_width;
    d3dpp.BackBufferHeight= screen_height;
    g_width = screen_width; g_height = screen_height;
    d3dpp.MultiSampleType=D3DMULTISAMPLE_NONE;
    g_bpp=
32; //32 bits per pixel
    g_common_format=D3DFMT_X8R8G8B8;
    
// create a device class using this information and the info from the d3dpp struct
    DWORD Behavior=D3DCREATE_SOFTWARE_VERTEXPROCESSING;
//I like to be able to play with vertices a lot
#ifdef USE_HARDWARE_DEVICE
    
if(FAILED(g_d3d->CheckDeviceType(D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,
    D3DFMT_X8R8G8B8,D3DFMT_X8R8G8B8,TRUE))) Problem=
"Unable to check this device.";
    
else {
        D3DCAPS9 d3dCaps;
        g_d3d->GetDeviceCaps(D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,&d3dCaps);
        
if(d3dCaps.DevCaps&D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
           
if(d3dCaps.DevCaps&D3DDEVCAPS_PUREDEVICE) {
               Behavior=D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE;
//add flags
           }
           
else if (d3dCaps.DevCaps&D3DDEVCAPS_TLVERTEXSYSTEMMEMORY) {
                Behavior = D3DCREATE_MIXED_VERTEXPROCESSING;
           }
       }
     }
    NF(g_d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, g_hwnd,
        Behavior, &d3dpp, &g_gpu),
"Error creating device\n")

Above I used a function from the Direct3D interface (g_d3d) called CheckDeviceType to see if we have a graphics accelerator (HAL instead of REF) available for the pixel format X8R8G8B8 -- and if so we use GetDeviceCaps to see if it's capable of hardware based transformation and lighting (should be) and then we set our Behavior variable based on what it can do -- if it can do purely hardware based stuff we add those flags and if it uses system memory (normal RAM) for transformation and lighting then we set it as mixed vertex processing. Most adapters / gpu's can do pure..
If it did not have a failure then we can go ahead and create the "device" (the g_gpu - which is used for most of our interaction with the graphics processing unit). (hr can be checked to see what kind of failure happened if u want to)

#else
    
NF(g_d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, g_hwnd,
    Behavior, &d3dpp, &g_gpu), "Error creating device\n")

#endif

Since we're using hardware acceleration - the compiler will skip the above bit of code (reference rasterizer setup)

    hr=g_gpu->CreateDepthStencilSurface(screen_width,
// the z-buffer width
        screen_height,
// the z-buffer height
        D3DFMT_D16,
// 16-bit pixel format
        D3DMULTISAMPLE_NONE,
// no multi sampling
        0,
                  // no multi sampling quality
       TRUE,
            // discard old buffer data
       &g_zbuffer,   
// the address of the buffer
       NULL);
          // reserved...we don't use this.
   if(FAILED(hr)) FatalError(
"Unable to create stencil buffer.");

We use the above gpu function to create a depth stencil surface (z-buffer) -- which the hardware uses to determine which pixels will be visible in the final screen render (ones farther away should be covered up by ones closer)

return true;
}
//INIT_D3D - init_d3d_simple

STEP 15:

Now lets write a function to shut down D3D:

//--------------------------
// E N D D 3 D - end_d3d
//--------------------------

void end_d3d(LPDIRECT3DDEVICE9 &g_gpu,LPDIRECT3D9 &g_d3d) {
    SAFE_RELEASE_FINAL(g_gpu);
    SAFE_RELEASE_FINAL(g_d3d);
}//end_d3d

Uses our definitions we made before to safely release the d3d and gpu interfaces. The reference count should be zero on exiting -- but sometimes it shuts these down after (I don't know why).
(Make sure to save...)

STEP 16: (setup MAIN)

Now we're ready to add INIT_D3D and END_D3D to Main.cpp:

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {
    INIT_WINDOW;
    INIT_D3D_SIMPLE;
    
//insert more code here
    END_D3D;
    END_WINDOW;

return 0;
}

Click File>>Save All -- now hit F7 to build the code - check the output window and you should see 0 errors and 0 warnings. If all is well hit F5 or debug>>start debugging -- to test it out.. If all is well it should start a new window which will go black for a split second and exit immediately.. This is what we expect for now - because we haven't added a main loop to wait for a keypress -- and we haven't added any graphics yet..

Let's go ahead and add the memory leak checker stuff and reset the debug.txt file (this stuff will come in handy later for finding bugs)

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {
    BEGIN_MEMORY_LEAK_CHECK;
    RESET_REPORT;
    
INIT_WINDOW;
    INIT_D3D_SIMPLE;
    
//insert more code here
    END_D3D;
    END_WINDOW;
    SHOW_MEMORY_LEAK_DATA

return 0;
}

When you try this now it show show you 2 warning windows: Debug Warning! program blablabla...bla.exe and then it should be blank (no actual memory leaks) - All it should say is Examine Outstanding Memory Leaks (dump memory leaks): and then a bunch of **************** and press retry to debug and 3 buttons - abort, retry, ignore -- just hit ignore on both warning windows -- or hit abort if you aren't interested in checking for other memory leaks. If it says something like Memory leaks detected and then a bunch of hexidecimal number stuff - then you have a leak -- at this point you won't have any leaks. debug.txt should also be a blank document cuz we haven't reported anything.

Now let's set up the main game loop:

#define TIMING_DELAY 15

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {
    BEGIN_MEMORY_LEAK_CHECK;
    RESET_REPORT;
    INIT_WINDOW;
    INIT_D3D_SIMPLE;
    
//---MAIN_LOOP--->
    
while(!g_exit_game) {
        DWORD starting_point = GetTickCount();        
        message_pump();
        
if (g_WindowReactivate==1) { //begin
            g_WindowReactivate=
0;ShowWindow(g_hwnd,SW_MAXIMIZE);
            UpdateWindow(g_hwnd);SetFocus(g_hwnd);
continue;
        }
        
if (!g_bWindowActive) continue;
        
//ALL INPUT GOES HERE!!!
        
if (KEYPRESS(ESCAPE)) {
            PostMessage(g_hwnd, WM_DESTROY,
0, 0);
        }
        
do{}while ((GetTickCount() - starting_point) < TIMING_DELAY);
    }
//<---main_loop---
    END_D3D;
    END_WINDOW;
    SHOW_MEMORY_LEAK_DATA;

return 0;
}

while(!g_exit_game) { } <--loops anything in here until g_exit_game=true but as long as it's false(0) then keep doing..
You'll notice that if the ESCAPE keypress occurs, then we post a message to destroy the window (which when default_window_proc receives that - it will set g_game_exit to true which will cause the loop to exit) - then END_D3D and END_WINDOW will happen...
At the start of the loop - I get the start point for the timer... then at the end of the loop after using up time rendering and such(which we're not doing yet), we check to see if we should wait a little longer before proceeding to the next frame in the loop. If it is processing too fast - sometimes the game will speed up and get all weird and stuff -- so we do this to obtain a consistent frame rate. I set the TIMING_DELAY to 15 -- though I've used other values in the past (like 10 for super fast and smooth - and 20 or even 25 for a choppier animation) - I've found 10-15 to be a good range for smooth animation.
Again - the message pump is needed here to make sure system messages (like joystick found) are processed.
If g_WindowReactivate is 1 (just reactivated) then it must make sure the window is active and in focus (we will also use this for fading in music and such later).
If the window is not active -- make sure to pause all game activity by using continue to go back to the start of the loop..

At this point - the Windows Key can screw up our game or even freeze things up (if this happens hit Alt+Tab,Enter,shift+F5 again and again until it quits) To prevent this annoying problem - let's disable the windows key..

Add this between INIT_WINDOW and INIT_D3D_SIMPLE:

INIT_WINDOW;
//for windows key disable:
g_hKeyboardHook=SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), 0);
INIT_D3D_SIMPLE;

and add this between END_WINDOW and SHOW_MEMORY_LEAK_DATA:

END_WINDOW;
UnhookWindowsHookEx(g_hKeyboardHook);
SHOW_MEMORY_LEAK_DATA

Only problem is we haven't yet programmed LowLevelKeyboardProc - so first save main.cpp and open up startup.cpp where we will program LowLevelKeyboardProc which will absorb the windows key press.

//for disabling windows key
// L O W L E V E L K E Y B O A R D P R O C

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode < 0 || nCode != HC_ACTION ) // do not process message
        return CallNextHookEx( g_hKeyboardHook, nCode, wParam, lParam);
    bool bEatKeystroke = false;
    KBDLLHOOKSTRUCT* p = (KBDLLHOOKSTRUCT*)lParam;
    switch (wParam) {
        case WM_SYSCOMMAND: {
            switch (wParam & 0xFFF0) {
                case SC_SCREENSAVE: return 0;//prevent it from happening
                case SC_MONITORPOWER: return 0;//prevent it from happening
            
}
            break;
        }
       case WM_KEYDOWN:
       case WM_KEYUP: {
           bEatKeystroke = (g_bFullscreen && g_bWindowActive && ((p->vkCode == VK_LWIN)
               || (p->vkCode == VK_RWIN))); break;       
        }
    }
    if(bEatKeystroke) return 1;
    else return CallNextHookEx( g_hKeyboardHook, nCode, wParam, lParam );
}//LowLevelKeyboardProc

If it's the right type of message, we check the wParam and mask it against 0xFFF0 to see if it's a system command to start the screensaver or to power down the monitor -- if so we cancel that.. We also set p to be the lParam cast as a KBdllHOOKstruct - then check it's vkCode to see if it's one of the windows keys was used in the game -- if so we set the boolean, bEatKeystroke to tell the system to eat that keystroke basically(return 1)-- otherwise we just return normally.

You'll notice now - if you run the program - the windows key does nothing - which is what we want..

Let's also add the dialog box we made earlier which shows any Problem if one exists (otherwise it says: 0 errors)...
Save and go back into main.cpp and add this between UnhookWindowsHookEx and SHOW_MEMORY_LEAK_DATA:

UnhookWindowsHookEx( g_hKeyboardHook );
if (Problem[0]!='0') {
    message_pump();
    DLGPROC lpproc=(DLGPROC)MakeProcInstance((FARPROC)DisplayProblemList, hInst);
    DialogBox(g_instance,MAKEINTRESOURCE(IDD_DIALOGBAR),NULL,lpproc);
}
SHOW_MEMORY_LEAK_DATA;

You may remember that we set Problem="0 errors" in main_defs.h - so initially the first character will be 0. This way it only shows error messages if it finds one cuz the first character will be something else. We call message_pump once to prevent accidentally closing the dialogbox right away - then create an instance of the procedure: DisplayProblemList which we programmed earlier -- we then create the dialog box using its corresponding ID (as it is in resource.h).
If you want to see the dialog box even when there aren't errors - you can try it out by commenting out the
//if(Problem[0]!='0') { and the closing curly bracket: //} -- but if you want to make things simpler and look a bit better, you can replace the entire if statement with a definition (probably best to place this definition in main_defs.h):

#define SHOW_PROBLEMS if (Problem[0]!='0') {message_pump(); \
    DLGPROC lpproc=(DLGPROC)MakeProcInstance((FARPROC)DisplayProblemList, hInst); \   
    DialogBox(g_instance,MAKEINTRESOURCE(IDD_DIALOGBAR),NULL,lpproc); }

Then in main.cpp you can do this instead:

UnhookWindowsHookEx( g_hKeyboardHook );
SHOW_PROBLEMS;
SHOW_MEMORY_LEAK_DATA;

We'll try not to use too many #defines, but I like to use them every once in a while to hide code that I already know to be very well tested and clean up any annoying clutter.

STEP 17:

Before we start this step - go into main_defs.h and add this #define to the list:
#define FATAL(p1,p2) if (Problem[0]=='0') FatalError(p1); else FatalError(p2);
(it sometimes makes it easier to find the actual cause of the bug)

We need to add a function that sets up the GPU for rendering graphics the way we want them.
For 3D stuff - we will want lighting on and zenable true and we'll set a dark grey color as the minimal illumination that a polygon can have (ambient).
Then we'll initialize a light or 2 (probably just one for now) and set up our camera in the 3D world.
We'll also need to indicate the field of view and how 3D images will be projected onto our 2D screen.

Next let's add the following function to main.cpp a few lines under the end of MAIN. (we already made a prototype for this in main_defs.h to help out the compiler)

#define SET_D3D(p1,p2) g_gpu->SetRenderState(p1, p2);
//--------------------------------
// I N I T  R E N D E R  V A R S
//------------------------------
--
void InitRenderVars() {

Above the function declaration you can see I added a define which just means less typing in this function. SetRenderState is basically how we tell direct3D we would like to render. You can check DirectX documentation for a big list of render-state options.

    SET_D3D(D3DRS_LIGHTING, TRUE)
    SET_D3D(D3DRS_ZENABLE, TRUE)
    SET_D3D(D3DRS_AMBIENT, D3DCOLOR_XRGB(100,100,100))

The only render-states I'm interested in for now is lighting, zenable, and ambient(cuz we'll be working mostly with 2D graphics). We'll add more later if need be...

    init_lights();

We'll make the init_lights function later. It will set colors, starting positions, and behaviors for any lights we want.

    //setup camera:
    ViewDesc.Position.x=0.0f; ViewDesc.Position.y=0.0f; ViewDesc.Position.z=600.0f;
    ViewDesc.Target.x=ViewDesc.Target.y=ViewDesc.Target.z=0.0f;
    ViewDesc.xa=ViewDesc.ya=0.0f;
    ViewDesc.uni_scale=1.0f;

If you remember - we made a VIEWDESC struct and ViewDesc object in main_defs.h.. The point of it is to keep track of where the camera is and what it's looking at. For now all we'll need to use is position and target. The other stuff is more for making it easier to smoothly follow a 3D character around in a 3D world -- don't worry about that for now.

    D3DXMatrixIdentity(&ViewDesc.RotationMatrix);
    D3DXMatrixIdentity(&ViewDesc.TranslationMatrix);
    D3DXMatrixIdentity(&ViewDesc.ScaleMatrix);

NEVER forget to always initialize your matrices as an identity matrix -- if you don't things can get really screwy. If you need to know more about how a matrix works or how matrix multiplication works you should google it -- but all you really need to know for now is that a matrix is used for translating or rotating coordinates in 3D (or 2D). It is important to know - you can combine a size-change("scale), rotation, and position change("translation") into a thing called a matrix. When doing so you should always make sure to rotate the object to face in whatever direction first, then do the position change after. If you first did a translation(position change) and rotation after - it would rotate the object around the point it started at -- (which would be ok if that's what you intended..) which would be like having it sticking out on the end of a pole and turning it(usually not what we want).
You should also scale the size of the object before moving it too.
The point of a matrix is it combines all the math we want to do to a vertex(a 3D point) and does it in one operation - and the matrix can be applied to all the vertices(points) in the entire 3D object we want to manipulate. We can send a Matrix to the GPU and it will apply the matrix operation to whatever object we specify(whether it's 3D or 2D -- in our case mostly 2D - so it's really working on 2 triangles that make up 1 rectangle which contains our image/texture[4 vertices]). Each 3D object is either 1 vertex list or we can pick sub-sections of vertices of a vertex-list to work on... but I digress...

    D3DVIEWPORT9 vp; vp.X = 0; vp.Y = 0; vp.Width = g_width; vp.Height = g_height; vp.MinZ = 0.0f; vp.MaxZ = 1.0f;     g_gpu->SetViewport(&vp);

The above makes a Direct3D viewport which we set to the screen dimensions(we set g_width and g_height in startup.cpp) - pixels will have z values scaling from 0.0f to 1.0f...

    D3DXMatrixIdentity(&g_world_matrix);
    D3DXMatrixIdentity(&g_viewing_matrix);
    D3DXMatrixIdentity(&g_projection_matrix);

Now we initialize our matrices for world, viewing and projection. The world matrix is used when calculating 3D world coordinates/position/orientation -- I'll explain these matrices more later if we decide to do something more 3D..
Viewing matrix is the final matrix we pass to the gpu for setting the camera view and the projection matrix determines how the perspective will look(ie: field-of-view) - which we might decide to play with a bit for certain effects.

   D3DXMatrixLookAtLH(&g_viewing_matrix,&ViewDesc.Position, &ViewDesc.Target, &D3DXVECTOR3 (0.0f, 1.0f, 0.0f));
   D3DXMatrixPerspectiveFovLH(&g_projection_matrix,D3DXToRadian(45),(float)g_width/(float)g_height,NEAR_Z,FAR_Z);

D3DXMatrixLookAtLH - is a d3dx function which creates our global viewing matrix based on the position and target we specified when setting up ViewDesc. We put the camera at x=0, y=40, z=180 and pointing at x=0, y=0, z=0 -- so it's actually up 40 and 180 into the distance and looking at 0,0,0 -- I originally set this up as a starting point in a 3D game thingy I was working on so that the camera was looking slightly down at a character standing at 0,0,0 -- in this 2D game, where we put the camera won't make much difference (not yet anyway...)
(note I use the LH versions cuz usually the 3D geometry we load will be constructed based on left-hand rule - which has to do with the order vertices are used to determine whether the polygon is facing toward or away from the camera(polygons facing away are not drawn[unless set as 2-sided]- speeding up the rendering process)) The perspective field of view function uses the angle and width and height and the near and far clipping planes to determine how to project 3D onto the 2D screen. Basically if you imagine that a vertex is a point on a screen and each screen is further away from you(depending on its Z value) - the size of the screen gets smaller as it gets further away -- so basically it is scaled toward the center of your screen as you make it look further away. The vertex is scaled inside that screen too as it get's further. That's moreless how points are put into perspective in 3D to show it on a 2D display. Don't worry though - the GPU will do the calculations for us... all we need to do is specify how warped we want our field of view to be. 45 degrees (3.1415926f/180.0f*45.0f - or we just use D3DXToRadian(45) to get the proper value in radians) gives us a reasonable FOV (field of view) - it gives us a 90 degree viewing angle(up-down,left-right) -- most of this we don't need to worry about if we are working strictly in 2D - but we'll play around with some 3D later...

    g_gpu->SetTransform(D3DTS_VIEW,&g_viewing_matrix);
    g_gpu->SetTransform(D3DTS_PROJECTION,&g_projection_matrix);

The above functions actually hand off the matrices we want to use for viewing orientation/position and perspective projection to the GPU so it can calculate the rest for us using these.
Now we create our g_sprite object which is used for processing 2D graphics - scale, rotate, draw, etc...
We'll also create a couple different font objects that will work in our accelerated graphics display setup.

    //create 2D graphics processing objects:
    
if (!g_sprite) {
        if (FAILED(D3DXCreateSprite(g_gpu, &g_sprite))) FATAL("D3DXCreateSprite FAILED",Problem);
        
D3DXFONT_DESC FontDesc = {20,0,800,0,false           ,DEFAULT_CHARSET,OUT_TT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_PITCH,"Arial"};
        D3DXFONT_DESC FontDesc2 = {12,0,800,0,false           ,DEFAULT_CHARSET,OUT_TT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_PITCH,"Arial"};
             //^^- just check D3DXFONT_DESC in DirectX documentation for details on setting up fonts
        D3DXCreateFontIndirect(g_gpu,&FontDesc,&g_font);
        D3DXCreateFontIndirect(g_gpu,&FontDesc2,&g_font2);
    }

}//InitRenderVars

Nice thing about these d3dx objects is they are really fast and really handy. You will learn to love d3dx sprites (^__^)
We better add the global variables needed by this function... Go up above MAIN and add them like this:

#define TIMING_DELAY 15

//<-------------------------------------------------------------------- insertion point---------------------
D3DCOLOR text_color = 0xFF008811; //<--I chose dark green with slight hint of blue for a starting text color
/*note: D3DCOLOR is DWORD - 32bit number -- every 2 hex numbers make up one of: alpha, red, green, blue - so alpha = FF = 255, red = 00 = 0, green = 88 = 136, blue = 11 = 17 (16*hex#+hex# = integer -- in integer form, each channel ranges from 0-255 (or 0-FF in hex))*/
bool DeviceLost=false;
LPD3DXSPRITE g_sprite = NULL;
RECT text_position;
LPD3DXFONT g_font = NULL;
LPD3DXFONT g_font2 = NULL;

TCHAR* g_current_directory;
unsigned int g_directory_buf_size, g_size_of_directory = 260;
D3DLIGHT9 g_light;
float LPX=50.0f,LPY=50.0f,LPZ=0.0f; //light position
D3DXMATRIX g_viewing_matrix;
D3DXMATRIX g_projection_matrix;
D3DXMATRIX g_world_matrix;
D3DXMATRIX g_matrix_2d;

//^^--insert above main with some blank space here-----------------------------------^^>

//---------------
// M A I N
//---------------

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) extcode">
D3DXMATRIX g_viewing_matrix;
D3DXMATRIX g_projection_matrix;
D3DXMATRIX g_world_matrix;
D3DXMATRIX g_matrix_2d;

//^^--insert above main with some blank space here-----------------------------------^^>

//---------------
// M A I N
//---------------

int WINAPI WinMain(HINSTANCE hInstC,HINSTANCE hInstP,LPSTR lpCmdLine,int nCmdShow) {

... Just a brief explaination of the variables we inserted above:
text_color  - we'll use this to change the font color whenever we feel like it
DeviceLost - set true when control of the graphics card is lost(at which point we try to regain control and restore gfx)
g_sprite     - object used for controlling/drawing sprites
text_position - we can set the area on the screen where text/font will appear..
g_font       - a font or text style that works in our display mode
g_font2     - another type or size of font
g_current_directory - keeps track of the directory/path used for loading and saving files (ie: "C:\mygame\")
g_directory_buf_size - not sure if we'll actually use this but it takes the returned buf size when we get the directory info
g_size_of_directory - the number of characters we use to hold the directory name/path
g_light      - a light to be used on 3D stuff
LPX, LPY, LPZ - global light position - we won't really use these until we start using point lights or spot lights
g_viewing_matrix - matrix for controlling camera view (technically it modifies world so it's relative to camera orientation/pos)
g_projection_matrix - matrix for controlling camera FOV and projection from mathematical 3D universe to 2D display
g_world_matrix - final matrix which effects 3D world orientation and coordinates of all points(vertices) in all objects
g_matrix_2d - this is what we will use to control scale, rotation, and position of our 2D sprites on the screen! (^_^)

I guess we're gonna need to make that init_lights function now before we forget:

STEP 18:

Just place this code between our MAIN function and the InitRenderVars function we just made (that way we don't need to make a prototype cuz the compiler sees this function before we have a call to it in InitRenderVars..) (remember too - it is always a good idea to insert 4 or 5 lines of blank space between each function so you can spot them easier)

// I N I T   L I G H T S
void init_lights(void)
{
//LIGHT_TYPE D3DLIGHT_SPOT D3DLIGHT_POINT //DIFFUSE_RGBA //AMBIENT_RGBA //SPECULAR_RGBA //Light Direction //Light Position //RANGE //Attenuation 1,2,3 //Falloff,Theta,Phi //light#
    CREATE_LIGHT(g_light, D3DLIGHT_DIRECTIONAL,
        
1.0f,1.0f,1.0f,1.0f, 0.3f,0.3f,0.3f,0.3f, 0.6f,0.6f,0.6f,0.4f,
        1.0f,-1.0f,-1.0f,
LPX,LPY,LPZ, 3000.0f, 1.0f,0.0f,0.0f, 0.12f,0.898f,1.047f,1);
return;
}

Here we could use a spot, point or directional light -- I chose directional cuz it's less complicated that way. (you can look these up in D3DXDocumentation D3DLIGHT9 (also check out SetLight and LightEnable). I chose a bright white light for diffuse lighting, a dark grey for ambient lighting(minimum lighting), and a light grey for specular light -- I used 1,-1,-1 for light direction but u might like to try something different - LPX, LPY, LPZ are just global vars which I use for keeping track of the light position(more useful if we use a point light or spot light), 3000 is the maximum distance range of this light and the rest of these vars are more useful for spot light stuff - I might explain more on this in the future - also I set it as light #1...
This light will come in handy later when we add 3D stuff to our 2D game..

Now go back to WinMain function (MAIN) - and on the line right after INIT_D3D_SIMPLE; insert the call: InitRenderVars(); -- should look like this:

INIT_D3D_SIMPLE;
InitRenderVars();
//<--- inserted
//---MAIN_LOOP--->

While we're at it - let's insert these 2 as well:

INIT_D3D_SIMPLE;
InitRenderVars();

LoadGraphics(true); //<--- inserted
InitVars();
//<--- inserted
//---MAIN_LOOP--->

So we setup D3D, setup the GPU with viewing and lighting stuff, and now we need load some graphics from file and setup some game variables too...

STEP 19:

Let's start the actual function for loading graphics (but not load anything yet):

//----------------------------
// L O A D   G R A P H I C S
//----------------------------

void LoadGraphics(bool restore_all) {
if (restore_all) {
   
  //insert base-graphics loading here (like character animations and stuff) - only need to load once normally
}
//insert level loading graphics stuff here -- selects graphics to load based on what level or place we are at
}
//LoadGraphics

Basically what this does is if restore_all is set - we load all graphics from scratch (like of the game just started or if DeviceLost happened) - otherwise we just load new graphics for whatever level(or area) we're at...

STEP 20:

Let's add the InitVars function and start by turning off the cursor and storing the current game directory

//----------------------------
// I N I T V A R S
//----------------------------

void InitVars() {
    ShowCursor(FALSE);        
    g_current_directory =
new TCHAR[g_size_of_directory]; //DIRECTORY
    g_directory_buf_size = GetCurrentDirectory(g_size_of_directory, g_current_directory);

}
//InitVars

Just turn off the cursor, allocate memory for the name of the current directory and then get the directory (ie: g_current_directory = "c:\MyGame\" -- this could be handy if we change directory and need to come back..

STEP 21:

Always make sure to update your CleanupGraphics function whenever you make a new object or graphics item. Don't do anything else when you make one until you've done this. (ie: Textures, Sounds, Music, Fonts, etc... ) - Without doing this - you should receive memory leak warnings and when you check the Output screen in VisualStudio - it may also indicate unfreed memory -- which could cause problems...

//----------------------------------
// C L E A N U P  G R A P H I C S
//----------------------------------

void CleanupGraphics() {
  //release graphics:
  //...
  //release objects:

  SAFE_RELEASE_FINAL(g_font);
  SAFE_RELEASE_FINAL(g_font2);
  SAFE_RELEASE_FINAL(g_sprite);
  SAFE_RELEASE_FINAL(g_zbuffer);
 
 if (DeviceLost) return; //don't free stuff after this until the game exits... (if this was called because of lost device - just return)
  SAFE_DELETE(g_current_directory);
  ShowCursor(TRUE);
}
//CleanupGraphics
You may remember we defined a SAFE_RELEASE and SAFE_DELETE stuff in common.h

Next we'll make some graphics to load and we'll begin with a layered(parallax) scrolling background.

CLICK >> PAGE 3 << to continue to the next page of this tutorial.

Return to Home Page