A DirectDraw Game in C++
A practical look at Microsoft's new high-speed graphics API

By Gyorgy Grell and Paul Modzelewski  

The previous Power Programming column gave an overview of what DirectDraw does and how to use it ("Using DirectDraw with C++," June 25, 1996); now it's time for a more practical look at Microsoft's new high-speed graphics API for Windows 95. In this article, we'll discuss the reusable C++ classes we devised to encapsulate DirectDraw and the game we created using those classes (Figure 1).

Our main goal in developing the game was to demonstrate that DirectDraw and C++ make an excellent team for multimedia programming. We have created a number of classes, mostly based on the Microsoft Foundation Classes (MFC). A basic understanding of Windows programming, MFC, and C++ is assumed.

The basis of our game is contained in two classes and their descendants: CGameApp and CGameFrame. We decided to get rid of the document and view classes created by Microsoft Visual C++'s AppWizard, since we didn't need the overhead of the document/view architecture. CGameApp is a standard CWinApp descendant. Instead of creating a document template that would create the main window, we directly create a CFrameWnd ourselves in CGameApp's InitInstance function.

    m_pMainWnd = GetNewFrame();
    ASSERT(m_pMainWnd != NULL);
    m_pMainWnd-> ShowWindow(SW_SHOWNORMAL);

Notice the call to GetNewFrame, a virtual function that creates a new instance of the current application's main window class. This will come into play when we create our own descendants of CGameApp and CGameFrame.

