44 High-Performance Graphics and Sound: The Game SDK
Early in my career I had the opportunity to spend almost a year as a member of a programmer team working on games for the Commodore 64 home computer. Although we barely earned enough to eat, I have the fondest memories of this period. I don't think I ever had as much fun with computers as I did then, designing green dragons, writing a real-time executive, coding Bartok's Allegro Barbaro on the C64's primitive sound synthesizer, and learning about high-performance drawing algorithms. Game programming is probably the most technically challenging form of programming for desktop systems, and the joy of seeing your creation come to life is a reward by itself.
Games under Windows are as old as Windows itself; I vaguely remember playing Reversi with a machine that had Windows 1.0 installed. However, creating true arcade-style games with real-time animation and sound under Windows proved to be an elusive target until now. That despite the fact that the benefits of using the Windows platform for game development are enormous. Under Windows, you no longer need to contend with the multitude of graphics accelerators, sound cards, TSR drivers, and the rest of the paraphernalia that make developing even the simplest graphic application under MS-DOS a nightmare. Windows' device-independence takes care of it all.
One reason game development under Windows has lagged so far behind the DOS platform is speed. The GDI, although great on device-independence, is not very efficient; updating a window of a relatively large surface area takes a considerable amount of time.
In the past, Microsoft has attempted to provide high-efficiency graphics libraries and other tools for game developers. However, nothing compares to their latest product, the Windows 95 Game SDK. At the heart of this product is the family of DirectX libraries for graphics, sound, joystick control, and communications (for multiple-player games). These libraries are based on Microsoft's Common Object Model, the foundation of Microsoft's OLE technology.
The Game SDK is not part of Visual C++; it is available separately, as part of the Microsoft Developer Network Level 2 subscription.
The Windows 95 Game SDK consists of a series of APIs. The names of these APIs begin with the word Direct, and thus they are collectively referred to as the DirectX APIs.
The first of these APIs, DirectDraw, provides a low-level, high-speed interface to your computer's graphics hardware. Through DirectDraw, it is possible to write games that use real-time animation in a window; the API also provides the capability to write full-screen graphical applications.
The DirectSound API provides low-level access to your computer's sound hardware. The most important capabilities of this API include the ability to play sounds continuously in a loop and the ability to mix sounds.
The DirectPlay API provides a game-oriented communication interface to facilitate multiplayer gaming across a modem, network connection, or through an online service.
The DirectInput API provides improved, more reliable access to joystick services.
Lastly, the DirectSetup API provides a single-function setup capability for DirectX run-time components.
The DirectDraw, DirectSound, and DirectPlay APIs provide services using the Common Object Model (COM). A COM object is exposed to the outside world through one or more interfaces; each interface is essentially a table of function pointers representing the object's methods. Thus, the interface resembles the implementation of a pure virtual class in C++; the actual implementations of these functions are provided by the object itself.
The similarity to C++ classes ends, however, when it comes to inheritance. Although it is possible to create a new interface using an old interface, the new interface will not inherit the implementation of the old interface's methods. For example, although all COM interfaces are derived from the basic interface, IUnknown, they must individually implement mandatory IUnknown methods.
COM interfaces are compatible with the implementation of C++ classes. The table of methods in a COM interface easily translates into virtual member functions of the C++ class representing the interface. When applications use COM interfaces from the C language, they must refer to the interface's virtual function table (vtable) explicitly. The DirectX API header files provide convenience macros for all DirectX methods.
As I mentioned, all COM interfaces are derived from IUnknown. The IUnknown interface has three methods. The first of these methods, QueryInterface, can be used to determine if a particular interface is present. AddRef and Release implement reference counting for objects as they are created, referred to from other objects, or released. These methods must be implemented by all IUnknown-derived COM interfaces.
By convention, the names of COM interfaces begin with the uppercase letter I. Also by convention, interface methods are referenced using a C++ syntax even though they can also be accessed from C using an explicit vtable reference.
DirectDraw is a client to the services provided by the DirectDraw HAL (Hardware Abstraction Layer) and HEL (Hardware Emulation Layer). DirectDraw is implemented in the dynamic link library ddraw.dll. The DirectDraw HAL implements device-dependent functions; it only implements functions that are supported by the device. For unsupported functions, the HAL simply reports that they are unavailable. The HAL performs no parameter validation; all parameter validation is performed by DirectDraw.
The DirectDraw HEL appears to DirectDraw just like the DirectDraw HAL and provides software emulation for capabilities not implemented in hardware. The DirectDraw HAL and DirectDraw HEL are implemented in the form of driver libraries like ati.vxd and atim32.drv).
The DirectDraw API provides access to low-level graphics services through a series of COM (Common Object Model) interfaces. These interfaces and their relationships are schematically depicted in Figure 44.1.
The first of these interfaces, IDirectDraw, represents the graphics hardware (accelerator card) in your computer. Applications that use DirectDraw services begin their operation by creating a DirectDraw object using the DirectDrawCreate function. DirectDrawCreate returns a pointer to an IDirectDraw interface. Before doing any other work, applications must also call IDirectDraw::SetCooperativeLevel. Applications that request exclusive access to the computer's graphics hardware through IDirectDraw::SetCooperativeLevel can change the video mode and implement full-screen graphics and animation; other applications are restricted to the current video mode and should confine their graphic activity to windows they own.
The IDirectDraw::CreateSurface method creates a DirectDrawSurface object and returns a pointer to an IDirectDrawSurface interface. DirectDrawSurface objects represent rectangular areas in memory (typically, video memory) where applications can draw. The primary drawing surface is the visible display surface; secondary drawing surfaces may exist in video memory or the computer's main memory and are used for off-screen drawing.
Drawing onto a surface is possible through any of a variety of bit blit methods provided by the IDirectDrawSurface interface or by using ordinary GDI functions. The IDirectDrawSurface::GetDC method can be used to obtain a device-context handle to the surface. GDI will treat this handle as a handle to a memory-device contexteven when the surface is the primary drawing surface.
IDirectDrawSurface supports smooth animation with back-buffer surfaces. The IDirectDrawSurface::Flip method can be used to switch the contents of the primary surface and the back-buffer surface. Applications can use the back-buffer surface to perform off-screen drawing, flip the two surfaces when drawing is finished, and start drawing the next frame in the back buffer. Of course, this is only one of several techniques that can be used to achieve smooth animation. If only small portions of the display surface are updated at any given time, another technique (such as the technique using multiple buffers implemented by the example shown later in this chapter) may be more beneficial.
Another interface, IDirectDrawPalette, provides access to palette services. This interface represents the hardware palette and bypasses Windows palettes. Because of this, use of IDirectDrawPalette requires exclusive access to the video hardware. IDirectDrawPalette can be used for many effects, including palette animation. Note that this interface may not be available on systems that do not support a hardware palette.
An IDirectDrawPalette interface is created by a call to IDirectDraw::CreatePalette. The palette must be attached to a display surface through IDirectDrawSurface::SetPalette.
The IDirectDrawClipper interface represents clip lists. A clip list can be attached to a DirectDrawSurface object and used during bit blit operations. A window handle can also be attached to a clip list, in which case the clip list represents the clipping rectangles of the window.
An IDirectDrawClipper interface is created by calling IDirectDraw::CreateClipper and attached to a drawing surface by calling IDirectDrawSurface::SetClipper.
The DirectDraw API can be used to represent not only the primary video hardware in your computer, but also secondary display devices that are not normally recognized by Windows. For example, consider a development system that has a secondary display card and monitor for testing purposes. An IDirectDraw interface representing this secondary device can be created by a call to DirectDrawCreate. The first parameter of this function represents the GUID (globally unique identifier) of a display driver; when it is set to NULL, DirectDrawCreate creates an interface representing the active display driver. However, you can also specify an explicit GUID representing any secondary device that may be installed on your system.
The DirectSound API provides access to your computer's waveform sound hardware. The sound device is represented by the IDirectSound interface; individual buffers are represented by IDirectSoundBuffer.
Note: DirectSound does not provide MIDI functionality. To utilize your sound hardware's MIDI capability, use the standard Win32 multimedia APIs for MIDI.
The most important DirectSound capability is wave mixing. It is accomplished by using a series of primary and secondary sound buffers.
A primary buffer represents the hardware buffer of the sound device; that is, the buffer that is currently playing. Secondary buffers may represent different audio streams that are mixed together into the primary buffer for playback. This mechanism is depicted schematically in Figure 44.2.
An IDirectSound interface is created by calling DirectSoundCreate. Before the interface can be used, you must also call DirectSoundCreate::SetCooperativeLevel to specify the level of access you require to the sound card. For most applications, this should be DSSCL_NORMAL. This level of access ensures smooth cooperation between applications that compete for the same hardware resources.
A sound buffer is allocated by calling IDirectSound::CreateSoundBuffer. An interface to the sound buffer is returned in the form of an IDirectSoundBuffer pointer. Applications do not normally need to allocate a primary sound buffer; this buffer is allocated implicitly when the contents of secondary buffers are played back.
When a secondary buffer is allocated, you must specify the size of the buffer. Afterwards, you can use IDirectSoundBuffer::Lock to obtain a pointer to this buffer. You can then use standard C library functions to copy waveform information into this buffer.
The content of a buffer is played back using the IDirectSoundBuffer::Play method. Calling this function while a buffer is playing will update playback flags but will not affect playback otherwise (for example, it will not cause playback to restart at the beginning of the buffer). To change the playback position, use IDirectSoundBuffer::SetCurrentPosition.
IDirectSoundBuffer::Play can also be used for continuous (looping) playback. This capability is ideal to provide background sounds, such as the engine sounds in an aircraft simulation game.
Game-oriented communication services are provided through the DirectPlay API.
The DirectPlay API consists of two components: the IDirectPlay interface and the DirectPlay server. Microsoft provides DirectPlay servers for modem and network connections; other servers (for example, servers for online services) are provided by third party developers. To find out what DirectPlay servers are installed on a computer, use the DirectPlayEnumerate function.
A DirectPlay object is created by the DirectPlayCreate function. You must pass the GUID of the selected DirectPlay server to this function. In turn, DirectPlayCreate returns a pointer to an IDirectPlay interface.
You can enumerate existing DirectPlay sessions by calling IDirectPlay::EnumSessions. This method must be called after the DirectPlay object has been created.
You can create a DirectPlay session or connect to an existing session using the IDirectPlay::Open method. This method actually establishes the communication link. DirectPlay invokes the necessary user interface for configuring the communication protocol; for example, if a modem connection is requested, DirectPlay will invoke a dialog requesting the telephone number and other dialing information.
Games using DirectPlay must be identified by a globally unique identifier, a GUID. You can generate a GUID using the guidgen.exe utility that is part of Visual C++.
After you have connected to a session, you must create players. A player is created through IDirectPlay::CreatePlayer. You can obtain the list of players in the session by calling IDirectPlay::EnumPlayers.
To actually exchange messages between players, you can use IDirectPlay::Send and IDirectPlay::Receive.
Other methods exist for managing sessions, players, groups of players, and messages. Players are destroyed using IDirectPlay::DestroyPlayer; a session is terminated by calling IDirectPlay::Close.
The DirectInput API consists of joystick-related Win32 services. These include the joyGetPosEx function that can return position and button state information for joysticks with six degrees of freedom (axes) and 32 buttons. They also include functions for querying device capabilities: joyGetDevCaps, joyGetNumDevs, and joyConfigChanged.
The DirectSetup API provides a single-function setup capability for DirectX redistributable components.
When distributing applications that use the DirectX APIs, you must distribute with them the contents of the redist subdirectory that is part of your Windows 95 Game SDK distribution. The contents of the redist directory must be redistributed in unaltered form, in accordance with the license Windows 95 Game SDK license agreement. Included in this subdirectory are the DirectSetup DLLs that implement the DirectXSetup function.
DirectXSetup is called with parameters that specify the installation root path and flags that specify which DirectX components to install. Although it is possible to install only selected components, Microsoft recommends that you do not do so; disk space savings would be minimal due to interdependencies among the DirectX components.
The existence of DirectXSetup is more than a convenience. Because of the complexities of DirectX installation, developers should not attempt to perform a manual installation.
This application uses both DirectDraw and DirectSound services. Its execution begins in WinMain, where first a perfectly ordinary window class is registered and the application's window is created.
Next, DirectDrawCreate is called to create an IDirectDraw interface to the display hardware. This interface is used to create a primary display surface. Then a clip list is created using IDirectDraw::CreateClipper and attached to this primary surface. The window handle of the application's window is attached to this clip list, ensuring that subsequent bit blit operations into the primary surface will operate correctly when the window is partially covered.
Next, a series of secondary display surfaces is created. These secondary surfaces are used in a special algorithm that ensures smooth repainting of the bouncing ball. Two of these surfaces are initialized, one with the image of the ball (created through the GDI Ellipse function), the other with the background color.
When initialization of the drawing surfaces is complete, an interface to the computer's sound hardware is created using DirectSoundCreate. Two secondary sound buffers are then created using IDirectSound::CreateBuffer. One is used to store the audio stream for the background hum, the other stores the stream for the bouncing noise. Continuous playing of the background sound is started by calling IDirectSoundBuffer::Play with the DSBPLAY_LOOPING parameter.
Before we enter the application's main message loop, a call is made to SetTimer to set up a 100-millisecond timer. This timer is used to periodically update the bouncing ball's position and redraw the display.
When a WM_TIMER event is received, the application's window procedure calls the MoveBall function. This function calculates the ball's new position based on the global velocity values vx and vy. The ball's position is expressed in the form of integers running between 0 and 500; these are then translated into screen coordinates using the MulDiv function.
Redrawing the ball is a complex process involving three secondary buffers. Why is this complexity necessary?
Obviously, if we are to update the ball's position on the screen, we have to do two things: erase the ball at its old position and draw it at its new position. However, unless the ball moves very fast, chances are that its old and new positions overlap. Simply erasing the ball at its old position would cause unwanted flicker, as those screen pixels that are covered by the ball at both its old and its new position also turn briefly white.
To avoid this, instead of simply erasing the ball at its old position, we create a secondary buffer that represents the ball's old position. Into this buffer we draw portions of the ball that will be visible after the ball is at its new position. We copy this buffer at the ball's old position; we also copy the entire ball at its new position. Figure 44.3 shows a schematic representation of this multistep process. When your application updates only small areas of its window, this procedure may yield better performance than updating a back-buffer and using the IDirectDrawBuffer::Flip function.
The application also responds to keyboard events. The arrow keys can be used to accelerate or decelerate the ball in the horizontal or vertical direction. Hitting the Escape key causes the application to terminate.
The two sounds that the application uses are in the waveform files hum.wav and bounce.wav. These files are referenced in the application's tiny resource file, shown in Listing 44.2.
Note that in order to keep the application simple, some explicit assumptions were made as to the contents and structure of these waveform files. If you wish to utilize another file, you may be well advised to look at some of the Game SDK samples for ideas as to how to provide a generic parsing capability.
The application can be compiled from the command line using the C/C++ compiler of Visual C++:
Recognizing the need for a high-performance interface for real-time Windows game applications, Microsoft developed the Windows 95 Game SDK. The Game SDK provides a family of APIs, collectively referred to as the DirectX APIs, for access to the computer's video, sound, communication, and joystick hardware.
The basis for many elements in the DirectX APIs is Microsoft's Component Object Model. Several of the interfaces in the DirectX APIs are derived from the COM IUnknown interface.
Video hardware is represented by DirectDraw objects, accessed through the IDirectDraw interface. Through a DirectDraw object, applications can create drawing surfaces, palettes, and clip lists. The interface to drawing surfaces, IDirectDrawSurface, provides high-performance bit blit capabilities. The IDirectDraw interface can be used to manipulate the display hardware in a variety of ways; this includes exclusive access to the display hardware for games that provide full-screen graphics and animation. A special use of IDirectDraw is to provide access to secondary video hardware not normally recognized by Windows itself.
Access to sound hardware is provided through the IDirectSound interface. Applications typically use the sound hardware by creating a series of secondary sound buffers; the contents of these buffers are mixed into the primary buffer and played back by DirectSound. This wave mixing capability as well as the capability to play back in looping mode are the key game-related capabilities of DirectSound.
DirectPlay provides communication services for multiplayer applications. Such applications can communicate through networks, modems, or online services. DirectPlay provides a simple send-receive functionality for game programs that participate in game sessions.
The DirectInput API consists of Win32 joystick services such as joyGetPosEx that can return position and other information for multibutton, multiaxis control devices.
Setting up the DirectX API is a complex task. A single-function interface that performs DirectX setup from the standard redistributable directories is provided in the form of the DirectXSetup function.