CS-360 Fall, 2000 Class 4X R. Eckert DirectX and Windows Game Programming Game Programming Until 1995 there were no really "good" Windows games. The best games ran only under DOS. It is really ironic that the GUI-based Windows operating system is so slow at handling graphics. DOS enables the game programmer to access the system's video memory directly thus allowing graphics applications to run very fast. Windows, on the otherhand, uses its Graphics Device Interface (GDI) to attempt to provide a device- independent environment which requires all graphics handling to go through the GDI library of useful, but generally slow graphics functions. Under standard Windows, no access is permitted to the actual video hardware. This makes high-speed visual games next to impossible. Fast games with high-resolution animated graphics are extremely demanding for a PC. For example, a 640 X 480 screen in which each pixel's color is determined by an 8-bit value requires a video memory of almost one-third a megabyte. Imagine one of these 256-color images as the background scene in a flight simulation game. Because the background will change in each frame as the aircraft moves, the program must repeatedly transfer new images to the screen. The minimum rate would have to be 15 new images per second to provide a continuous animation. This means about 5 megabytes of graphical data transferred to the video memory every second. But things are even worse. In most games there are small objects that move around on top of the constantly-changing scene. These objects (called sprites) might include things like the player's on-screen character and/or his enemies. Sprites are also graphical objects that must also be transferred to the screen memory. So the program must not only transfer the background scene to the screen 15 times a second, but it must also transfer each sprite, one by one, onto this image. And the above description ignores flicker which can make sprites constantly appear and disappear. As discussed earlier, flicker can be avoided by composing a complete scene in memory before transferring it to the screen. But this essentially doubles the amount of graphical data that must be transferred because the sprites and background scene must first be placed in one memory block and then that whole block must be transferred to the screen. The GDI BitBlt() and StrecthBlt() functions are hopelessly inadequate to cope with this task. Microsoft was very much aware of the inadequacies of the Windows GDI in coping with fast game applications, and in 1995 came out with something it called the "Game SDK." Subsequently renamed "DirectX," the Game SDK consists of a series of components called "COM objects." COM (Component Object Model) is an object-oriented interface developed by Microsoft for creating objects at the operating system level. A COM object so closely resembles a C++ class that, when programming in C++, the programmer can access it in exactly the same way as a C++ class. DirectX consists of the following components: DirectDraw--Provides direct control over the computer's video hardware. In particular, it enables programs to very quickly transfer graphics between memory and the screen. It is also designed to take advantage of hardware capabilities that may be present on the video card. And if certain capabilities are not available on the video card, DirectDraw can emulate them in software. DirectSound--Provides an almost device-independent method for directly dealing with the computer's sound card. It enables the programmer to easily add sound effects and music to games and to synchronize sound effects with events occurring on the screen. It also can handle 3D sound effects. DirectInput--Provides for easy use of joystick and other game controller devices in a device-independent way. DirectPlay--Provides for the implementation of multiuser games over a network or modem. "DirectPlay provides a transport-independent, protocol-independent, and on-line-service-independent way for games developed for Windows to communicate with each other." Direct3D--Provides optimized three-dimensional capabilities to Windows games. It also enables games to take advantage of 3D acceleration hardware, if available, without any additional coding by the game developer. DirectX games run under Windows, which means that they can benefit from all the built-in Windows functionality. In other words, they can use the GDI graphics functions, all of the Windows user interface capabilities, all of the fonts and other standard Windows drawing objects, and, in general, the entire Windows Win32 API. DirectDraw-- The main purpose of DirectDraw is to provide directly-accessible drawing "surfaces" in memory and the ability to transfer those drawing surfaces quickly to the screen. A surface is a block of memory used for drawing. Usually a separate surface is used to hold each sprite in an animated scene and another to hold the background. These are then composed into a final image and transferred to the primary screen surface. The following programming steps are normally required in order to use DirectDraw in a Windows program (check the online help for details on the use of each DirectDraw function): 1. Call DirectDrawCreate() to create a DirectDraw object. 2. Call the DirectDraw object's SetCooperativeLevel() member function to get exclusive control over the screen resolution and palette. 3. Call the DirectDraw object's SetDisplayMode() member function to set the screen's resolution and color depth. 4. Call the DirectDraw object's CreateSurface() member function to create at least a primary surface and probably one or more secondary drawing surfaces (called back buffers). 5. Call the primary DirectDrawSurface object's GetAttachedSurface() member function to acquire a pointer to a back buffer. 6. Call the back buffer DirectDrawSurface object's Lock() member function to obtain a pointer to the back buffer surface's memory. 7. Draw an image on the back buffer. 8. Call the back buffer DirectDrawSurface object's Unlock() member function to tell DirectDraw that the program is done with the back buffer. 9. Call the primary DirectDrawSurface object's Flip() member function to swap the surface memory associated with the primary surface and that of the next back buffer surface, thus displaying the newly-drawn image. 10. When terminating the application, all direct draw objects should be removed by calling their Release() member functions. The LINESMINIMUM DirectDraw Example Application-- This example creates a 640 X 480 X 8-bit-color primary surface, draws 256 horizontal lines (using the current palette) on a back buffer attached to this surface, and flips surfaces so the lines are displayed on the screen. It is a Win32 API program that has no menu. The action occurs in response to the user pressing the <F1> keyboard key. A press of the <ESC> key terminates the application. The application also keeps track of and displays the time required to draw the lines on the back buffer and that required for the surface switch. The program uses the various DirectDraw member functions in the simplest ways possible; in a more robust "real" application, extensive error checking would be done after most of the function calls. (Refer to the references.) A class we've called CDirDraw (specification in cdirdraw.h, implementation in cdirdraw.cpp) does most of the work in this example. The class defines a pointer to a DirectDraw object and two pointers to DirectDraw surfaces. Its constructor performs steps 1 through 5 (above), its destructor performs step 10, and it has two member functions that do most of the rest of the work (steps 6 through 8). The ChangeColor() member function fills the entire back buffer with the integer given as its parameter. This is a color index, and in the call that is made to the function from the WndProc() in the main module (horlines.cpp), the value 0 is used. In the default palette for this color mode, 0 corresponds to black. The DrawLines() member function actually draws the lines by filling the appropriate memory locations in the back buffer with the numbers 0 through 255--all of the color indices in the 8-bit-color mode being used. The key idea here is that any pixel on the back buffer can be set to a given color by setting the buffer position corresponding to that pixel to the given color index value. If, for example we wanted to set the pixel at (x,y) to color c, we would do the following: buf[y*pitch + x] = c; where buf is the address of the start of the back buffer and pitch is the number of pixels in each row of the back buffer. The address of the start of the back buffer and its pitch are obtained by calling the back buffer object's Lock() function, one of whose parameters is a pointer to a DirectDraw Surface Descriptor (DDSURFACEDESC) structure. The back buffer start address and pitch are two of the several members of this structure. After drawing the lines on the back buffer, step 9 above (flipping the surfaces to make the back buffer visible) is done in the main program's WndProc() by making a call to the DirectDraw object's Primary Surface Flip() function. In order to display the times involved for these operations, TextOut() is called. But TextOut() (as well as any of the other GDI functions) requires a handle to the Device Context that is associated with the primary surface. This handle is obtained by making a call to the GetDC() member function of the DirectDraw object's Primary surface. The API function wsprintf() is used to format the integer time information into a string (cBuf) that can be displayed by TextOut().