The most important function of our application class is Run, which is called by standard MFC processing after all application initialization has finished. In Run, we do any game initialization and then loop until the PumpMessage function returns TRUE, the signal that the application should quit. We use PeekMessage to check for any queued messages; if it returns TRUE, PumpMessage takes an existing queued message and forces it through MFC's processing:

    if (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
    {
    // pump message, but quit on WM_QUIT
    if (!PumpMessage())
    {
    GetGameFrame()->KillGraphics();
    GetGameFrame()->DestroyWindow();
    return ExitInstance();
    }
    }

The next part of the Run loop is where we call CGameFrame's UpdateGame function, which updates the game internals, performs the rendering, and flips the video pages:

    if (m_bPlaying)
    {
    if (!GetGameFrame()->UpdateGame())
    {
    StopPlay();
    PostQuitMessage(0);
    }
    }
    else
    {
    ::WaitMessage();
    }

You will see that the real meat of UpdateGame is actually in CSpaceFrame. Start- Play and StopPlay are two other important functions for restarting and pausing the game. In CGameFrame's ActivateApp function, you'll notice that we call StopPlay when the application loses focus and StartPlay when it gains focus. You'll notice that StopPlay sets CGameApp's member m_bPlaying to FALSE, which will prevent UpdateGame from being called. When the application gains or loses focus, OnActivateApp is called: This is where CGameApp's StartPlay and StopPlay are called. When the application loses focus, we call StopPlay, and when it gains focus, we call StartPlay; thus the application will not take up any cycles when it is in the background.

CGameFrame encapsulates our main window creation and functionality. It includes the basic data members needed by all game applications: a DirectDrawSurface that points to the main flipping surface (m_pFrontBuffer), a pointer to our drawing surface (m_pBackBuffer), and our palette for the game (m_pGamePalette). As we noted in the first article, all rendering is done on the drawing surface, which is then flipped into view for smooth animation.

We create the main window in the constructor. First we register our own window class using AfxRegisterWndClass in order to change the default cursor, icon, and background brush. We set the cursor to NULL so that it won't show up; the icon is loaded from the resources, and the background brush is set to black so that the main window will be less obvious before our first page flip. Next we create the window using CreateEx. Note that we create windows with different attributes for debug and release versions. We found that using a small, overlapped window would allow us to debug the application more easily, whereas in release mode, the window is a topmost, pop-up window whose size is set to the screen's resolution. Why do we use the window style WS_SYSMENU? Because we want our game to show up on the Taskbar when we switch to another application; if we don't give the window the WS_SYSMENU style, the Taskbar won't show the application's icon.

The InitGraphics function contains all the setup and initialization of DirectDraw that we talked about in the first part of this article. Another essential function is RestoreSurfaces, which is needed after a switch to another application. When a switch occurs, GDI regains its control over video memory. Therefore, when our game resumes, we need to call RestoreSurfaces on all our surfaces to reclaim their video memory and then copy images back onto the surfaces if needed.

Since we create the main window with the WS_SYSMENU style, the window has a menu. Because of this, if the user were to press the Alt key, the game would stop and the system menu would get focus. But we might want to use the Alt key for control. In order to allow that, we created the OnSysCommand function, which catches and discards menu keys (Alt or F10), passing on any other keystrokes to the default message handler.

In addition to the MFC-derived classes, we wrote CAnim, a straight C++ class to handle animation under DirectDraw. This class has no dependencies on MFC, and removing the #include "stdafx.h" from the beginning of animation.cpp will allow you to use it in any DirectDraw program. The following snippet shows how you would typically construct a CAnim:

    m_pShipAnim = new CAnim(m_pDD, "ship", "media/", 32);

The first parameter is a pointer to the application's DirectDraw object. The second and third parameters tell the application where to find the bitmap files that contain the individual frames. The second parameter is the root of the filenames, each of which must consist of three elements: a four-character root identifying the group to which the frame belongs, followed by a zero-padded, four-digit identifier indicating the frame's position in the animation sequence (starting with 0000), and a .BMP extension. The third parameter is the directory in which the files are located. The fourth parameter is the number of frames in the animation, and a fifth parameter is a Boolean that defaults to TRUE; this parameter determines whether CAnim will attempt to load the images into video memory. If there is not enough video memory to store the images, they will go into system memory. Likewise, if the fifth parameter is explicitly set to FALSE, then the images will be loaded into system memory. The above snippet would load the files ship0000.bmp to ship0031.bmp from the subdirectory media and store the images in video memory if space is available. It is important to make sure that all the images in a given animation have the same height and width.

Constructing a CAnim does not load the images or allocate memory for image storage; all of that actually happens in the Load function, so you must call the Load function before using a CAnim. The Load function actually calls the protected member function CAnim::InternalLoad, which is also called by CAnim::Restore. The InternalLoad function creates a single DirectDrawSurface, onto which all the individual images in the animation are blitted in a grid. CAnim::Render then indexes into that grid based on the number of the frame to be drawn. Here is the prototype for CAnim::Render:

    HRESULT Render(int nX, int nY, int nFrameNum, LPDIRECTDRAWSURFACE pDestSurface, BOOL bTransparent = TRUE);

The first two parameters are the X and Y coordinates in pixels of the top-left corner of the animation. The third parameter is the number of the frame to be drawn, followed by the destination surface (usually the back buffer). The final parameter is used to set the transparency of the blit operation and defaults to TRUE, or transparent. The return value is the HRESULT from the destination surface's BltFast function. The primary reason for returning this value is to determine if the surface has been lost. You should always compare the returned HRESULT with DDERR_SURFACELOST, and restore all of your surfaces if they match.

Restoring the surface of a CAnim is as easy as calling the Restore function. Restore will first call IDirectDrawSurface::Restore on the surface that it owns, getting back the surface's memory. Restore then calls CAnim::InternalLoad(TRUE), which will recopy the image files into the newly restored surface. CAnim::Release simply calls IDirectDrawSurface::Release on CAnim's surface, and this should be done prior to destruction of the C++ object. (Forgetting to call this function usually won't matter, since the main DirectDraw object releases all the surfaces and palettes it has created when it is released, but explicitly calling it is good programming practice.) CAnim also has three getters--GetNumFrames, GetWidth, and GetHeight--whose usage is self-explanatory.

Our game is simple and doesn't need a lengthy explanation. You are the purple ship; shoot all the boxes on the screen to progress to the next level, where you will have to destroy even more boxes. Run into a box and you lose a life. Lose three lives and the game is over. The Left and Right Arrow keys rotate your ship, the Up Arrow accelerates, the Down Arrow stops the ship, Ctrl fires, and Esc quits the game. The number at the top of the screen is the current frame rate of the game in frames per second. This is just an indication of how smoothly the game will animate; the user has no control over frame rate.

In order to run the game, you must have the Games SDK runtime files on your system. They are installed by any game that uses the Games SDK, so if you have already installed such a game, you'll be able to run the program. To compile the sample code, you will need Visual C++ 4.x and the Games SDK, which is now included in MSDN Level II and in Visual C++ 4.1.

So where is all the game-playing code? We decided to use the features of C++ and create some classes that are descended from our existing frame and application classes. That way we could break out generic work into CGameApp and CGameFrame and place our game-specific code into CSpaceApp and CSpaceFrame. We probably could have broken it out even further, but we didn't want to go crazy with OOP-ness. You'll notice that CSpaceApp consists only of GetNewFrame and the global object theApp. We decided not to put this code into CGameApp so that it would be clear where the generic code ends and the game-specific code begins.

CSpaceFrame uses CGameFrame as the base of its functionality, overriding several key virtual functions, and adding quite a few nonvirtual functions specific to the implementation of our game. CSpaceFrame::InitGraphics first calls CGameFrame::InitGraphics, which sets up our DirectDraw object, sets the graphics mode, creates our primary flipping surface, and calls InitPalette. CSpaceFrame::InitGraphics then creates two DirectDrawSurfaces, one for displaying the frame rate and another for any other text that we display on the screen. We also construct and load all of the CAnims that we use in the game here. The Restore function in CSpaceFrame calls CGameFrame::Restore to restore the primary surface's memory, then restores the frame rate and text surfaces, as well as calling Restore on all of the CAnims.

In order to handle game processing, CSpaceFrame also overrides UpdateGame. There is no need to call the base class version of UpdateGame, since it simply returns TRUE. This is essentially our main game loop, called at regular intervals by CGameApp::Run, and it is really the guts of our game. The actual implementation of UpdateGame is fairly simple, since all of the complex work is farmed out to helper functions. The essence of the process is as follows:

    * Move the sprites.
    * Check for collisions.
    * Fill the screen with black to erase the previous frame.
    * Draw the sprites.
    * Flip pages.

We used a classic doubly linked list of structures to represent sprites. Here is the definition from SpaceFrame.h:

    typedef struct _SPRITE
    {
    struct _SPRITE* pNext;
    struct _SPRITE* pPrev;
    SpriteType    eType;
    double      dPosX, dPosY;
    double      dVelX, dVelY;
    int       nDirection;
    int       nFrame;
    int       nDelay;
    CAnim*      pAnim;
    } SPRITE;

The first two variables are pointers to the next and previous sprites in the list, and the list wraps, so pNext of the last sprite points to the head of the list, and pPrev of the head points to the last sprite. The eType member is used to determine what type of sprite is represented by a given node; the possible values are typeShip, typeEnemy, and typeBullet. The nDelay variable is used to slow the rate at which enemies and bullets are animated and controls the rate at which bullets are fired by the ship.

Several functions in CSpaceFrame are used to handle the sprite list. MoveSprites loops through the entire list of sprites, changing position, velocity, and frame number information as necessary. Each type of sprite is handled slightly differently; for example, when updating the ship, MoveSprites polls the keyboard to determine the sprites' movement. CollideSprites contains all of the collision-detection code, and behaves somewhat like MoveSprites in that it loops through the list and processes each type of sprite in a slightly different manner.

When a collision occurs, CollideSprites takes the appropriate action, depending on the types of sprites involved. DrawSprites is used to draw all the sprites onto the back buffer. It loops through the entire list, calling the Render member function of the CAnim owned by each sprite. The functions AddSprite and RemoveSprite are used to add and remove sprites dynamically from the list, and m_head is the head of the list and the player's ship, which we store on the stack.

You can download the game and all of the source code from PC Magazine Online (see the sidebar "Guide to Our Utilities" in this issue's Utilities column for downloading instructions). If you want to build the application, here's how:

    * Unzip the archive using -d to keep the hierarchy structure intact.
    * You need Visual C++ 4.x to compile the app. The debug build uses the DLL version of MFC (for faster links), and the release version uses the static linking MFC.
    * The DirectDraw lib and header directories need to be added to the search path through Tools | Options... under the Directories tab. The MEDIA subdirectory contains all of the sprite bitmaps.

There are many exciting ways to use the power of C++, particularly when combining Visual C++ 4.x with DirectDraw. Since DirectDraw and all the other components of the Games SDK are based on COM objects, they lend themselves to C++ encapsulation rather