CD3DGame implements the core application framework for a DirectX game. I based it largely off of the d3dapp class bundled with the DirectX SDK. It's a base class - to write a game with it, you have to create a derived class that actually implements the virtual functions (HandleInput(), Update(), Render(), etc) from CD3DGame. See the StarControl class for details on that.

 




Just a few useful macros here. CLAMP reassigns x, if necessary, whereas CLAMPED does not modify x and can be used in an expression.

   

/* helper macros */
#define CLAMP(x, l, r)          { if ((x)<(l)) x = l; else if ((x)>(r)) x = r; }
#define CLAMPED(x, l, r)        ( (x) < (l) ) ? (l) : (x) > (r) ? (r) : (x)
#define MAX(a,b)                (((a) > (b)) ? (a) : (b))
#define MIN(a,b)                (((a) < (b)) ? (a) : (b))

/* math constants */
#define _PI                     3.141592653589
#define _E                      2.718281828459

 

 

 

 

 

The DebugMsg function pops up a message box with a rather verbose debugging message. Any time you're calling a DX library function (or anything that returns HRESULT) and your code requires that it succeed, wrap it in the DEBUG_CALL macro so the app crashes gracefully.

   

/* error handling */
#define ERROR_MSG( strMsg, hr, bExit )                                          \
    g_pD3DGame->DebugMsg( __FILE__, (DWORD)__LINE__, strMsg, hr, bExit );

#define DEBUG_CALL( procCall )                                                              \
{                                                                                           \
    HRESULT __dchr;                                                                         \
    if( FAILED( __dchr = procCall ) )                                                       \
    g_pD3DGame->DebugMsg( __FILE__, (DWORD)__LINE__, "CALL:\t" #procCall, __dchr, TRUE );   \
}

 

 

 

 

 


GetString returns a string, given its ID in the string table resource.

Switch to the 'Resources' pane in VC++ and find the string table resource. All important string constants should go here (such as paths into the game.dat file, in-game labels and messages, etc).

IMPORTANT: Take a look at the implementation - for convenience, the strings that GetString returns are stored in a static array within the function. Because of that, the contents of the string it returns are temporary; if you're not planning on throwing it away immediately, copy the string to new memory.


   

//-----------------------------------------------------------------------------
// Helper functions
//-----------------------------------------------------------------------------
LPSTR GetString( UINT uID );

 




Two important global variables. A HINSTANCE is essentially a Windows handle to the process running the application. Lots of API functions ask for the app instance. For instance, the LoadResource() function requires it in order to find the resources (bitmaps, icons, strings, dialog templates) in your process's memory space that were loaded from the executable file.


   

//-----------------------------------------------------------------------------
// Globals
//-----------------------------------------------------------------------------
class CD3DGame;
static CD3DGame* g_pD3DGame     = NULL;     // provides global access to the app
static HINSTANCE g_hInstance    = NULL;     // provides global access to the app instance

 

 

 

 

When you want to have the user interact with an interface widget, call SetShowCursor(TRUE) and monitor dxInput::MouseButtonClk(). For things like clicking buttons, where you need the absolute mouse position, use the windows input variables instead of dxInput.

If you're not familiar with it, 'volatile' tells the compiler that the variable might be modified by more than one thread. Important for the network code...


   

//-----------------------------------------------------------------------------
// Class CD3DGame
//-----------------------------------------------------------------------------
class CD3DGame
{
protected:  // Flags
    BOOL volatile           m_bActive;
    BOOL volatile           m_bReady;
    BOOL volatile           m_bDisplayReady;
    BOOL volatile           m_bHasFocus;

    BOOL                    m_bWindowed;
    BOOL                    m_bUseHWCursor;
    BOOL                    m_bUseDepthBuffer;
    BOOL                    m_bShowFPS;
    BOOL                    m_bShowCursor;
    BOOL                    m_bClipCursor;

 

 

 

 

 

 

 

 


Variables for mouse and keyboard input. These are updated by Windows, not DirectInput. The only time these should be used is for the mouse when you need absolute screen coordinates.


   

BOOL m_bKeyDown[256]; // Fallback input state from MsgProc()
BOOL m_bKeyPressed[256];
BOOL m_bKeyReleased[256];
BOOL m_bMouseLBDown;
BOOL m_bMouseMBDown;
BOOL m_bMouseRBDown;
DWORD m_dwMouseX;

 

 

 



Some more member variables. Important ones are the fonts (peruse d3dfont.h to see how to render them), m_dFrameTime (we can use this to adjust the LOD of game objects at runtime), m_dxInputMgr (unlike the other member variables, you don't need protected access to CD3DGame to be able to access this instance of the input class; more on this in dxinput.html), and m_dxIniEngine (this is the INI file reader object for \cfg\Engine.ini - feel free to add new categories and constants to the file and read them using this object).

m_dxFileMgr is the reader for the resource file (game.dat in the BIN directory). For those who don't know, a resource file is like a WAD file - it's a bundle that contains many other files arranged in virtual directories. Nukefile is a free library I found on the web. To use it, you first have to add files to game.dat, which is accomplished with the little utility I bundled in with the source code. To extract files, the easiest thing to do is use the ExtractFile function, which places the file in a temporary directory until the app closes, and returns a path to it. You can also load a file directly into memory.


   

    CD3DFont*               m_pFontFW;              // Fixed-width font
    CD3DFont*               m_pFontStd;             // Standard variable-width font
    CD3DFont*               m_pFontSm;              // Small variable-width font

    SET_DEVOBJ              m_setDevObjs;           // List of loaded device objects

    DOUBLE                  m_dFrameTime;           // Last frame time
    DOUBLE                  m_dFrameRate;           // Instantaneous frame rate

    dxInput*                m_dxInputMgr;           // The DirectInput interface
    dxIniReader             m_dxIniEngine;          // INI reader for engine settings
    dxBillboard2D           m_sprCursor;            // The software mouse cursor sprite

#ifndef NONUKEFILE
    CNukeFile*              m_dxFileMgr;            // The resource file
#endif

 

 

 

 

 

 

 



You can ignore all but the last three functions. ShutDown immediately destroys the app, and spins in the message loop until it closes - it DOES NOT return. DisplayErrorMsg is handy - pass TRUE to the boolean parameter if you want the app to shut down after the message is displayed. DebugMsg shouldn't be used directly; call it using the debugging macros defined above.

To see how the program actually gets up and running, look at StarControl.cpp. I placed WinMain (the windows entry point) at the top of the file.


   

public:   // Exposed functions
    CD3DGame();

    // handles windows messages
    LRESULT MsgProc( HWND, UINT, WPARAM, LPARAM );

    // initialize the CD3DGame
    HRESULT Create( HWND, HINSTANCE );

    // start the main game loop
    DWORD   Run();

    // stop the app
    VOID    ShutDown();

    // error handling
    DWORD   DisplayErrorMsg( DWORD, BOOL );
    HRESULT DebugMsg( LPSTR, DWORD, LPSTR, HRESULT, BOOL );

 

 

 

 

 

 

 

 



This is the first set of functions that need to be overridden in the derived class. Just like GLUT...


   

protected:  // Overridable functions for the 3D scene created by the app
    virtual HRESULT HandleInput()           { return S_OK; }
    virtual HRESULT Update()                { return S_OK; }
    virtual HRESULT PreRender()             { return S_OK; }
    virtual HRESULT Render()                { return S_OK; }
    virtual HRESULT OneTimeInit()           { return S_OK; }

    virtual HRESULT InitGameObjects()       { return S_OK; }
    virtual HRESULT DeleteGameObjects()     { return S_OK; }

    virtual HRESULT FinalCleanup()          { return S_OK; }

 

 

 

 

 

 

Here's the second set of virtual functions, which requires a lot of explanation.

The first four deal with "device objects." You've probably had the experience where someone sends you a message on AIM while you're playing a game, which causes the game to minimize, and then you can't alt-tab back into it without your game getting screwed up. Sometimes you don't hear sound (Counterstrike anyone?), or the graphics would be all screwed up (Everquest).
The special effects book talks about this on page 281 -- when you work with graphics objects (as well as sound objects), chances are you want them to be loaded into the memory on your video or sound card instead of system memory. If the app is running fullscreen and the user switches to another task, by the time your app regains focus, chances are all the textures and other data that were loaded into vid memory are now gone and need to be refreshed from system memory or from disk. RestoreDeviceObjects takes care of that.

After the app regains focus (we know when this happens because Windows sends our MsgProc a WM_ACTIVATE message -- see the bottom of d3dgame.cpp), the first thing RestoreDeviceObjects does is call the Reset() function of the direct3d device. Direct3D has a lot of built in support for restoring device objects; any mesh, texture, or other kind of object that you created with the 'Managed' memory flag will automatically be restored from a system memory copy when Reset is called.

To help with device objects, I've created a base class called dxDeviceObject. All classes we make that use device memory in any way should be derived from and implement its interface. Once you make such a class, whenever you instantiate it, you should "register" the object. If you look at the code for RegisterDeviceObject, you'll see I'm just maintaining a STL set of dxDeviceObjects; when the app receives a WM_ACTIVATE message, MsgProc calls CD3DGame::RestoreDeviceObjects, which iterates through this list and restores each object. Similarly, before you delete an object you've instantiated, be sure to call UnRegisterDeviceObject on it. InvalidateDeviceObjects and DeleteDeviceObjects also iterate through the list. InitDeviceObjects does not (wouldn't be very useful if it did).

In the dxDeviceObject-derived class you write, think of InvalidateDeviceObject as paired with RestoreDeviceObject; Invalidate should "prepare" for a device losing focus, and everything it does should be reversed by Restore. A lot of the time it can just be empty. Treat DeleteDeviceObject as the destructor for your class.

SetStateBlock sets up the rendering state of the device. DirectX has an object called a state block that compiles a bunch of successive calls to SetRenderState - SetRenderState is basically the DirectX equivalent of glEnable. When you're setting up how you want things textured and shaded, you can save it in a state block, and it's more efficient to execute the state block than call SetRenderState a bunch of times. I intend SetStateBlock to set up the "default" rendering state.


   

    // Device object management
    virtual HRESULT InitDeviceObjects();
    virtual HRESULT RestoreDeviceObjects();
    virtual HRESULT InvalidateDeviceObjects();
    virtual HRESULT DeleteDeviceObjects();

    virtual HRESULT RegisterDeviceObject( dxDeviceObject* );
    virtual HRESULT UnRegisterDeviceObject( dxDeviceObject* );

    virtual HRESULT SetStateBlock();

 

 

 

 

 

 

DrawFPS and DrawMouseCursor get called every tick - if you want to display the framerate, flip m_bShowFPS. If you want to display the mouse cursor, call SetShowCursor().

ExtractFile, as described above, extracts a file from the game.dat file and stores it in a temporary directory. The one that takes a UINT is expecting an ID from the string table.


   

protected:  // Utilitiy functions
    VOID    DrawFPS( FLOAT, FLOAT );
    VOID    DrawMouseCursor( FLOAT, FLOAT );

    VOID    PageFlip();
    HRESULT ResizeDisplay();

    // extract a file from the resource archive
    LPSTR   ExtractFile( UINT );
    LPSTR   ExtractFile( LPSTR );


    VOID    SetShowCursor( BOOL );
    VOID    SetClipCursor( BOOL, LPRECT = NULL );

 

 

 

 

 

 

 

 

HandleMessages() polls the event queue for the process and passes incoming events to their handlers. When you need to have the application wait for something to happen (network ACK, semaphore, etc), make sure you stick a call to this in your while loop to prevent the app from locking up.


   

private:    // Internal
    BOOL    HandleMessages();
    HRESULT LoadEngineSettings( LPSTR );
};

 

 

 

That's it!