june/july 1996

GAME DEVELOPER MAGAZINE GAME PLAN GGAMEAEM

Editorial Director Larry O’Brien [email protected] Senior Editor Alex Dunne [email protected] Reboot Managing Editor Diane Anderson [email protected] Editorial Assistant Jana Outlaw [email protected] o, there’s nothing wrong with future versions of DirectX SDK, it Contributing Editors Barbara Hanscome [email protected] your dials. You may have been would make sense to have a base to Chris Hecker expecting Larry O’Brien’s build upon. Besides the articles in this [email protected] sagacious words in this space, issue on the Windows 95 Game SDK, David Sieks and here you are reading a we’ll feature three articles in the next [email protected] column by the guy who for- issue on DirectDraw, DirectInput, and Web Site Manager Phil Keppeler merly wrote Crossfire. “Just . Though our demographics [email protected] what the heck is going on?!” indicate that most of our readers work Editor-at-Large Alexander Antoniades [email protected] Nyou might be asking. on games for Intel-based machines, Changes are afoot here at Game we’ll also add more diverse platform Cover Photography Charles Ingram Photography Developer, though nothing so radical as coverage. We’ll be devoting coverage to a Spindler reorg. Larry’s been kicked the upcoming Game SDK Publisher Veronica Costanza upstairs to editorial director, which for those of you working on Mac Group Director Regina Starr Ridley means that he gets to lean back in his games. Special Projects Manager Nicole Freeman chair and watch pandemonium unfold, Beginning this fall, we’ll review [email protected] rather than having to dive into the mire development tools. The first review was Advertising Sales Staff with the rest of us. Oh, and he’s also going to cover C++ compilers, but, Western Regional Sales Manager doing a lot more programming (some wouldn’t you know it, Chris Hecker Steve Nikkola (415) 905-2256 people get to have all the fun). So, stole my thunder with his latest series [email protected] instead, I’ve taken over much of the on compilers. But look for reviews of Promotions Manager/Eastern Regional Sales Manager day-to-day housekeeping here at the various 3D animation and C++ tools. Holly Meintzer (212) 615-2275 magazine. As with every byte of content that [email protected] Now that I’ve been entrusted with goes into the magazine, let us know Marketing Manager Susan McDonald a smidge of power, my first goal is to what appeals to you and what doesn’t. Marketing Graphic Designer Azriel Hayes work towards beefing up the number of Want more space in the magazine Advertising Production Coordinator Denise Temple articles every issue. More bang for your devoted to articles, and have us post Director of Production Andrew A. Mickus buck. I’m going to try to squeeze every code on our FTP site instead of taking Vice President/Circulation Jerry M. Okabe inch of this magazine to fit as many up space on the pages? Or perhaps you Circulation Director Gina Oh articles as possible into upcoming want more code in the magazine and Associate Circulation Director Kathy Henry issues. As my previous beat was indus- fewer articles. Are there any subjects that Group Circulation Manager Mike Poplardo try news and analysis, this column will we haven’t covered that you’d like to read Assistant Circulation Manager Jamai Deuberry address these issues, and the Crossfire about? Let us know either through our Newsstand Manager Debra Caris column will slowly fade into the sunset. web site, http://www.gdmag.com, or Reprints Stella Valdez (916) 729-3633 This issue, we examine Microsoft’s pick up a pen and scribble us a note. DirectX II SDK. If you haven’t looked I’ve just returned from the Com- Chairman of the Board Graham J.S. Wilson at the SDK yet, check out Robert puter Game Developers Conference in Chairman/CEO Marshall W. Freeman Hess’s article on page 24 which pro- Santa Clara, Calif., which was more President/COO Thomas L. Kemp vides an overview of the kit. If you’ve packed with people than ever before— Senior Vice President/CFO Warren “Andy” Ambrose already started down the DirectX trail, close to 4,000. Check out the Bit Blasts Senior Vice Presidents David Nussbaum, Darrell however, this article may be slightly column, as well as our web site for Denny, Donald A. Pazour, Wini D. Ragus remedial. Don’t fret, Game Developer information about new products that Vice President/Production Andrew A. Mickus ■ isn’t reneging on its commitment to were launched at the show. Vice President/Circulation Jerry Okabe high quality, technically deep content. Vice President/ We did feel, however, that since there Software Development Division Regina Starr Ridley is going to be substantial coverage in Alex Dunne Miller Freeman the magazine about the current and Senior Editor A United News & Media publication

6 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com SEZ U!

Exploring the World of Texture Mapping

KEEPING CHRIS ON HIS TOES! ded in the bottom, but we want to get rid of the such fast texture mapping, while also doing top garbage. To do this, we just subtract our game AI, Z buffering, and anything else that Dear Editor: “big number” (your 1.1 * 223) from this as an needs to be done? When it changes detail level, ’ve enjoyed reading Chris Hecker’s series on integer: what is it doing? perspective texture mapping. His articles were Ivery informative, and they were very timely Stuart Doyle with respect to my current project. Thanks! 11001011001111111111111111111000 Via e-mail However, there are a few things in the - February/March 1996 issue that confused me. 01001011010000000000000000000000 First, on page 24, Figure 3, should the resulting = Chris Hecker replies: exponent have the same binary exponent value 11111111111111111111111111111000 Well, general quads will work with a projective as the higher magnitude value? In this case (Which equals -8) mapping (they won’t with affine). But, if your 10000111 (135)? quads (or even general polygons) are coplanar, Also, in the first paragraph of page 24, he Thanks a lot! Keep up the good work! you can use the plane equations to generate the talks about subtracting “the integer representa- gradients without using the vertex texture coor- tion of our large, floating-point shift number dinates. The math is a bit much, but I might from the integer representation of the number X MODE MARKS THE SPOT cover it in a later article. You can derive it your- we just converted....” What are these numbers? Dear Editor: self if you think about what you need to describe The values to be subtracted are now: our magazine is great! I use a lot of the the plane of the texture (a vector from the origin articles constantly. Especially the ones to the texture origin, a U direction vector, and a 0 | 10010110 | 10000000000000000000000 about breaking into the game business. It V direction vector, all in view space). Solve for u 23 Y (1.1 *2 ) actually helped me land a job! and v in terms of screen space x and y (which - Your X mode programming optimizations are view point x/z and y/z) and you’ve got it. 1 | 10010110 | 00000000000000000001000 (Our 8.75 lined up) were great. Thanks for producing a much need- You’ll end up with rational linear equations that = ed magazine. look like: 1 | 10010110 | 11111111111111111111000 John Bryant (This gets us our -8) Via e-mail u = (ax + by + c)/(dx + ey + f) u = (gx + hy + i)/(dx + ey + f) I think this is correct! Kenneth Chao I HAVE A TEXTURE MAPPING QUESTION! Also, Descent doesn’t Z buffer. The major Via e-mail Dear Editor: speedups in real games come from novel data- ’ve been getting Game Developer magazine base traversal algorithms that cull out most of since March 1995, and I’ve been especially the world before they gets to the texture map- Chris Hecker replies: Ifollowing your series on texture mapping. per. The fastest texture mapped triangles are Yes, this is a bug, thanks for spotting it! I must Do you have any suggestions on how I could the ones you didn’t draw. have cut and pasted when I did the make a quad texture mapper, assuming that diagram. the points are clockwise and coplanar? Theoret- In regards to integer presentation, you have ically, it should not be too much of a problem, Say It! got one bit wrong in your result. The .1 bit gets since the gradients across the quad are the Please send all feedback to: Game Developer borrowed from, so you end up with: same as either of the two triangles which make magazine, Feedback, 600 Harrison St., S.F., it up, or at least, should be, as far as I have 1 | 10010110 | 01111111111111111111000 Calif. 91407 or to [email protected]. reasoned. Thanks! Now, we’ve got a result that has our -8 embed- Also, how do games like Descent manage

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 9 SEZ U!

Exploring the World of Texture Mapping

KEEPING CHRIS ON HIS TOES! ded in the bottom, but we want to get rid of the such fast texture mapping, while also doing top garbage. To do this, we just subtract our game AI, Z buffering, and anything else that Dear Editor: “big number” (your 1.1 * 223) from this as an needs to be done? When it changes detail level, ’ve enjoyed reading Chris Hecker’s series on integer: what is it doing? perspective texture mapping. His articles were Ivery informative, and they were very timely Stuart Doyle with respect to my current project. Thanks! 11001011001111111111111111111000 Via e-mail However, there are a few things in the - February/March 1996 issue that confused me. 01001011010000000000000000000000 First, on page 24, Figure 3, should the resulting = Chris Hecker replies: exponent have the same binary exponent value 11111111111111111111111111111000 Well, general quads will work with a projective as the higher magnitude value? In this case (Which equals -8) mapping (they won’t with affine). But, if your 10000111 (135)? quads (or even general polygons) are coplanar, Also, in the first paragraph of page 24, he Thanks a lot! Keep up the good work! you can use the plane equations to generate the talks about subtracting “the integer representa- gradients without using the vertex texture coor- tion of our large, floating-point shift number dinates. The math is a bit much, but I might from the integer representation of the number X MODE MARKS THE SPOT cover it in a later article. You can derive it your- we just converted....” What are these numbers? Dear Editor: self if you think about what you need to describe The values to be subtracted are now: our magazine is great! I use a lot of the the plane of the texture (a vector from the origin articles constantly. Especially the ones to the texture origin, a U direction vector, and a 0 | 10010110 | 10000000000000000000000 about breaking into the game business. It V direction vector, all in view space). Solve for u 23 Y (1.1 *2 ) actually helped me land a job! and v in terms of screen space x and y (which - Your X mode programming optimizations are view point x/z and y/z) and you’ve got it. 1 | 10010110 | 00000000000000000001000 (Our 8.75 lined up) were great. Thanks for producing a much need- You’ll end up with rational linear equations that = ed magazine. look like: 1 | 10010110 | 11111111111111111111000 John Bryant (This gets us our -8) Via e-mail u = (ax + by + c)/(dx + ey + f) u = (gx + hy + i)/(dx + ey + f) I think this is correct! Kenneth Chao I HAVE A TEXTURE MAPPING QUESTION! Also, Descent doesn’t Z buffer. The major Via e-mail Dear Editor: speedups in real games come from novel data- ’ve been getting Game Developer magazine base traversal algorithms that cull out most of since March 1995, and I’ve been especially the world before they gets to the texture map- Chris Hecker replies: Ifollowing your series on texture mapping. per. The fastest texture mapped triangles are Yes, this is a bug, thanks for spotting it! I must Do you have any suggestions on how I could the ones you didn’t draw. have cut and pasted when I did the make a quad texture mapper, assuming that diagram. the points are clockwise and coplanar? Theoret- In regards to integer presentation, you have ically, it should not be too much of a problem, Say It! got one bit wrong in your result. The .1 bit gets since the gradients across the quad are the Please send all feedback to: Game Developer borrowed from, so you end up with: same as either of the two triangles which make magazine, Feedback, 600 Harrison St., S.F., it up, or at least, should be, as far as I have 1 | 10010110 | 01111111111111111111000 Calif. 91407 or to [email protected]. reasoned. Thanks! Now, we’ve got a result that has our -8 embed- Also, how do games like Descent manage

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 9 SchmoozeSchmooze newsnews ...... 10 [email protected]. Williams dropping grapesinhismouth. endowed womanwhospentthetime actually showedupwithawell- Wanna gossip? Danny Gorlin Sierra CUC Playboy Bunny female...one ofthemmaybea toga attendeeswouldbe it didn’tmatterthatonly.01%of was toppedwiththestatementthat DirectX Doesn’tSuck”listwhich more developersbyhis“10Reasons Game Evangelistoffendedeven power) astheirtheme,but cation ofsubjectstatesbyalarger use PaxRomana(theforciblepacifi- Not onlydidMicrosoftunwittingly 96DIRECT:BEER” cated CrashAddress: gons amIholdingup?”and“Unrelo- included: “Quick,Howmanypoly- Jose microbrewery.T-shirtthemes DirectBeer 1ABeta”partyataSan toga partyfeeheldthe“FirstAnnual Microsoft Game developersunhappyabout forthcoming Cole the fold.He’sworkingon and drummingbuthascomebackto years forastintofAfricandancing Remember stem defections. providing anymanagementfixto the takeoverdoesn’tlooklikeit’s such assetsas high turnoverandregularlyloses other thandesigner way ofgamedevelopmenttalent thing, buttheydidn’tgetmuchinthe old Sierragamesisworthsome- What didtheybuy?Theinventoryof published by E-mail GAME DEVELOPER •JUNE/JULY 1996 , theshoppingnetwork,bought . CUCpromiseshandsoff,so The GossipLady for acoolbilliondollars. . Sierraisknownforits Choplifter ’s $125 Banzi Bug Grolier Lori dropped outforfour . Worseyet,he Pax Romana Roberta and . ? Developer Cory game tobe Gravity' at: s BIT BLASTS Yamahaannouncedplanstodesign • IntelthrewabigpartyattheCGDC • time oftheCGDC: that wereannouncedatoraroundthe Reader’s Digest site foramorecompletescoop.Here’s devote spaceto,socheckoutourweb to listalltheproductswewanted a periodofshowsaturation. Yamahaannouncedsupportforthe • DiamondMultimediareleasedDia- • A the Show On With 567-2300 support Intel’sMMX.Call hardware andsoftwaresolutionsto get MMXdevelopmentinformation. intel.com/pc-supp/multimed/mmx/ Intel architecture.See major multimediaenhancementtothe to announceitsMMXtechnology,a ed betatestsites. Call OEMs, content developers, andselect- drivers areavailabletoYamaha’s of 3Dgraphicsaccelerators.Direct3D Direct3D driversfortheRPA family Direct3D APIandavailability of Call ator withMicrosoftDirect3Dsupport. mond Edge3D,amultimediaacceler- Unfortunately, wedidn’thaveroom (408) 325-7000 all seemstrangelynormalafter laces, andtemporarytattoos heads, sillystring,neonneck- mouse pads,Nerds,Lemon- Pog guns,dartdogtags, myself otherhumansexist. must decompress.Iremind fter afewdaysatCGDC,I http://www.gdmag.com . synopsis ofsomeproducts . (408) 567-2300 http://www. (408) to . AppleannouncedGameSprockets, • Apple,Netscape,andSilicon Graph- • 3Dfxannounced System3D, a • SiliconGraphicsannouncedSilicon- • 3DLabsInc.announcedthatits • http://www.gdmag.com. Developer Game please surftothenewandimproved information ontheseandotherproducts, For fulldescriptions,pricing,and contact http://www.dev.apple.com/games their newGameSDK.Checkout worlds acrosstheInternet. parsing of3Dobjectsandvirtual compression, filestreaming,andfaster the Internet,whichenableshigher tion fordynamic3Denvironmentson is anopen,cross-platformspecifica- (3DMF) technology.MovingWorlds based onApple’s3Dmetafileformat 2.0) (a leadingproposalforVRML binary fileformatforMovingWorlds companies plantodevelopanew graphics fortheInternet.Thethree ics recentlyagreedtocooperateon3D chipset. Call announced itsVoodooGraphics mapped 3Darcadegames.3Dfxalso games; itisdesignedfortexture- Obsidian graphicsboardforLBE scaleable systembasedon3Dfx’s (415) 960-1980 based studioservicesnetwork.Call tem, andStudioLiveisitsInternet- Firewalker isacontentauthoringsys- is itsassetmanagementcompanion, building digitalstudios.StudioCentral Studio, anopen-architecturefor Direct3D API.Call accelerators willsupportthenew Glint andPermedia3Dgraphics Diane Anderson (415) 934-2400 . web sitelocated at (408) 436-3455 . . . BEHIND THE SCREEN

PowerPC Compilers: Still Not So Hot

believe it was Theodore Roosevelt the more the article turned into an explo- grammers or nonproduction programmers who first called the presidency of the ration of how we as programmers have to writing toy programs. Compilers them- United States a “bully pulpit,” which help the compilers do a good job with our selves are written for those reviewers, and is a catchy way of saying that the code. So while I’m still going to talk so we end up with the current mess, president can rant on a subject, peo- about five compilers and give comparison where compiler vendors focus on silly new ple will actually listen, and maybe charts like a normal review, I’m actually features to please silly reviewers instead of those people will even do something going to concentrate on how our source focusing on things that actually help pro- about whatever the topic of the rant code changes affect the assembly the duction programmers do their jobs well. Ihappens to be. Magazine columns can be compiler generates. When I evaluate a compiler I look bully pulpits as well, and, while a comput- Most other compiler reviews focus for two things: C++ compliance and code er magazine column is clearly not a pulpit on the compiler’s integrated development optimization. The former is basically a on the same level as the White House, I environment, on the fancy editor with lost cause at this point because the C++ don’t expect to hear Bill Clinton taking color syntax highlighting that doesn’t even draft standard is still a moving target and compiler vendors to task about lame opti- let you write a simple macro, on the there’s no solid conformance suite. I pray mization quality in the next State of the debugger’s silly ToolTip windows (that this will change soon. By contrast, com- Union Address, so I might as well do it pop up over variable names with their val- piler writers have had years to work on myself. ues if you hold your mouse there forever), compiler optimizations, and not much and on the compiler supplied class library has changed since the early days. Review Problems that violates just about every precept of By focusing on optimizations, we’ll This article started out as a comparative good object-oriented design in C++ and is not only learn which compilers optimize review of compiler optimizations, but the bloated and slow to boot. Wow! As you the best, we’ll also learn what we can do more I learned about the various compil- can see, I’m no fan of compiler reviews—I to help a compiler do its best with our ers and how they did or did not optimize, believe most are written by either nonpro- code. This time, we’ll be covering compil- ers for the PowerPC chip on the Macin- Table 1. Transform Cycle Counts tosh, and next time we’ll cover the Intel x86. Even if you don’t program for the Listing Listing KAPed 1 Listing Listing Listing PowerPC, reading this will help you learn Compiler 1 2 (not shown) 4 5 6 a lot about compilers and how they opti- mize, and that knowledge will carry across CodeWarrior 40.7 50.5 50.9 34.3 29.7 19.6 to whatever CPU you care to program. The compilers we’ll cover this issue are: Metrowerks CodeWarrior 8, Syman- Symantec C++ 76.6 94.9 82.8 50.9 31.9 25.7 tec C++ 8, version 1.0f3e2 of Apple’s MrCpp compiler (which is included with the Symantec compiler), Motorola’s 2.1.1 Motorola C++ 34.5 47.4 39.5 33.2 30.8 20.6 PowerPC C++ compiler, and the Microsoft Visual C++ for Macintosh 4.0 cross compiler. Apple’s MrCpp 52.0 65.0 56.2 36.1 28.8 19.5 The Test Code We’ll use a simple inner product of a Microsoft VC++ 41.6 49.3 42.8 31.9 21.9 22.7 three-by-three matrix and a three ele- ment column vector to evaluate each

12 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com compiler’s optimization quality. Obvi- good idea to make sure neither you nor ously, a single function is not going to the compiler has introduced any bugs Chris Hecker tell the whole optimization story, but it while optimizing. should give us an idea of what sorts of optimizations we can expect from today’s Anti-Alias Compilers. What are compilers. If you’ve looked ahead at the other Listing 1 shows the function Trans- results in Table 1 and the other listings, formVectors. I made it transform an array you’re probably wondering what the sec- of vectors so the compilers would have to ond column of data means, and why they good for? Chris work a bit harder, but, even so, the code is Listing 2 is almost identical to Listing 1. trivial. I used 1,000 calls to this function Even though you and I know we with 500 transforms on each call to gather wouldn’t call TransformVectors from List- timing information. The first column of ing 1 with the source or destination data in Table 1 shows the approximate pointing to the same vector, or, worse Hecker steps to the cycle counts for each product measured yet, with the destination pointing into with the MacOS call Microseconds for the the middle of the matrix somewhere, the various compilers on my Power Comput- compiler doesn’t know this, so it can’t ing 604. I turned on all the optimizations assume we didn’t do something silly. bully pulpit to rant I could find on each compiler to gather When a variable points to another live this data. I made sure my test program variable in the function, it’s called was producing correct results on every “pointer aliasing,” and when the compil- compiler by making the source vectors er sees a write through a pointer, it needs about the state of eigenvectors of the transform matrix and to assume that the data could have land- checking to see if the transformed vector ed anywhere, including into variables it’s was the same as the source—it’s always a already loaded into registers. This means current PowerPC Listing 1. The Test Function void TransformVectors( float *pDestVectors, float const (*pMatrix)[3], float const *pSourceVectors, int NumberOfVectors ) compilers. Sadly, these { int Counter, i, j; for(Counter = 0;Counter < NumberOfVectors;Counter++) { for(i = 0;i < 3;i++) { float Value = 0; days, most compilers for(j = 0;j < 3;j++) { Value += pMatrix[i][j] * pSourceVectors[j]; } *pDestVectors++ = Value; need a lot of help } pSourceVectors += 3; } } optimizing code.

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 13 BEHIND THE SCREEN

Listing 2. The Non-Aliasing Test Function which is disappointing. For example, the compiler doesn’t bother to load the source void TransformVectors2( float *pDestVectors, vector into registers outside the loop, even float const (*pMatrix)[3], float const *pSourceVectors, int NumberOfVectors ) though it’s used three times and cannot { be aliased because of our temporary int Counter, i, j; results array. Also, instead of leaving the for(Counter = 0;Counter < NumberOfVectors;Counter++) { temporary results in registers, it actually float aTemp[3]; copies them out to the stack and then for(i = 0;i < 3;i++) { copies the stack to the destination. float Value = 0; It even increments the destination for(j = 0;j < 3;j++) { pointer in the loop with three discrete Value += pMatrix[i][j] * pSourceVectors[j]; instructions instead of using offsets and } doing one addition at the end, or even aTemp[i] = Value; using the PowerPC’s autoincrement } pSourceVectors += 3; instructions. The Motorola compiler also for(i = 0;i < 3;i++) { produced the fastest code for Listing 1, *pDestVectors++ = aTemp[i]; and the difference between the timings } for Listings 1 and 2 can be attributed to } the naive compilation of the temporary } copy loop at the end of Listing 2 (even though the temporary loop was supposed the optimizer has to continually reload not only did they still reload all the reg- to help by eliminating the possibility of variables into registers in case we’re alias- isters, they also naively implemented the aliasing). Overall, not a great showing, ing parameters, so I wrote Transform- copy loop at the end of Listing 2! even by our winner in this round. Clearly, Vectors2 in Listing 2 to give the compil- Let’s look at the code generated by the compilers need more help. ers some help. Since aTemp is defined the winner of this round, the Motorola local to our function, the compiler C++ compiler. Listing 3 shows the Bust a KAP knows it can’t be aliased, so writes to PowerPC assembly language generated The Motorola compiler ships with an aTemp shouldn’t cause spurious register for TransformVectors2, our supposedly interesting tool, called the Kuck and reloads. non-aliased function. Despite some odd Associates Preprocessor for C (KAP). Well, at least that’s what I thought, ways of moving values into registers, this Basically, KAP compiles your C code (it anyway. As you can see from the tim- code is a pretty straightforward transla- doesn’t support C++), optimizes it, and ings, all the compilers got slower because tion of our source into assembly language, then generates C code as its output

Listing 3. Motorola C++ Assembly for Listing 2 TransformVectors2__FPfPA3_CfPCfi.b: stfsx f2,r8,r7 ; *(stack + r7) = f2 cmpi 0x7,0x0,r6,0 ; compare count to 0 addi r7,r7,4 ; r7 next float addi r11,r0,0 ; Counter = r11 = 0 bc 0x10,0x0,L..9 ; branch if (—ctr) bc 0x4,0x1d,L..11 ; bail out if count = 0 lfs f1,0(r8) ; f1 = stack[0] addi r8,sp,24 ; allocate some stack lfs f2,4(r8) ; f2 = stack[1] L..8: ori r9,r4,0x0 ; r9 = pMatrix lfs f3,8(r8) ; f3 = stack[2] addi r10,r0,0 ; r10 = 0 stfs f1,0(r3) ; pDest[0] = f1 ori r7,r10,0x0 ; r7 = 0 addi r3,r3,4 ; pDest++ subfic r10,r10,3 ; r10 = 3 addi r11,r11,1 ; Counter++ mtctr r10 ; ctr = 3 stfs f2,0(r3) ; pDest[1] = f2 L..9: lfs f1,0(r9) ; f1 = pMatrix[0] addi r3,r3,4 ; pDest++ lfs f2,0(r5) ; f2 = pSource[0] cmp 0x7,0x0,r11,r6 ; flags = Counter < lfs f3,4(r9) ; f3 = pMatrix[1] NumVecs lfs f4,4(r5) ; f4 = pSource[1] addi r5,r5,12 ; pSource += 3 fmuls f1,f1,f2 ; f1 = f1 * f2 stfs f3,0(r3) ; pDest[2] = f3 lfs f2,8(r9) ; f2 = pMatrix[2] addi r3,r3,4 ; pDest++ lfs f5,8(r5) ; f5 = pSource[2] bc 0xc,0x1c,L..8 ; branch if (Counter < fmadds f3,f3,f4,f1 ; f3 = f3 * f4 + f1 NumVecs) addi r9,r9,12 ; pMatrix->next row L..11: addi sp,sp,48 ; clear stack fmadds f2,f2,f5,f3 ; f2 = f2 * f5 + f3 bclr 0x14,0x0 ; return

14 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com BEHIND THE SCREEN

Listing 4. The KAPed Listing 2 void TransformVectors2( float *pDestVectors, _Kii1 = Counter * 3; const float (*pMatrix)[3], const float *pSourceVectors, _Krr1 += pMatrix[0][0] * pSourceVectors[_Kii1]; int NumberOfVectors ) _Krr2 += pMatrix[1][0] * pSourceVectors[_Kii1]; { Value += pMatrix[2][0] * pSourceVectors[_Kii1]; int Counter, i, j; _Krr1 += pMatrix[0][1] * pSourceVectors[_Kii1+1]; float aTemp[3]; _Krr2 += pMatrix[1][1] * pSourceVectors[_Kii1+1]; float Value, _Krr1, _Krr2, _Krr4, _Krr5; Value += pMatrix[2][1] * pSourceVectors[_Kii1+1]; long _Kii1, _Kii2; if (1) { for ( Counter = 0; Counter

instead of assembly language. When I first got the Motorola compiler, I figured my test would be so simple that there was no way KAP could help out, but after looking at the results we just discussed, I figured anything was worth a try. Listing 4 shows the output of running Listing 2 through KAP (they call the processed code KAPed). If you’ve never seen machine-generated C code, don’t be sur- prised by stuff like the “if (1)” block— compilers output weird stuff like that for bizarre reasons. However, you should be surprised at how poor the code is. It unrolls the loop, which is fine, but why does it go to the trouble of putting the temporaries in aTemp and then looping over aTemp to copy them into the destina- tion? More absurd yet is that something as mundane as unrolling a loop in this simple function actually helped the com- pilers produce faster code. You can see the timing results in Table 1. The KAPed Listing 1 is not even worthy of print, and as you can see the compilers all got slower on that ver- sion. The KAPed Listing 2 (shown in Listing 4) actually made a positive differ- ence, even on the best compilers, and it made a huge difference on Symantec. Even so, if you thought it was bad that KAP generated the redundant loop at the end of Listing 4, it’s even worse that every compiler generated actual assembly lan- guage code for that loop! The worst offender is clearly Symantec. Symantec ships a prerelease version of Apple’s MrCpp compiler with their package, so it’s unclear if I should even review Symantec on their optimization quality because I think they expect you to use MrCpp if you care about run-time speed. However, MrCpp and Symantec’s main-

16 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com BEHIND THE SCREEN

Listing 4. Continued from p. 16 Listing 5. Hand-optimized Listing 1 Value += pMatrix[2][2] * pSourceVectors[_Kii1+2]; void TransformVectors( float *pDestVectors, } const float (*pMatrix)[3], const float *pSourceVectors, aTemp[0] = _Krr1; aTemp[1] = _Krr2; aTemp[2] = Value; int NumberOfVectors ) _Kii2 = Counter * 3; { for ( i = 0; i<=2; i++ ) { int Counter; _Krr5 = aTemp[i]; _Krr4 = _Krr5; float Value0, Value1, Value2; pDestVectors[_Kii2+i] = _Krr4; for ( Counter = 0; Counter

18 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com BEHIND THE SCREEN

Listing 6. Hand-optimized Listing 2 timing results in the occur, and which variables are constant last two columns of throughout a loop iteration. These are all void TransformVectors2( float *pDestVectors, Table 1 that it made things the compiler is supposed to do for const float (*pMatrix)[3], const float *pSourceVectors, int NumberOfVectors ) a big difference on all us, so we can work on more important { the compilers. stuff, like design and algorithms, or int Counter; Why did it assembly language code for our most float Value, _Krr1, _Krr2; make such a big dif- inner loops. We’re supposed to trust the for ( Counter = 0; Counter

20 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com BEHIND THE SCREEN

CodeWarrior version of Listing 6; it’s at I are the losers in this situation. Pundits there’s no pointer aliasing that allowed least 6 instructions smaller than any of have been saying that assembly language them to perform this optimization, but the compiled versions of Listing 2, and is dead—especially on RISC chips like they didn’t take advantage of the assump- about two to five times as fast. Motorola the PowerPC—and it should be eminent- tion anywhere else that I could see. Given produced similar code. (MrCpp decided ly clear from the listings in this article this optimization, I’m not sure why their now was the time to unroll the entire that those people have no clue what version of Listing 6 wasn’t faster than the function, which tripled the size of the they’re talking about. Even a beginning others…it may have been a pipelining code for absolutely no performance assembly language programmer could issue, or the loop might have been cache increase.) Don’t for a minute think this produce better code than any of the com- bound. As soon as I learn a bit more is a compliment for CodeWarrior or pilers for Listings 1 and 2, and this is about the subtleties of the PowerPC 604, Motorola, it’s really a damning insult to simple code. While you might not choose I’ll get back to you on this one. all the compilers: on maximum opti- to write your code in assembly language, In the next issue, I’ll quickly cover a mizations they didn’t find the smaller you end up with C code that looks like bunch of Intel x86 compilers, but we will and faster version of a basic function like assembly language if you want respectable still have room to talk about some opti- a matrix transform. Heck, just by inspec- performance, like Listings 5 and 6. mization programming techniques of our tion I can see how to save a couple more If I had to choose a winner, I’d pick own, because the compilers clearly aren’t instructions in Listing 7. And people the Motorola C++ compiler, because it going to do it for us. actually say that writing assembly lan- seems like the least incompetent optimiz- This Just In: I was keeping Mike guage is a dying art. er of the bunch. The Microsoft compiler Phillip at Motorola’s compiler group showed some promising aggressiveness by posted on my results, and he just got You Lose Some loading the entire matrix into registers back to me with a command line switch If this was a normal compiler review, it once at the top of the loop for its version for KAP that will assume there’s no would be time to pick a winner, but, of Listings 5 and 6. Microsoft has an aliasing. When you turn it on, KAP pro- instead, it’s time to point out that you and option I turned on that tells the compiler duces something resembling Listing 6,

22 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Listing 7. CodeWarrior version of Listing 6

TransformVectors2__FPfPA3_CfPCfi mr r0,r6 ; r0 = NumVecs cmpwi r6,0 ; flags = NumVecs == 0 mtctr r0 ; ctr = NumVecs blelr ; bail if(NumVects == 0) L1: lfs fp1,0(r4) ; fp1 = pMatrix[0][0] lfs fp3,0(r5) ; fp3 = pSource[0] lfs fp0,12(r4) ; fp0 = pMatrix[1][0] lfs fp2,24(r4) ; fp2 = pMatrix[2][0] fmuls fp7,fp1,fp3; fp7 = fp1 * fp3 lfs fp1,4(r4) ; fp1 = pMatrix[0][1] lfs fp5,4(r5) ; fp5 = pSource[1] fmuls fp8,fp0,fp3; fp8 = fp0 * fp3 fmuls fp6,fp2,fp3; fp6 = fp2 * fp3 lfs fp0,16(r4) ; fp0 = pMatrix[1][1] lfs fp4,28(r4) ; fp4 = pMatrix[2][1] lfs fp2,8(r5) ; fp2 = pSource[2] fmadds fp7,fp1,fp5,fp7 ; fp7 = fp1 * fp5 + fp7 addi r5,r5,12 ; pSource += 3 lfs fp3,8(r4) ; fp3 = pMatrix[0][2] fmadds fp8,fp0,fp5,fp8 ; fp8 = fp0 * fp5 + fp8 lfs fp1,20(r4); fp1 = pMatrix[1][2] fmadds fp6,fp4,fp5,fp6 ; fp6 = fp4 * fp5 + fp6 lfs fp0,32(r4); fp0 = pMatrix[2][2] fmadds fp7,fp3,fp2,fp7 ; fp7 = fp3 * fp2 + fp7 fmadds fp8,fp1,fp2,fp8 ; fp8 = fp1 * fp2 + fp8 fmadds fp6,fp0,fp2,fp6 ; fp6 = fp0 * fp2 + fp6 stfs fp7,0(r3) ; pDest[0] = fp7 stfsu fp8,4(r3) ; pDest[1] = fp8, pDest++ stfsu fp6,4(r3) ; pDest[1] = fp6, pDest++ addi r3,r3,4 ; pDest++ bdnz L1 ; branch if(—ctr) blr ; return so all the compilers do well. However, not to be out-done, I decided to take this no-aliasing assumption to the limit and explicitly load the matrix into tempo- raries (much like the Microsoft compiler tried to do). The result: another 25% speedup, with times around 15 cycles, for something the compiler could have done itself. The compilers lose again. ■

Chris Hecker tries to live an optimized life, but he does about as good of a job on his own life as the current crop of compilers does on his code. Contact him at [email protected].

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 23 DIRECTX II

Introduction to the DirectX II APIs

nce upon a time, developing dows rival or exceed performance on based games can still take advantage of games for the PC wasn’t easy. DOS-based platforms and console sys- the hardware cards available to the DOS allowed game develop- tems and to provide a robust, standard- Game SDK developer; however, DOS- ers access to low-level systems ized, and well-documented platform for based game developers must conform to functions, enabling good per- game developers. A high-performance varying implementations of cards— formance, but few standards Windows-based game installs success- which complicates the installation. were in place to support the fully and takes advantage of hardware wide variety of hardware accelerator cards, Windows hardware New Standards for foundO in PCs. Developing under Win- and software standards (such as Plug- Hardware Vendors dows 3.x wasn’t any better, due to per- and-Play), and the Windows communi- The Game SDK provides portable formance bottlenecks. However, with cations services. access to the features of DOS today, last year’s release of Windows 95, a door The Game SDK, which you can keeps a high level of performance, and has been opened to the development of find in the Microsoft Developer Net- removes obstacles to hardware innova- high-performance, Windows-based work (MSDN) Level II, provides a con- tion. The Game SDK tries to provide games. And the key to that door is sistent interface for hardware manufac- guidelines for hardware companies Microsoft’s Game Software Develop- turers and game developers. It reduces based on feedback from game develop- ment Kit (SDK). the complexity of installing and config- ers and independent hardware vendors The primary goal of the Game uring games and uses a computer’s (IHVs). Therefore, the Game SDK SDK is to make performance on Win- hardware to the best advantage. DOS- components often provide specifications for hardware accelerator features that do not yet exist. In many cases, these speci- fications are emulated in software. In other cases, the capabilities of the hard- ware are polled first, and the feature is bypassed if it is not supported by the hardware. The Game SDK supports a num- ber of video hardware features that have already come out (or will be released in the near future). These include: • Overlays • Page flipping • Sprite engines • Stretching with interpolation • Alpha blending • Z buffer-aware bit-block transfers. Overlays will be supported so that page flipping will also be enabled within a window using graphic device interface (GDI). Page flipping is the double- buffer scheme used to display frames on the entire screen. Sprite engines are used to make overlaying sprites easier.

24 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Robert Hess Stretching with interpolation is stretch- • The DirectDraw API accelerates ing a smaller frame to fit the entire hardware and software animation screen; it can be an efficient way to con- techniques by providing direct access serve video RAM. Alpha blending is to bitmaps in offscreen video memory Through the use of used to mix colors at the hardware pixel as well as fast access to the bit-block level. Three-dimensional (3D) accelera- transferring and buffer-flipping capa- tors with perspective-correct textures bilities of the hardware. DirectX APIs, you can will allow textures to be displayed on a • The DirectSound API enables hard- 3D surface; for example, hallways in a ware and software sound mixing and castle generated by 3D software can be playback. develop with a textured with a brick wall bitmap that • The DirectPlay API lets you develop maintains the correct perspective. As games for play over a modem line or 3D games generally need at least 2MB a network. consistent interface; of video memory, the Game SDK sup- • The Direct3D API provides direct ports this. A compression standard to low-level access to 3D hardware, put more data into display memory will allowing DirectDraw surfaces to be your Windows include transparency compression, will used both as 3D rendering targets be usable for textures, and will be very and as source texture maps. fast when implemented in software as • The DirectInput API provides joy- well as hardware. stick input capabilities that are games can The audio hardware features that scaleable to future Windows hard- the Game SDK supports or will soon ware input API and drivers. include are the following: • The AutoPlay feature lets your CD- outperform their • 3D audio enhancers that provide a ROM run an installation program or spatial placement for different the game itself immediately upon sounds (particularly effective with insertion of the disc. DOS-based ancestors headphones) Note: DirectInput and AutoPlay • Onboard memory for audio boards exist in the Microsoft Win32 API and • Audio-video combination boards that aren’t unique to the Game SDK. and automatically share onboard memory. In addition, video playback will see DirectDraw the benefit from Game SDK-compati- The biggest gain in performance in the take advantage of ble hardware. Future releases of the Game SDK comes from the DirectDraw Game SDK will support hardware- services, which are a combination of four accelerated decompression of YUV COM interfaces: IDirectDraw, IDirect- hardware video. DrawSurface, IDirectDrawPalette, and IDi- rectDrawClipper. A DirectDraw object, cre- Taking It Apart ated using the function DirectDrawCreate, acceleration where The Game SDK is made of several represents the video display card. One of interfaces that target performance the object’s member functions, IDirect- issues of game programming under Draw::CreateSurface, creates the primary Windows 95. The following are APIs: DirectDrawSurface object, which repre- it‘s available.

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 25 DIRECTX II APIS

sents the video display memory being Draw manages clipped regions of dis- Microsoft provides two methods for viewed on the monitor. From the prima- play memory by using these objects. A achieving this: MIDI streams and ry surface, offscreen surfaces can be cre- bitmap is transferred to a surface using a DirectSound. MIDI streams are actually ated in a linked-list fashion. transparent bit-block transfer, and a cer- part of the Windows 95 multimedia In the most common case, a back- tain color (or range of colors) in the API. They provide the ability to time- buffer surface is created (in addition to bitmap is defined as transparent. Color stamp MIDI messages and send a buffer the primary surface) and is used to keying achieves a transparent bit-block of these messages to the system, which exchange images with the primary sur- transfer. Source color keying defines can then efficiently integrate them with face. While the screen is busy displaying which color or color range on the its processes. More information about the lines of the image in the primary bitmap is transparent (and therefore not MIDI streams can be found in the surface, the back-buffer surface frame is copied during a transfer operation). Win32 SDK documentation. composed by transferring a series of off- Destination color keying defines which DirectSound is built on the COM- screen bitmaps stored on other Direct- color or color range on the surface will based interfaces IDirectSound and IDi- DrawSurface objects in video RAM. Call be covered by pixels of that color or rectSoundBuffer, and it’s extensible to the DirectDrawSurface::Flip member color range in the source bitmap. other interfaces. DirectSound imple- function to display the recently com- Finally, DirectDraw supports over- ments a new model for playing back posed frame, which sets a register so lays in hardware and by software emula- digitally recorded sound samples and that the exchange occurs when the tion. Overlays are an easy means of mixing different sample sources togeth- screen performs a vertical retrace. This implementing sprites and managing er. As with other object classes in the is asynchronous, so the game can con- multiple layers of animation. Any Game SDK, you should use the hard- tinue processing after calling DirectDraw- DirectDrawSurface object can be created ware to its greatest advantage whenever Surface::Flip. (The back buffer is auto- as an overlay, and any overlay has all of possible and emulate a hardware feature matically write-blocked after calling the capabilities of any other surface— in the software when the feature is not DirectDrawSurface::Flip until the plus extra capabilities associated only present in hardware. You can query exchange occurs.) After the exchange with overlays. These capabilities require hardware capabilities at run-time to occurs, the game continues to compose extra display memory, and, if there are determine the best solution for any the next frame in the back buffer, calls no overlays in display memory, the over- given personal computer configuration. DirectDrawSurface::Flip, and so on. lay surfaces can exist in host memory. The DirectSound object represents DirectDraw improves performance Color keying works the same for the sound card and its various attributes. over the Windows 3.1 GDI model, overlays as for transparent bit-block Using a DirectSound object, you create which does not have direct access to transfers. The Z order of the overlay the DirectSoundBuffer object, which rep- bitmaps in video memory. Therefore, automatically handles the occlusion and resents a buffer containing sound data. bit-block transfers always occur in host transparency manipulations between Several DirectSoundBuffer objects can RAM and are then transferred to video overlays. exist and be mixed together into the pri- display memory. Using DirectDraw, all You can find source code for a mary DirectSoundBuffer object. Direct- processing is done on the card whenever DirectDraw application on the Game Sound buffers are used to start, stop, possible. DirectDraw improves perfor- Developer web site. The bulk of this and pause sound playback and to set mance over the Windows 95 and Win- code comes from the generic sample attributes such as frequency, format, and dows NT GDI model that uses the Cre- sources I wrote for the Win32 SDK. I so on. Depending on the card type, ateDIBSection function to enable hard- made minor additions and modifications DirectSound buffers can exist in hard- ware processing. to enable this application to utilize the ware as onboard RAM, wave table The third type of DirectDraw DirectDraw features of the new DirectX memory, a DMA channel, or a virtual object is DirectDrawPalette. Because the APIs for Windows. This code is not buffer (for an I/O, port-based audio physical display palette is usually main- meant to illustrate a great DirectDraw card). Where there is no hardware tained in video hardware, an object rep- application, but simply to show you how implementation of a DirectSound resents and manipulates it. The IDirect- easily DirectDraw can be added to a buffer, it is emulated in host system DrawPalette interface implements Windows application model. There are memory. palettes in hardware. These bypass several sample applications that come The primary buffer is generally Windows palettes and are therefore only with the Game SDK that better illus- used to mix sound from secondary available when a game has exclusive trate some of the more advanced fea- buffers but can be accessed directly for access to the video hardware. Direct- tures and capabilities of DirectX. custom mixing or other specialized DrawPalette objects are also created from activities. (Use caution in locking the DirectDraw objects. DirectSound primary buffer, because this blocks all The fourth type of DirectDraw Game programming requires efficient access to the sound hardware from other object is the DirectDrawClipper. Direct- and dynamic sound production. sources.) Secondary buffers can store

26 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com common sounds that are played oper as COM-based interfaces for the store the data for the object only once per throughout a game. A sound stored in a retained (IDirect3DRM) and immediate file. The Direct3D file format is used secondary buffer can be played as a sin- mode APIs (IDirect3D). These interfaces natively by the Direct3D retained-mode gle event or as a looping sound that are responsible for operations including API, providing support for reading pre- plays repeatedly. You can use secondary DirectDraw rendering devices, textures, defined objects into an application or buffers to play sounds that are larger materials, lights, viewports, animations, writing mesh information constructed by than available sound buffer memory. and picking. the application in real time. The file for- When used to play a sound larger than The Direct3D high-level retained- mat will be supported by content creators the buffer, the secondary buffer is a mode API is designed for manipulating for modeling 3D objects and scenes and queue that stores the portions of the 3D objects and managing 3D scenes, defining complex animation paths, and it sound about to be played. while insulating the developer from the will be used by title developers for incor- mesh structures and transformation cal- poration into their titles. DirectPlay culations. It is targeted at developers The Direct3D hardware abstraction One of the best features of PCs as a who don’t want to create their own layer (HAL) provides a driver interface game platform is their easy access to geometry engines or object database rou- for giving developers a transparent, communication services. DirectPlay tines but want to easily add 3D capabili- device-independent means to access the capitalizes on this and allows multiple ties to new or existing Windows-based features of 3D hardware acceleration. players to interact during game play applications. For example, the retained The Direct3D hardware emulation layer through standard modems, network mode API supports the loading of a pre- (HEL) provides software-based emula- connections, or online services. defined, textured 3D object with a single tion of 3D rendering services not sup- The IDirectPlay interface contains API command; the application can use ported by the hardware device. For methods providing capabilities such as additional simple API commands to example, Direct3D supports the acceler- creating and destroying players, adding rotate, move, or scale the object to ation of any or part of the 3D rendering players to and deleting players from manipulate it in the scene in real time. pipeline including transformations, groups, sending messages to players, The retained mode API also supports lighting, and rasterization—many 3D inviting players to participate in a game, key frame animations. hardware accelerators on the market and so on. The Direct3D low-level immedi- today only offload the rasterization mod- DirectPlay is composed of the ate-mode API, on the other hand, is a ule of the pipeline, so the transforma- interface to the game, as defined by the thin polygon- and vertex-based API tions and lighting are handled by the IDirectPlay interface and the DirectPlay layer that gives you direct access to fea- software emulation routines. This archi- server. DirectPlay servers are provided tures of 3D hardware in a device-inde- tecture ensures that services exposed by by Microsoft for modems and networks, pendent manner. Because the immedi- the Direct3D APIs are always available as well as by third parties. When using a ate-mode API does not provide its own to the application, whether the underly- supported server, DirectPlay-enabled geometry engine (unlike the retained- ing hardware supports it or not. Devel- games can bypass connectivity and com- mode API), the application handles the opers can query the underlying charac- munication overhead details. object and scene management. The teristics of the hardware to identify the immediate-mode API lets you port capabilities supported and determine Direct3D existing high-performance multimedia whether the hardware is providing the Direct3D is the newest addition to applications, such as games, to the Win- rendering services, to support tuning and Microsoft’s DirectX family of APIs and dows operating system. It also gives you scaling of the application in real time as provides developers with an API and the flexibility to make use of your own appropriate for the given configuration. system services for real-time 3D graph- rendering and scene-management tech- Direct3D integrates with Direct- ics. Direct3D is based on the Reality nologies while transparently taking Draw to provide 2D drawing and tex- Lab technology acquired by Microsoft advantage of the new generation of 3D ture services for 3D rendering. Applica- in 1995 when they purchased Render- hardware accelerators. tions use Direct3D and DirectDraw for Morphics and has been significantly Direct3D provides a rich file format 3D rendering in a relatively straightfor- enhanced to include tight integration for storing meshes, textures, animation ward manner. For example, the steps to with the DirectDraw API. Direct3D sets, and user-definable objects. This for- set up a scene and to render a triangle consists of the following components: mat facilitates the exchange of 3D infor- using the Direct3D immediate mode integrated retained mode and immedi- mation between applications. Support for API are as follows: First, you use ate mode APIs, extensible file format, animation sets allows predefined paths to DirectDraw to create the rendering sur- and a device-independent driver model be stored for playback in real time. faces, which consist of the front buffer, for transparent access to 3D hardware Instancing and hierarchies are also sup- back buffer, and (optionally) the Z- acceleration. ported and allow multiple references to a buffer, as DirectDraw surfaces. Next, Direct3D is exposed to the devel- single data object, such as a mesh, but you use the IDirect3D COM interface to

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 27 DIRECTX II APIS

set up the world, view, and projection DirectInput CD when inserted into a CD-ROM matrices and to create a viewport to con- The joystick represents a class of drive. Any CD-ROM product that trol the 3D clipping information. You devices that report tactile movements bears the Windows 95 logo must be create a material for the background of and actions that players make within a enabled with the AutoPlay feature. the viewport. You then create a Direct- game. DirectInput provides the func- Draw surface to serve as the texture for tionality to process the data represent- Game SDK COM Interfaces the triangle; then you create a material ing these movements and actions from The interfaces in the Game SDK have to define the surface reflectivity and joysticks, as well as other related been created at a very base level of the color. Then you create a light source to devices, such as trackballs and flight COM programming hierarchy. Each add lighting to the scene. Next, create harnesses. main device object interface, such as an execute buffer to represent the display DirectInput is currently another IDirectDraw, IDirectSound, or IDirectDraw list for holding the vertices and to define name for an existing Win32 function, derives directly from IUnknown. The cre- how the vertices are tied together into joyGetPosEx. This function provides ation of these base objects is handled by primitives for rendering the 3D object. extended capabilities to its predecessor, specialized functions in the library You add any transformations, lighting, joyGetPos, and should be used for any rather than by the Win32 CoCreateIn- and rendering state information to the joystick services. In future support for stance function normally used to create execute buffer, followed by the vertex input devices, including virtual reality COM objects. The Game SDK object and primitive operation information for hardware, games that use joyGetPosEx model provides one main object for representing the 3D object. Finally, you will be automatically supported for joy- each device, from which other support clear the viewport and render the exe- stick input services. This is not the case service objects are derived. For example, cute buffer, followed by a page flip oper- for joyGetPos. the DirectDraw object represents the dis- ation to display the rendered scene on play adapter. It is used to create Direct- the front buffer—repeat the process as AutoPlay DrawSurface objects that represent the the execute buffer is modified to ani- AutoPlay is the feature of Windows 95 video RAM and DirectDrawPalette mate the object. that automatically plays a CD or audio objects that represent hardware palettes. Similarly, the DirectSound object repre- sents the audio card and creates Direct- SoundBuffer objects that represent the sound sources on that card. Besides the ability to generate sub- ordinate objects, the main device object determines the capabilities of the hard- ware device it represents, such as the screen size and number of colors, or whether the audio card has wave table synthesis. By utilizing DirectX, it is finally possible to write state-of-the-art, fast- action, “rip the nerves from the tips of your fingers” games for Windows. And not only can these games far exceed the wildest dreams of Windows program- mers of the past, but they can leave DOS games twitching in the dust. Windows, it isn’t just for spreadsheets anymore. ■

When not underwater basketweav- ing, Robert Hess spends his time as a soft- ware design engineer in the Developer Relations Group at Microsoft. You can con- tact him at [email protected]. An extended version of this article is available on the Internet at the Game Developer web site.

28 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com DIRECTPLAY

Networking Your Game Using DirectPlay

ith the advent of the Win- DirectPlay API functions. The one to learn the details of all the different dows 95 Game SDK, Win- missing element in DirectPlay, however, network protocols. At this point, if you dows 95 is now positioned is synchronization support. Because of haven’t breathed a huge sigh of relief, as a powerful and interest- the many different approaches to solving please feel free to. The ability to write ing platform for network the game synchronization problem, network games without having to learn gaming. More specifically, DirectPlay forces you to implement your the details of network interfaces is truly the DirectPlay component own game-specific solution. Although it a giant step in game programming. of the Game SDK provides might seem like Microsoft took the easy DirectPlay lets you focus on the network aW network communication protocol that way out, in reality they just didn’t want aspects directly related to your game. stands to make life much easier for net- to force a specific synchronization solu- DirectPlay is composed of two work game developers and players alike. tion on game developers. parts: the DirectPlay COM (Compo- It provides a device- and network-inde- nent Object Model) object and the pendent communications model for DirectPlay Architecture DirectPlay service provider. The COM multiplayer games and a consistent user DirectPlay provides a network-indepen- object provides the programmatic inter- interface for establishing and maintain- dent programmatic interface to network face with which you establish network ing network connections. game development. This network inde- connections, maintain available sessions DirectPlay provides all the over- pendence means that you write game- and players, and handle the details of head, which enables players to connect communication code to the DirectPlay sending and receiving game data. The to each other in a consistent manner API, and it sends the information over DirectPlay service provider is a lower across a wide range of network types. At the network connection established for level DirectPlay component that handles the code level, you simply call the correct the game. This saves you from needing the dirty work of implementing net- work-specific communications. The ser- vice provider is implemented as a net- work server for each type of supported

Application network. Microsoft provides DirectPlay servers for IPX, TCP/IP, and modem networks. Third-party vendors must develop their own DirectPlay servers for DirectPlay Com Object supporting specialized network hardware and online services. DirectPlay servers are the network game equivalents of drivers in other DirectPlay DirectPlay DirectPlay Network Network Network parts of the Windows system. Servers Server Server Server take on the difficulties of implementing the DirectPlay API for a specific net- work. This approach works well because Network Network Network it maintains a consistent interface at the Hardware Hardware Hardware application level, while allowing extensi- bility at the network level. When a Figure 1. The DirectPlay communications model. DirectPlay COM object is created, a DirectPlay server is specified. DirectPlay then dynamically binds to this server,

30 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com through which all DirectPlay communi- where only one session can exist. Play- cations are carried out. Figure 1 shows ers in a particular network game are in Michael Morrison the DirectPlay communications model, the same session. Suppose you want to which illustrates how an application join one of two sessions of a network communicates through DirectPlay on a Poker game. You must choose one DirectPlay takes care particular type of network. poker game or the other to connect to. Players choose from a list of sessions DirectPlay Fundamentals that DirectPlay supports. DirectPlay provides a means of estab- DirectPlay can save information lishing a connection and communicating about a session in the registry for future over a network in a consistent manner. use. With a modem network, for exam- of developing a This is no small feat and puts a lot of ple, the remote player’s name, phone responsibility on DirectPlay network number, and optional password are servers. DirectPlay itself keeps up with saved. Speaking of modem connections, information regarding the network con- modem code is another huge responsi- nections and all parties involved. The bility taken on by DirectPlay. Remem- key components of a DirectPlay network ber that DirectPlay servers handle the connection are sessions and players. details of actually making the network network-based game connections. The DirectPlay modem Sessions server uses the Windows 95 Telephony Every DirectPlay game must establish a Application Programming Interface session, which is a communication (TAPI) to manage the intricacies of channel. Multiple sessions on a given modem connections. network correspond to different multi- To join a DirectPlay game, you while shielding you player games running on the network. connect to an existing session on the The exception is a modem network, network. Because this connection usually takes place from Listing 1. The CGame::DPInit Member Function for TicTacToe within a game, you select from the list BOOL of sessions that typ- CGame::DPInit() from all those messy { ically shows only // Clear the players one type of game. m_dpidPlayer[0] = 0; In other words, if m_dpidPlayer[1] = 0; you run a Chess game and try to // Prompt user to select a DP server, then create the DP connect to a ses- object sion, it will only network protocol and CServerSelDlg dlgServerSel; show you other if (dlgServerSel.DoModal() == IDOK) Chess sessions on return (::DirectPlayCreate(dlgServerSel.GetSelServer(), the network. This &m_pDirectPlay, NULL) == DP_OK); limiting of sessions return FALSE; is implemented at the application modem details.

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 31 DIRECTPLAY

can be sent to the group, in which case DirectPlay routes the message to each Server Player individual player in the group.

Create Messages DirectPlay manages communication between players. DirectPlay messages are

Game different from Windows messages and Session are sent and received through a different protocol. A few DirectPlay system mes- sages let you determine when a connec- Connect Connect tion has been established and when play- Connect ers and groups have been added or delet-

Client Client Client ed. Other messages are custom, game- Player Player Player specific messages that you define. To send a message to another player, you simply call the appropriate DirectPlay function and provide the ID of the play- Figure 2. DirectPlay client/server session connections. er with the message to be sent. The tar- get game then receives the message and processes it accordingly. level, so it is technically possible to show You might have noticed that DirectPlay Implementation all available sessions of all game types, DirectPlay imposes a client/server model DirectPlay is implemented as a COM which might be useful in a game finder for initially connecting to game sessions. object that represents the entire commu- application that shows all game sessions One of the players must perform the ini- nications environment for an applica- and then launches the appropriate one tial session creation. This is the server tion. The DirectPlay COM object, based on the user’s choice. game. All other players connect to this DirectPlay, provides access to Direct- How are sessions created to begin game as clients. After connections are Play’s functionality. DirectPlay contains with? The original player is responsible made, it doesn’t matter who made the two API functions used to enumerate for initially creating the game session to initial connection. In this way, the DirectPlay servers and create DirectPlay which other players will connect. When client/server model is in effect only dur- objects. You always use one of these creating a new session, you assign a ing the initial session creation and con- functions to create an initial DirectPlay name to it so other players can find it, nection. Figure 2 shows multiple client object. In fact, you will usually use both such as “Bill’s No-Holds-Barred Cage players connecting to a game created by functions; you will enumerate and dis- Match.” Because all available sessions are the server player. play the available DirectPlay servers and likely to be for the same type of game, it then create a DirectPlay object based on is important for you to give your session Players the server selected by the user. The an identifiable name. Then just sit back DirectPlay maintains a list of current DirectPlay API functions are DirectPlay- and wait for someone to connect to your players in a session and provides an inter- Create and DirectPlayEnumerate. session so you can get down to business. face to manage them. Each player gener- DirectPlayCreate creates and initial- Each type of session must be ally corresponds to other game instances izes a DirectPlay object: assigned a global identifier, which is on the network. Each player has a friend- guaranteed to be unique for all sessions. ly name and a formal name that are set HRESULT DirectPlayCreate(LPGUID lpGUID, DirectPlay uses this identifier when when the player is created, as well as a LPDIRECTPLAY FAR *lplpDP, referring to the session internally. This is player identifier (ID). DirectPlay does not IUnknown FAR * pUnkOuter) how DirectPlay keeps up with games use the player names internally; they are created independently. You can generate solely for player communication during DirectPlayEnumerate is the other half a global identifier for your game by run- the game or for a high score list. Direct- of the DirectPlay API function pair, ning UUIDGEN, which is an applica- Play always uses a player’s identifier when which is used to query the system for the tion that comes with the Win32 SDK. It working with players internally. available network service providers: requires a network card to generate DirectPlay also supports player unique identifiers, since all network groups, which can be thought of as HRESULT DirectPlayEnumerate(LPDPENUMDP- cards have a unique identifier associated teams. A player group appears like a CALLBACK lpEnumDPCallback, with them. player in the session. Information then LPVOID lpContext)

32 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Each installed network service FormalName, ers (BOOL bEnable) provider contains an entry in the reg- LPHANDLE lpReceiveEvent) istry. DirectPlayEnumerate searches for The EnumPlayers member function these entries and notifies you of each After you create or connect to a ses- enumerates the current players in a session: supported network server. Practically sion, you call CreatePlayer to create a speaking, you will always want to enu- local player. When you successfully cre- HRESULT IDirectPlay::EnumPlayers merate and display the available network ate a new player using CreatePlayer, (LPDPENUMPLAYERSCALLBACK servers so the user can select from them. DirectPlay sends a DPSYS_ADDPLAYER sys- lpEnumPlayersCallback, LPVOID lpCon- After the user selects a server, you pass tem message to all other players in the text, DWORD dwFlags) its global identifier into DirectPlayCreate session notifying them of the new player. to create the DirectPlay object and bind You are allowed to create multiple local The EnumSessions member function it to the selected network server. players, in which you use a single enumerates the current game sessions: The DirectPlay object itself repre- machine for multiple player interaction. sents the physical network connection An example of this scenario is having HRESULT IDirectPlay::EnumSessions and associated information about the two joysticks connected to one machine. (LPDPSESSIONDESC lpDPSessionDesc, connection. To create the DirectPlay DirectPlay imposes no limitations on the LPDPENUMSESSIONSCALLBACK lpEnumSes- object, you specify which DirectPlay number of local and remote players, sionCallback, LPVOID server the object will bind to for actual although you can limit the number of lpContext, DWORD dwFlags) communication. Once the DirectPlay players that can be added to your game. object is created, you can establish a net- The DestroyPlayer member function EnumSessions is used to build a list of work connection. When you get a point- destroys a player from a game session: the available sessions, in which you can er to a DirectPlay object via a call to DirectPlayCreate, you don’t have a point- HRESULT IDirect- er to the DirectPlay object itself; you Play::DestroyPlay- Listing 2. The CGame::DPCreateSession Member Function for TicTacToe have a pointer to the IDirectPlay inter- er(DPID DPId) BOOL face of the DirectPlay object. The IDi- CGame::DPCreateSession() rectPlay interface defines the functions You must call { implemented by the DirectPlay object. DestroyPlayer to if (m_pDirectPlay) The most useful functions supported by destroy any local { // Get session information the IDirectPlay interface are: Close, Enum- players you have cre- CSessionInfoDlg dlgSessionInfo; Sessions, Open, CreatePlayer, GetCaps, ated before closing if (dlgSessionInfo.DoModal() == IDOK) Receive DestroyPlayer GetMessageCount , , , the game session. { SaveSession, EnableNewPlayers, GetPlayer- After you successful- // Create a new DP session Caps, Send, EnumPlayers, GetPlayerName, ly destroy a player DPSESSIONDESC dpsdDesc; and SetPlayerName. using DestroyPlayer, ::ZeroMemory(&dpsdDesc, sizeof(DPSESSIONDESC)); The Close member function, HRE- DirectPlay sends a dpsdDesc.dwSize = sizeof(DPSESSIONDESC); SULT IDirectPlay::Close(), closes the DPSYS_DELETEPLAYER dpsdDesc.dwMaxPlayers = 2; communications channel (session) for system message to dpsdDesc.dwFlags = DPOPEN_CREATESESSION; the DirectPlay object. all the other players dpsdDesc.guidSession = TICTACTOE_10; This means the session will be in the session notify- ::strcpy(dpsdDesc.szSessionName, dlgSessionInfo.Get- Name()); closed, and all communications will be ing them of the if (m_pDirectPlay->Open(&dpsdDesc) == DP_OK) stopped. Because Close ultimately player exiting the { destroys the session connection, you session. // Create local player and set game info always must destroy any local players The EnableNew- m_pDirectPlay->EnableNewPlayers(TRUE); before calling it. Some service providers Players member func- if (DPCreateLocalPlayer()) will not allow a session to close until all tion toggles the { players have been destroyed. This is capability to add new DPCreateEventThread(); especially important when the player players and groups to m_bMyTurn = TRUE; who created the session tries to close it. a session and can be return TRUE; The CreatePlayer member function used to keep other } creates a player for a particular session: players from joining } } a session: } HRESULT IDirectPlay::CreatePlayer(LPDPID return FALSE; lpDPId, LPSTR HRESULT IDirect- } lpPlayerFriendlyName, LPSTR lpPlayer- Play::EnableNewPlay-

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 33 DIRECTPLAY

The GetPlayerCaps member function LPDWORD lpdwLength) retrieves the capabilities of a particular player: You use this function to receive information from other players and HRESULT IDirectPlay::GetPlayerCaps from DirectPlay regarding the status of (DPID DPId, LPDPCAPS lpDPPlayerCaps) the game. Receive always processes mes- sages with respect to a particular player. The GetPlayerName member function DirectPlay has a set of system messages queries DirectPlay for a player’s friendly with corresponding structures contain- and formal names: ing information specific to the system message. You can access the informa- HRESULT IDirectPlay::GetPlayerName tion in each system message first by (DPID DPId, LPSTR lpFriendlyName, casting the message data to the generic LPDWORD lpdwFriendlyNameLength, LPSTR message structure, DPMSG_GENERIC, and lpFormalName, LPDWORD lpdwFormalName- looking at the dwType message type Figure 3. The TicTacToe sample game. Length) member. The message type will corre- spond to one of the DirectPlay system The GetPlayerName function is very messages. Once you know the type, you provide an interface for the user to select useful if you want to notify others about then can cast the data to the message a session to join. This technique is useful a player’s actions—for example, a player structure of the appropriate type to on the client end of a game connection, leaving the game. access the message-specific data. because it looks for preexisting game ses- The Open member function opens The SaveSession member function sions to select from. the DirectPlay object and establishes a saves information regarding the current The GetCaps member function gets network connection, which means either session to the registry: the capabilities of the DirectPlay object, creating a new session or connecting to which is dependent on the network serv- an existing session: HRESULT IDirectPlay::SaveSession(LPSTR er to which the object is bound: lpName) HRESULT IDirectPlay::Open(LPDPSESSIONDE- HRESULT IDirectPlay::GetCaps (LPDPCAPS SC lpDPSessionDesc) This includes information, such as lpDPCaps) the player’s friendly and formal names The user interface required to actu- and phone number, in the case of a The GetMessageCount member func- ally establish the connection is handled modem connection. tion determines the number of Direct- by DirectPlay, such as the dialing inter- The Send member function is the Play messages waiting for a particular face for a modem connection. companion to Receive and is used to player and is used to determine when to The Receive member function send information to other players in the receive messages for a player: receives pending messages for a player: session:

HRESULT IDirectPlay::GetMessageCount HRESULT IDirectPlay::Receive(LPDPID HRESULT IDirectPlay::Send(DPID DPIdFrom, (DPID DPId, LPDWORD lpdwCount) lpDPIdFrom, LPDPID lpDPIdTo, DPID DPIdTo, DWORD DWORD dwReceiveFlags, LPSTR lpMessage, dwFlags, LPSTR lpMessage, DWORD dwLength)

Listing 3. The CGameDPCreateLocalPlayer Member Function for TicTacToe The SetPlayerName member function BOOL CGame::DPCreateLocalPlayer() { // Create local DP player CPlayerInfoDlg dlgPlayerInfo; if (dlgPlayerInfo.DoModal() == IDOK) return (m_pDirectPlay->CreatePlayer(&m_dpidPlayer[0], dlgPlayerInfo.GetFriendlyName(), dlgPlayerInfo.GetFormalName(), &m_hDPEvent) == DP_OK); Figure 4. The TicTacToe Server return FALSE; Selection dialog box. }

34 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com sets the friendly and formal names of a creates a new session and the other phone number of the server session, player: player connects to it. So, the server DirectPlay dials the number and estab- player first must choose Create from lishes the modem connection. Once con- HRESULT IDirectPlay::SetPlayerName(DPID the menu, which causes the Server nected, the remote player must enter his DPId, LPSTR Selection dialog box (shown in Figure or her own player information so that lpFriendlyName, LPSTR lpFormalName) 4) to appear. DirectPlay can create a local player. After In the example in Figure 4, a entering player information, the remote modem connection has been selected. player then sees a list of players currently Using DirectPlay in Games The Enter Session Information dialog in the game and must select one to play The first function of a DirectPlay game box appears and prompts for the name with. Of course, in TicTacToe, there is is to determine whether the user intends of the new session. After you enter the always just one other player. The main to create a new session or connect to an session information, the session is reason for including this feature in Tic- existing session. This function is accom- opened and the Enter Player Informa- TacToe is to show how to enumerate plished through some type of user inter- tion dialog box appears. Here, you enter other players when joining a session. The face, as determined by the DirectPlay information regarding the local player, remote player must select the server player service provider. After the user decides yourself. from a Player Selection dialog box. whether to create a new session or con- TicTacToe then nect to an existing session, the DirectPlay creates a player using the Listing 4. The CGame::DPConnectSession Member Function for TicTacToe object must be created and opened with friendly and formal BOOL the proper settings. names you entered in the CGame::DPConnectSession() The next step is to create local play- Enter Player Information { ers for the session. After the session is dialog box. At this point, if (m_pDirectPlay) open and the players are created, the a new session has been { game is ready to begin. Remember, the created with a player rep- // Select a DP session same application will be running on both resenting you, the local CSessionSelDlg dlgSessionSel(m_pDirectPlay); ends, so all players will be visible to each player. Now you just sit if (dlgSessionSel.DoModal() == IDOK) other as soon as they are created. The back and wait for a { game then begins, and the play carries remote player to join in. // Open remote DP session DPSESSIONDESC dpsdDesc; on in a way determined by your game- On the remote end, ::ZeroMemory(&dpsdDesc, sizeof(DPSESSIONDESC)); specific messaging protocol. the player chooses Con- dpsdDesc.dwSize = sizeof(DPSESSIONDESC); In addition to handling your own nect from the Game dpsdDesc.dwFlags = DPOPEN_OPENSESSION; messages, you need to handle DirectPlay menu. He or she must dpsdDesc.guidSession = TICTACTOE_10; system messages. This is very important choose a modem con- dpsdDesc.dwSession = dlgSessionSel.GetSelSession(); because it is possible for players to drop nection from the Server if (m_pDirectPlay->Open(&dpsdDesc) == DP_OK) out of the game, in which case you will Selection dialog box, { get a system message indicating that the just as you did. After // Prompt user to select the remote player player has left the session. selecting the network CPlayerSelDlg dlgPlayerSel(m_pDirectPlay); TicTacToe is a sample game that server, things change. if (dlgPlayerSel.DoModal() == IDOK) uses DirectPlay to manage network Instead of specifying a { // Set remote player communications for two players. It is a new session name, the m_dpidPlayer[1] = dlgPlayerSel.GetSelPlayer(); very simple turn-based game that shows remote player is prompt- // Create local player and set game info the basics of DirectPlay communication. ed with a list of available m_pDirectPlay->EnableNewPlayers(TRUE); Figure 3 shows what TicTacToe looks sessions from which to if (DPCreateLocalPlayer()) like during a game. choose. In this case, { there is only a single DPCreateEventThread(); Running TicTacToe entry for dialing up a m_bMyTurn = FALSE; The TicTacToe main menu contains a modem session. NewGame(); Game pull-down menu with the fol- After selecting the return TRUE; lowing menu choices: Create, Connect, modem dial session, the } End, and Exit. Create makes a new remote player sees the } } network session, Connect joins to an Dial dialog box. The } existing network session, End stops a DirectPlay model server } network session, and Exit terminates handles this interface. return FALSE; the application. To establish a two- After the remote } player network connection, one player player specifies the

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 35 DIRECTPLAY

Listing 5. The CGame::DPEventMsg Member Function for TicTacToe UINT pmsgGeneric)->szShortName); CGame::DPEventMsgStart(LPVOID pData) AfxGetMainWnd()->MessageBox(sText, { AfxGetAppName()); // Call the DP event handler // Set new player and start new game ASSERT((CGame*)pData); if (((DPMSG_ADDPLAYER*)pmsgGeneric)->dpId != ((CGame*)pData)->DPEventMsg(); m_dpidPlayer[0]) return 0; { } m_dpidPlayer[1] = ((DPMSG_ADDPLAYER*) void pmsgGeneric)->dpId; CGame::DPEventMsg() NewGame(); { } while(TRUE) break; { // Wait for event case DPSYS_DELETEPLAYER: if (::WaitForSingleObject(m_hDPEvent, INFINITE) != AfxGetMainWnd()->MessageBox("Player Deleted!", WAIT_TIMEOUT) AfxGetAppName()); { if (((DPMSG_DELETEPLAYER*)pmsgGeneric)->dpId == // Process event message m_dpidPlayer[1]) if (m_pDirectPlay) { { m_dpidPlayer[1] = 0; DPID dpidFrom, dpidTo; DPEndSession(); BYTE Msg[256]; } DWORD dwLen = 128; break; if (m_pDirectPlay->Receive(&dpidFrom, &dpidTo, } DPRECEIVE_ALL, Msg, &dwLen) == DP_OK) } { else if (dpidFrom == 0) if (dpidTo == m_dpidPlayer[0]) { { // Got a system message // Got a remote player turn message DPMSG_GENERIC* pmsgGeneric = (DPMSG_GENERIC*)Msg; if (dwLen == sizeof(POINT)) CString sText; { switch(pmsgGeneric->dwType) CPoint ptTile(*((POINT*)Msg)); { DPReceiveTurnMsg(ptTile); case DPSYS_CONNECT: } AfxGetMainWnd()->MessageBox("Connected!", else AfxGetAppName()); AfxGetMainWnd()->MessageBox( break; "Unknown player message.", AfxGetAppName()); case DPSYS_SESSIONLOST: } AfxGetMainWnd()->MessageBox("Session lost!", } AfxGetAppName()); } DPCleanup(); } break; } case DPSYS_ADDPLAYER: } // Notify of new player sText.Format("New Player : %s", ((DPMSG_ADDPLAYER*)

TicTacToe is set up so that the serv- things for a moment. When the remote Regardless of the outcome, the er player always gets to go first. Even so, player first connected to the server ses- player who was to go next starts the next it is important for the remote player to sion, the server player received a connec- game playing Xs. And the game goes on know that the game has begun. That is tion system message. After being notified until one of the players ends the session the reason for notifying the remote player of the remote player’s connection, the by choosing End from the Game menu of the server player’s turn. At this point, server player is sent an AddPlayer message or by closing the application. the game has begun, and the remote play- containing information about the remote er is waiting for the server player to make player. At this point, the server side now Under The Hood the first move. knows about the remote connection and Now that you’ve got a feel for how Tic- Let’s jump back to the server side of the remote player, so the game begins. TacToe runs, let’s take a look at how it

36 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com works. The code that supports Direct- new players and calls DPCreateLocal Play- created, and the m_bMyTurn member vari- Play is mostly located in the CGame er. The source code for DPCreateLocal able is set to FALSE to indicate that the class. Incidentally, all the source code Player is shown in Listing 3. client player goes second. Finally, a new files for the TicTacToe game can be DPCreateLocalPlayer displays a dialog game is started. found on the Game Developer web site. box using the CPlayerInfoDlg dialog object During the course of the game, all The CGame class models the TicTac- to obtain the friendly and formal names DirectPlay messages are processed by Toe game itself and maintains the of the new player. It then uses these the DPEventMsg member function (List- DirectPlay connection, along with the names in a call to the DirectPlay object’s ing 5). This function is called automati- players and the game-synchronization CreatePlayer member function to create cally when a DirectPlay event occurs. logic. CGame keeps a pointer to the the local player. You’ll notice that Cre- The first call is to WaitForSingleOb- DirectPlay object in m_pDirectPlay. This atePlayer is passed a pointer to an event ject, which is a Win32 API function pointer is set by the DPInit member handle, m_hDPEvent, as its last parameter. that waits for an event to be signaled function, which is called by the applica- This event handle specifies a Win32 Manu- before returning. The significance of tion to initialize DirectPlay services for al Reset event that is signaled when the WaitForSingleObject is that it remains in a the game. The source code for DPInit is player has waiting messages. After creat- sleep state while waiting for the event to shown in Listing 1. ing the local player, DPCreateSession cre- occur. You specify an infinite time-out DPInit initializes the player mem- ates an event thread by calling DPCre- period so that it will never time out. bers, m_dpidPlayer[2], and prompts the ateEventThread. Finally, DPCreateSession The first step in processing Direct- user to select a network game server by sets the turn member variable, m_bMyTurn, Play messages is to receive the message using the dialog object CServerSelDlg. to TRUE, which indicates that the server and check the identifier of the source The server identifier retrieved from the side of the game goes first. At this point, player to see whether it is a system mes- Server Selection dialog box then creates the session has been created and the local sage. System messages always are sent the DirectPlay object by calling Direct- player is eagerly awaiting a connection by from player 0. If the message is a sys- PlayCreate. another player. tem message, you cast the data to a DPCleanup is the corresponding So how does the remote player con- generic message structure to get the member function for cleaning up the nect to an existing session, like the one type of message. If a player has been DirectPlay support. It first calls DPEnd- created by the server player with DPCreate- added, you notify the local player, set Session, which destroys the local player Session? DPConnectSession connects to the remote player identifier member by calling DPDestroyLocalPlayer. It closes existing sessions and is very similar to variable, and start a new game. This the DirectPlay object and deletes the DPCreateSession. The primary difference is scenario occurs when a remote player DirectPlay event thread used to process that DPConnectSession displays the Session connects to a session created by the messages. Then DPCleanup releases the Selection dialog box using the CSession- local player. If a player is deleted, which DirectPlay object and NULLs the member SelDlg dialog object, instead of prompting would correspond to the remote player pointer. for information regarding a new session. quitting, the local player is notified and CGame creates a DirectPlay session The DPOPEN_OPENSESSION flag is used in the the session is terminated. through the DPCreateSession member DPSESSIONDESC structure to specify that you If the message is not a system mes- function, as shown in Listing 2. are attempting to open an existing ses- sage, the message is cast to a POINT struc- DPCreateSession first prompts for sion. The source code the name of the new session by using for DPConnectSession is Listing 6. The CGame::DPReceiveTurnMsg Member Function for TicTacToe the CSessionInfoDlg dialog object. It shown in Listing 4. then uses this name to help fill out a After opening BOOL DPSESSIONDESC structure that passes into the session, the local CGame::DPReceiveTurnMsg(CPoint& ptTile) { the Open member function of the Direct- player is prompted to // Check remote turn message for valid tile bounds Play object. The maximum number of select the server play- if ((ptTile.x >= 0) && (ptTile.x <= 2) && (ptTile.y >= 0) && players is set to 2 and the open flag is er from the dialog box (ptTile.y <= 2)) set to DPOPEN_CREATESESSION. The session displayed by the { identifier is set to TICTACTOE_10, which CPlayerSelDlg dialog // Update game with remote turn data specifies that this is version 1.0 of Tic- object. The identifier SetTileState(ptTile.x, ptTile.y); TacToe. TICTACTOE_10 is a global identi- of this player is stored fier that uniquely identifies the TicTac- away for later com- return TRUE; Toe game. It was obtained by running munication, and the } the UUIDGEN application, and is local player is created defined in the file GUID.H. by calling DPCreateLo- return FALSE; } After opening the new session, calPlayer. The player DPCreateSession enables the addition of event thread is then

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 37 DIRECTPLAY

ture and passed to DPReceiveTurnMsg. local player’s turn, in which case it You’ve seen first hand how you can DPReceiveTurnMsg notifies the local game sends the tile coordinates to the remote use DirectPlay to create a fully function- of the remote player’s move, as shown in player to signify the move. This is han- ing network game. Almost every aspect of Listing 6. dled by calling DPSendTurnMsg. DPSend- using DirectPlay was touched on, along The game-specific messages sent TurnMsg simply calls the DirectPlay with sample code for you to reuse in your between players correspond to coordi- object’s Send member function with the own games. nates on the TicTacToe grid. These coor- proper parameters. After updating the Although a practical network game dinates are used to specify each player’s remote game, SetTileState updates the implementation often gets messy, you move. A POINT structure is used to pass local game by changing turns, setting have the building blocks required to this information in DPReceiveTurnMsg. the tile state, and updating the window frame up a network game so you can DPReceiveTurnMsg receives this structure so that the new tile state is displayed. focus on synchronization details. You also and sets the state of the grid tile to the The source code for SetTileState is have some pretty clean interface objects to appropriate value, X or O, by calling Set- shown in Listing 7. use for working with DirectPlay. You TileState. After setting the new tile state in have all you need to go write a cool net- SetTileState is the workhorse both games, SetTileState proceeds to work game for Windows 95! ■ function for maintaining the state of check for a win or draw by calling IsWin- the game. It is passed the X and Y val- ner and IsDraw. These two functions con- Michael Morrison is the co-author of ues of the grid tile to be set. It first tain the logic for determining whether a Windows 95 Game Developer’s Guide checks to make sure that the tile is player has won the game or whether the to Using the Game SDK. You can contact empty. It then checks whether it is the game is a draw. That’s it! him via e-mail at [email protected].

Listing 7. The CGame::SetTileState Member Function for TicTacToe : BOOL // Determine winner and notify CGame::SetTileState(UINT uiX, UINT uiY) if (m_bMyTurn) { { ASSERT((uiX < 3) && (uiY < 3)); CWave wavLose(IDW_LOSE); CWave wavTile; wavLose.Play(); if (m_tsGrid[uiX][uiY] == tsEMPTY) AfxGetMainWnd()->MessageBox("Bummer, you lost!", { AfxGetAppName()); // Send tile info to remote player via a turn message } if (m_bMyTurn) else if (!DPSendTurnMsg(CPoint(uiX, uiY))) { { CWave wavWin(IDW_WIN); AfxGetMainWnd()->MessageBox("Error sending turn message.", wavWin.Play(); AfxGetAppName()); AfxGetMainWnd()->MessageBox("Congratulations, you won!", return FALSE; AfxGetAppName()); } } // Change turns and set the tile state // Start new game m_bMyTurn = !m_bMyTurn; return NewGame(); m_tsGrid[uiX][uiY] = (m_uiTurns % 2) ? tsO : tsX; } // Update grid else AfxGetMainWnd()->Invalidate(FALSE); { // Play the tile wave if (IsDraw()) wavTile.Create((m_uiTurns % 2) ? IDW_O : IDW_X); { wavTile.Play(); // Play draw wave } CWave wavDraw(IDW_DRAW); else wavDraw.Play(); { // Notify of a draw // Play the tile error wave AfxGetMainWnd()->MessageBox("It's a draw!", AfxGetAppName()); wavTile.Create(IDW_ERROR); // Start new game wavTile.Play(); return NewGame(); return FALSE; } } } // Check for winner/draw return TRUE; if (IsWinner()) } {

38 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com DIRECTSOUND

DirectSound Unplugged

ound is a powerful, expressive Boheme in New York City, but you wear ware directly, and, as a bonus, provides a medium—more powerful, I earplugs. Now, although you may actually solid base for future sound technology believe, than even our visual find the music tolerable under this condi- developments. sense for conveying informa- tion, opera without sound is essentially In this article, we’ll discuss every- tion and emotion. John Rat- just a bunch of fat mimes. And who thing you need to know to add Direct- cliff, designer of Seawolf and wants to watch that for three hours? Sound to your application. We’ve only 688 Attack Sub, has a So there’s really no doubt about how got four thousand words to do it—which favorite example of sound’s much atmosphere sound can add to a isn’t a lot (my bad memories of writing Simpact: compare a tyrannosaurus rex game. Unfortunately, the Windows APIs class notwithstanding), so we’re going to scene in Jurassic Park both with and traditionally have given short shrift to have to cruise. Buckled in? without the sound track. audio. Well, no longer—under Windows My example is even more dramatic: 95, DirectSound allows you to do every- This is Not an Overview imagine you watch the great opera La thing you could do by accessing the hard- Normally, the folks at Game Developer magazine respond to the word “overview” Listing 1. A Function that Creates Awesome Secondary Buffers like a French chef would respond to a request for ketchup. So, to keep the edi- HRESULT CreateDSBuffer(LPDIRECTSOUND lpDS, LPDIRECTSOUNDBUFFER * lplpDSB, torial saliva out of my alphabet soup, we’ll DWORD SoundBytes, DWORD Frequency, int IsStereo, int Is16Bit) zoom through this section as quickly as { we can. DSBUFFERDESC dsbd; First, the DirectSound API is based PCMWAVEFORMAT fmt; on the Component Object Model fmt.wf.nChannels=(IsStereo)?2:1; (COM). COM arrived with OLE, but it fmt.wBitsPerSample=(Is16Bit)?16:8; can stand alone as a standard way to pre- fmt.wf.nSamplesPerSec=((DWORD)Frequency); sent an API to an application. It lets C++ fmt.wf.nBlockAlign=fmt.wf.nChannels*(fmt.wBitsPerSample>>3); people access the API with nice object- fmt.wf.nAvgBytesPerSec=((DWORD)fmt.wf.nSamplesPerSec)*((DWORD)fmt.wf.nBlockAlign); oriented code, and it lets C people access fmt.wf.wFormatTag=WAVE_FORMAT_PCM; memset( &dsbd, 0, sizeof(dsbd) ); the API with weird macros. We’ll show dsbd.lpwfxFormat=(LPWAVEFORMATEX)&fmt; both types of calling sequences in this dsbd.dwSize=sizeof(DSBUFFERDESC); article. dsbd.dwBufferBytes=SoundBytes; COM-based APIs are all used the dsbd.dwFlags=0; same way. You call a Create function that In C++: return( lpDS->CreateSoundBuffer( &dsbd, lplpDSB, 0) ); returns a pointer to an object (C pro- In C: return( IDirectSound_CreateSoundBuffer( lpDS, &dsbd, lplpDSB, 0) ); grammers read “structure”). This object } contains the important data, as well as // Sample use of the CreateDSBuffer function member functions (C programmers read LPDIRECTSOUNDBUFFER lpDSB; “function pointers”) that operate on the if (CreateDSBuffer( lpDS, &lpDSB, TotalSoundBytes, 22050, 0 , 0) ) { // Open 22050, mono, object. So, with COM, everything the 8 bit sample // Use the DirectSoundBuffer API can do is accessed through an object. In C++: lpDSB->Release(); In the DirectSound COM API, we In C: IDirectSoundBuffer_Release( lpDSB ); find two objects: the DirectSound object } and the DirectSoundBuffer object. You create the DirectSound object to gain

40 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Jeff Roberts access to everything that DirectSound can COM happy. do. Once you have created this object, it Once you have the DirectSound can (among other things) create the object, you can call any of the eleven DirectSoundBuffer object, which is the member functions that it currently con- You‘ve long wanted object that actually plays sounds (you tains. However, there are really only three knew that feature was in there some- member functions that you will normally where, right?). use: SetCooperativeLevel, CreateSound- Make sense? If not, don’t sweat it— Buffer, and Release. The other member direct access to the just remember that we have to create functions are for infrequent tasks like objects to do anything in DirectSound querying capabilities, compacting on- (and in any other DirectX APIs for that board sound memory, and managing matter). speaker configuration. Don’t worry about them—I’ve never had to use them and hardware within DirectSound Objects you probably won’t either. The DirectSound object is the key to using You must, on the other hand, use the the DirectSound API. To create a Direct- SetCooperativeLevel member function. If Sound object of this type, you simply call you don’t call it after creating your Direct- Windows, eh? the DirectSoundCreate function. Since this Sound object, most of the other member call is one of only two functions that functions won’t work. This silly goof has aren’t member functions of an object (the burned me at least once, and, judging by other is DirectSoundEnumerate), the calling the CompuServe message traffic, plenty of Here ‘tis. DirectSound sequence is the same for both C and C++: others. So, if you get a DSERR_INVALIDPARAM result from one of the DirectSound func- LPDIRECTSOUND lpDS; tions, check your code and make sure you if (DirectSoundCreate(NULL, &lpDS, NULL) have set your co-op level. prodvides a method == DS_OK) Since the SetCooperativeLevel call is // lpDS is now a valid DirectSound our first member function, let’s stop for a object moment and discuss calling a COM else member function from C++ and C. An for playing back and // the DirectSoundCreate call example of a SetCooperativeLevel call in failed (lpDS is NULL) the two dialects is as follows: In C++: The first NULL in the DirectSoundCre- ate call is the ID of the DirectSound lpDS->SetCooperativeLevel mixing digitally device that you want to open—it will ( YourMainHwnd, DSSCL_NORMAL ); almost always be NULL. You can get a list of other valid IDs with the DirectSound- In C: Enumerate function. The second parameter recorded audio is a pointer to where you’d like the IDirectSound_SetCooperative Level ( DirectSound pointer to be placed (a lpDS, YourMainHwnd,DSSCL_ NORMAL); pointer to an object pointer). The final parameter must always be NULL to keep You can see how C++ treats a within Windows 95.

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 41 DIRECTSOUND

COM object just like a normal C++ Sound mutes all your sound! Although The final common DirectSound object—you call the function just like this is correct behavior for most apps, I member function is Release. This func- you would a normal C++ member func- believe it should have been under our tion simply frees the DirectSound object. tion. In C, however, you must use control—not the API’s. DirectSound 2 Call it at the end of your application to macros to make calls to the member is supposed to fix this lapse with sup- close DirectSound. You may notice that function. These macros serve to make port for background sounds. the Release function isn’t shown in the the function calls cleaner and to mask Anyway (I’ll make it through this DirectSound help file because Release any changes Microsoft may make to function yet), the final parameter to is one of the standard COM member COM in the future. SetCooperativeLevel is the priority level functions. It is there, though, and you All COM object macros follow you are requesting. There are several should always call it when you’re fin- the same naming convention: an upper- different priority levels, but you will ished with DirectSound. case “I,” the name of the object, an almost always use DSSCL_NORMAL, which That wraps up the DirectSound underscore, and, finally, the name of signifies fully cooperative status (as object—doesn’t do much, does it? It the member function that you wish to opposed to grumpy, pain-in-the-ass does, however, allow us to create call. For example, a BillG COM object status, I suppose). Actually, the other DirectSoundBuffer objects, where the with a Boolean member function would priority levels mostly create primary true coolness of DirectSound lies. have a macro called IBillG_IsLoaded() sound buffers, which you should rarely that always returned TRUE. need to do. So, for our purposes, just DirectSoundBuffer Objects OK, back to SetCooperativeLevel— use DSSCL_NORMAL. DirectSoundBuffer objects are containers the first parameter (besides the object The next function on my common for your actual audio data. They con- pointer itself) is a handle to your appli- list is CreateSoundBuffer. This function tain both the sound format (bit-depth, cation’s main window. Why would creates a DirectSoundBuffer object. We frequency, and so on) and a buffer for DirectSound need an HWND? Good ques- will discuss these objects in the next the sound data itself. There are two tion! Microsoft considers sound a “sys- section—they’re where all the action is, types of DirectSoundBuffer objects: pri- tem resource,” so when a user flips so let’s finish up the DirectSound object mary and secondary. You will always away from your application, Direct- first. create secondary buffers, unless you have a very unusual use for the primary buffer (I know of only one, which I’ll Listing 2. The Locking Process talk about in a moment). Secondary buffers are nice because HRESULT LoadSoundData(LPDIRECTSOUNDBUFFER lpDSB, char* SoundDataPtr, DWORD TotalBytes) you can have many open at once. Dur- { ing playback, each buffer is volume- LPVOID ptr1,ptr2; scaled, pan-scaled, bit-depth adjusted, DWORD len1,len2; and mixed with other buffers complete- HRESULT result; ly on the fly. After the final buffer is TryLockAgainLabel: mixed, the resultant sound data is In C++: result = lpDSB->Lock( 0, TotalBytes, &ptr1, &len1, &ptr2, &len2, 0 ); placed into the primary buffer to be In C: result = IDirectSoundBuffer_Lock( lpDSB, 0, TotalBytes, &ptr1, &len1, &ptr2, &len2, heard. You don’t have to worry about 0 ); converting, massaging, or mixing any switch (result) { of the data—you just let DirectSound case DS_OK: // The DirectSound buffer was locked successfully memcpy( ptr1, SoundDataPtr,len1); deal with it. Pretty cool! if (ptr2) Which, indirectly, brings us to the memcpy( ptr2, SoundDataPtr + len1, len2); only reason to use primary buffers— In C++: lpDSB->UnLock( ptr1, len1, ptr2, len2 ); because all secondary buffers are mixed In C: IDirectSoundBuffer_Unlock( lpDSB, ptr1, len1, ptr2, len2 ); into the primary buffer, it is the prima- break; ry buffer that governs the final sound case DSERR_BUFFERLOST: // The DirectSound buffer was lost - try to restore quality. For example, if you play a 16- In C++: result=lpDSB->Restore(); bit, 44 KHz secondary buffer, but the In C: result=IDirectSoundBuffer_Restore( lpDSB ); primary buffer is only 8-bit, 11 KHz, if (result == DS_OK ) // If the restore worked, go do the lock again then your sound data will be scaled goto TryLockAgainLabel; down to the primary buffer’s format. break; } So, if your sound card is capable, return( result ); you can create a primary buffer and } change its output format to deal with this problem. Usually though, the pri-

42 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com mary buffer will be set in the best out- Loading Data into a parts of DirectSound is the fact that put mode for your particular sound DirectSoundBuffer you can “lose” your sound buffer. Los- card, so you’ll never need to change it. To load sound data into our secondary ing a sound buffer means that the buffer Because of this fact, we’ll focus on the buffer, we have to use the Lock, Unlock that was holding your sound data has more useful secondary buffers for the and Restore member functions. The been appropriated for other Direct- remainder of this article. If you really locking process is a bit complicated, so Sound needs. (Even stranger, on some want to use the primary buffer and get let’s start with an example function as new video-sound combination cards, stuck, e-mail me, and I’ll try to help. shown in Listing 2. you can also lose your sound buffers to So how do we create these awe- Geez, that’s a lot of code just to DirectDraw!) some secondary buffers? Well, the load a buffer! It’s pretty simple once Losing a buffer is usually no big example function in Listing 1 does just we’ve walked through it though. deal—you just call the Restore member that. The Lock function gives us access function and reload the sound data into The first thing this function does to the DirectSound buffers. Its first para- the buffer. You can implement various is set up a PCMWAVEFORMAT structure that meter is the starting byte location of the strategies to deal with this: reload the contains the type of sound data the sec- lock you request—this will normally be sound files back off the disk, keep the ondary buffer will contain. Usually, you zero unless you’re streaming sound data sound in another system RAM buffer will simply load this structure from the into the sound buffer (we’ll talk about so that you can reload it at any time, or, header of a .WAV file. For a good this later). The next parameter is the best of all, use streaming buffers (we’ll example of loading and parsing .WAV number of bytes you are locking—this talk about streaming a bit later). The files, check out an article titled, will almost always be the same amount sample code above simply calls the “Recording and Playing Waveform that you used for the dwBufferBytes field Restore function if the buffer was lost, Audio” on Microsoft Developer Net- when you created the buffer. and then retries the lock. work (MSDN). Two sets of pointers and lengths Finally, after you’ve successfully Next, the code sets up a DSBUFFER- are filled in by the lock call. There are locked the buffer and loaded your sound DESC structure that describes the two sets of pointers and lengths because data, you must call the Unlock member requested secondary buffer. The you could conceivably request a lock function to give the buffer back to dwBufferBytes field specifies how large that wraps around the end of the sound DirectSound. Notice that the Unlock the secondary buffer should be in bytes. buffer. If your lock parameters didn’t function doesn’t take pointers to the This amount is usually extracted from cause DirectSound to wrap around its pointers and lengths (like Lock does), the DATA chunk in a .WAV file. sound buffer, then ptr2 will be NULL. but accepts the pointers and lengths The second important field in the With these two pointers, you can use themselves. (Try saying that three times DSBUFFERDESC structure is dwFlags. In memcpy or memmove to place your sound quickly.) this case, we are setting dwFlags to data into the DirectSoundBuffer object. So, loading sound data isn’t too zero, but other useful options are DSB- So far so good, but what does the bad at all. Just remember to have an CAPS_CTRLVOLUME, DSBCAPS_CTRLPAN, and other code do? Well, one of the trickier easy way to reload it if your DirectSound DSBCAPS_CTRLFREQUENCY. These options tell DirectSound that you will be adjusting the volume, pan, or frequen- Listing 3. Code to Play a DirectSoundBuffer cy while the sound is playing. If you don’t specify these options when you DWORD status; create the DirectSoundBuffer, then you TryPlayAgainLabel: won’t be able to control these sound In C++: if ( lpDSB->Play( 0, 0, 0 ) == DS_BUFFERLOST ) attributes at playback time. In C: if ( IDirectSoundBuffer_Play( lpDSB, 0, 0, 0 ) == DSERR_BUFFERLOST ) The code then asks the DirectSound if ( LoadSoundData( lpDSB, SoundDataAddress, TotalSoundBytes ) == DS_OK ) object to go ahead and create a Direct- goto TryPlayAgainLabel; // Try to play the buffer again SoundBuffer object for us. If the function GetAsyncKeyState(VK_ESCAPE); // Clear the state of the Escape key succeeds, the lpDSB variable will now for (;;) { In C++: lpDSB->GetStatus(&status); contain our DirectSoundBuffer object In C: IDirectSoundBuffer_GetStatus(lpDSB, &status); pointer. As with the DirectSound object, if (status!=DSBSTATUS_PLAYING) DirectSound- once we’re done with a break; Buffer, we must call the Release member if (GetAsyncKeyState(VK_ESCAPE)) // If the Escape key is hit, stop the sound function. In C++: lpDSB->Stop(); Now we know how to create a sec- In C: IDirectSoundBuffer_Stop( lpDSB ); ondary buffer, but how do we get our } sound data into it?

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 43 DIRECTSOUND

Listing 4. A Streaming Example that can be Pasted into a DirectSound Application buffer is ever lost. I try to make a stand- alone function that I can call from any- typedef struct DSSTREAMTAG { where in my application if my buffer int Playing; // This field will be non-zero while sound is streaming disappears. int PleaseClose; // Set this field to stop sound streaming Now that we have sound data in char* CurrentPosition; // The next sound address that will be mixed into the DS our DirectSoundBuffer object, we’re buffer ready to play it! DWORD BytesLeft; // How many bytes are left from CurrentPosition DWORD NoCallbacks; // When this is non-zero the timer callback won’t execute Simple DirectSoundBuffer DWORD HalfBufferPoint; // The size of half the DirectSound buffer (don’t change) Playback DWORD LastHalf; // The pointer to the last half buffer that we were in In comparison to the set up and loading (don’t change) int CloseOnNext; // Internal flag to mark the end of playback (don’t change) of the DirectSoundBuffer object, play- LPDIRECTSOUNDBUFFER lpDSB; // The DirectSound buffer that is handling the streaming back is a piece of cake. The two play- char SilenceByte; // The value for silence (different for 8 and 16 bit sounds) back control member functions are Play } DSSTREAM; and Stop, and they do exactly what static void StreamCopy(DSSTREAM* s, char* ptr, DWORD len) // Copy from buffer into DS with you’d guess. As an example, let’s look at end of buffer handling the code in Listing 3 which plays a { DirectSoundBuffer until you press Escape. DWORD amt; The Play member function actually amt=(len>s->BytesLeft)?s->BytesLeft:len; // Only copy what’s left in the main sound starts the sound. It takes three parame- buffer ters—the first two are reserved and if (amt) { must be zero. The final parameter is a memcpy(ptr,s->CurrentPosition,amt); s->CurrentPosition+=amt; flag field. Currently, the only flag is s->BytesLeft-=amt; DSBPLAY_LOOPING which tells DirectSound } to keep looping the DirectSoundBuffer len-=amt; object over and over. The DSBPLAY_LOOP- if (len) { // Fill the remainder of the buffer with silence ING flag is also used to set up a stream- memset(ptr+amt,s->SilenceByte,len); ing sounds. s->CloseOnNext=1; // Set the “done on the next buffer switch” flag Notice that, again, you have to } watch for the sound buffer being lost. If } the buffer is lost, then this code simply static void StreamFillAHalf(DSSTREAM* s, DWORD half) // fill a half of the DirectSound calls the LoadSoundData function that you buffer wrote earlier. This is a workable but { char* ptr1; clumsy solution, because you have to char* ptr2; buffer the sound data twice—once in DWORD len1, len2; your own buffer and once inside the DirectSoundBuffer object. Alternatively, TryLockAgainLabel: you could load the sound data off the switch (s->lpDSB->Lock(half, s->HalfBufferPoint, &ptr1, &len1, &ptr2, &len2, 0)) { disk to save the double memory use. case DS_OK: However, as I alluded to earlier, the StreamCopy(s, ptr1, len1); // Copy sound data into the first pointer best solution is probably to stream the sound data. if (ptr2) // Copy sound data into the second pointer if necessary OK, so once the sample begins StreamCopy(s, ptr2, len2); playing, the above code simply waits s->lpDSB->Unlock(ptr1, len1, ptr2, len2); break; until the GetStatus member function case DSERR_BUFFERLOST: // The DirectSound buffer was lost - try to restore tells us that the DirectSoundBuffer object if (s->lpDSB->Restore() == DS_OK) is finished. This will happen if the goto TryLockAgainLabel; DirectSoundBuffer plays through to the break; end of the sample, or you hit the Escape } key and cause the Stop member function } to be called. static void CALLBACK StreamTimer(UINT id, UINT msg, DWORD user, DWORD dw1, DWORD dw2) There are other member functions { for controlling the volume, pan, fre- DWORD playp,writep; quency, and playback position of the DirectSoundBuffer object, but these are

44 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com all pretty self-explanatory so I’ll let you Listing 4. Continued from p. 44 experiment with them on your own. And that’s all there is to simple DWORD whichhalf; playback. No problem, right? Good, DSSTREAM* s=(DSSTREAM*)user; because our final discussion will be if (s->NoCallbacks++==0) { about streaming audio. It is a bit more if (s->PleaseClose) { // Programmer requested Close - shutdown immediately complicated, but definitely worth ShutDownStreamingLabel: understanding. timeKillEvent(id); timeEndPeriod(62); DirectSound Streaming s->lpDSB->Stop(); s->lpDSB->Release(); Sound streaming is the act of using a s->Playing=0; tiny buffer to play a large sample a little return; bit at a time. Streaming is generally } used to play sound data off a hard drive s->lpDSB->GetCurrentPosition(&playp,&writep); // Get the current position and figure or CD-ROM, but you can also use it to the current half play a large piece of sound data into a whichhalf=(playp < s->HalfBufferPoint)?0:s->HalfBufferPoint; tiny DirectSound buffer. if (whichhalf != s->LastHalf) { This is how you get around the if (s->CloseOnNext) // If we previously used up our sound data, then do a lost buffer problem—you load an entire shutdown sample into system memory and then goto ShutDownStreamingLabel; use DirectSound to play a little bit at a StreamFillAHalf(s, s->LastHalf); // Fill the buffer half that we just left s->LastHalf=whichhalf; time. In this situation, if you lose the } sound buffer, it’s no big deal because } you are not losing the entire sample— s->NoCallbacks--; just a little piece. } As you can imagine, playing sound void StartStreaming(DSSTREAM* s, void* addr, DWORD len, LPDIRECTSOUND lpDS, LPWAVEFORMATEX data this way is more complicated than format) just calling the Play member function. { The nice thing is that this technique DSBUFFERDESC dsbd; can be encapsulated into one function if (s) { call fairly easily, so you can just use the memset(s,0,sizeof(DSSTREAM)); same code over and over again. if ((addr) && (lpDS) && (format)) { memset( &dsbd, 0, sizeof(dsbd) ); Basically, DirectSound streaming dsbd.lpwfxFormat=format; is accomplished by creating a looping dsbd.dwSize=sizeof(DSBUFFERDESC); secondary buffer and placing data into dsbd.dwBufferBytes= ((format->nAvgBytesPerSec/4)+2047)&~2047; it at the right time. DirectSound dsbd.dwFlags=0; believes that it is playing the same if (lpDS->CreateSoundBuffer( &dsbd, &s->lpDSB, 0) != DS_OK) sound over and over, but actually we’re return; placing new sound data into the buffer s->NoCallbacks=1; // Don’t let the callback do anything until we’re fully setup each time it loops around to simulate timeBeginPeriod( 62 ); one long seamless sound. if (timeSetEvent( 62, 0, StreamTimer, (DWORD)s, TIME_PERIODIC )==0) { The easiest way to learn streaming timeEndPeriod( 62 ); is to start with an example that can be s->lpDSB->Release(); } else { pasted right into a DirectSound appli- s->HalfBufferPoint=dsbd.dwBufferBytes/2; cation to implement streaming immedi- s->CurrentPosition=addr; ately. This example will play a sound s->BytesLeft=len; sample that is loaded into system s->SilenceByte= (format->wBitsPerSample==16) ? 0:128; RAM, but you could easily modify it to StreamFillAHalf(s, 0); play sound off a hard drive or CD- StreamFillAHalf(s, s->HalfBufferPoint); ROM. Let’s check it out in Listing 4 s->CloseOnNext=0; // Clear the close flag, so that the first two buffers are (only the C++ calls are shown to make played the code easier to read). s->lpDSB->Play( 0, 0, DSBPLAY_LOOPING ); To start streaming with this exam- s->NoCallbacks=0; ple code, call the StartStreaming func- } tion with a stream structure, the sound

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 45 DIRECTSOUND

Listing 4. Continued from p. 45 terrific, low-level API to play digital sound with little to no latency response. } As this article demonstrates, however, } DirectSound is not a high-level API. A } DirectSound application must handle // Streaming test code: buffer management, callbacks, stream- volatile DSSTREAM s; ing, start and stop control, and such on StartStreaming( (DSSTREAM*)&s, SoundDataAddress, TotalSoundBytes, lpDS, &SoundFormat ); its own behalf. GetAsyncKeyState(VK_ESCAPE); // Clear the state of the escape key For those who don’t want to deal while (s.Playing) { // wait until the sound is done or the user hits escape with the low-level coding that Direct- if (GetAsyncKeyState(VK_ESCAPE)) Sound requires, there are several good s.PleaseClose=1; } libraries that combine DirectSound’s awesome playback abilities with a true application-level API to give you the best of both worlds. data address, the sound data length, the moved from one half buffer to the next, Personally, I think the best thing DirectSound object to use, and the for- then the old half buffer is ready for new about DirectSound is that my com- mat of the sound data. From there, sound data. plaints about the old days of Windows everything is handled automatically, The StreamFillAHalf function is programming are getting better and and sound will start immediately. used to load sound data into one half of better. If you’d like to stop the streaming, the DirectSound buffer. It handles the “Back when I was a Windows pro- just set the PleaseStop field to non-zero. locking, restoring, and unlocking of the grammer, we had to walk to work in You can monitor the playback with the DirectSoundBuffer object. The Direct- two feet of snow every morning, and Playing field: non-zero means that the SoundBuffer object, in turn, calls the hexadecimal hadn’t been invented yet, stream is still playing, and zero means StreamCopy function to move the data and we didn’t have DirectSound, and that the stream has been stopped and its from your large sound buffer into the my mouse was a real dead mouse with resources have been freed. Finally, to tiny DirectSound buffer. wires shoved up its ....” ■ track the playback position, use the Cur- One bit of semi-tricky logic is rentPosition field (it is a pointer that found when the StreamCopy function increases from your starting address as determines that the end of your sound Jeff Roberts is a programmer at RAD playback proceeds). data has been reached. StreamCopy can’t Software, publisher of Smacker and the Now let’s shift from describing how just close the stream immediately, Miles Sound System. He can be reached to use the streaming example to how it because you wouldn’t hear the last little via e-mail at [email protected]. actually works. We’ll begin with the bit of sound, so it sets a flag called StartStreaming function. CloseOnNext. This flag is checked on the The StartStreaming function only next buffer switch in the StreamTimer has to set the appropriate values in the function which lets you hear the last DirectSound for the Impatient DSSTREAM structure and start up the buffer’s worth of sound. or those of you who don’t timer callback. First it creates a small This example code is rather sim- want to read this whole arti- DirectSoundBuffer that will handle one- ple—integrating it into your own appli- cle, just follow the following fourth of a second of audio data. It cation should be a snap. A few cool fea- F easy steps to add Direct- then sets a timer to call the StreamTimer tures to tack on: the ability to pause the Sound to your application in no time: function and assigns all of the initial playback, a smarter callback that handles streaming values. Then the StreamTimer multiple streams (instead of one callback 1. Create a DirectSound object. function will be called 16 times per sec- per stream), and the ability to stream 2. Set the cooperative level to ond (every 62 milliseconds) to process from a disk file. Go crazy with it—after DSSCL_NORMAL. all of the sound data. all, you have the source code! 3. Create a secondary sound buffer. The StreamTimer callback contains 4. Lock the sound buffer. 5. Fill the buffer with your sound data most of the logic for streaming. The This is Not a Summary using the two pointers and lengths first thing it does is to check to see if the Well, you made it! You now know returned by Lock. PleaseClose flag is set; if so, it closes the almost everything there is to know about 6. Unlock the sound buffer. streaming for this sound. Next, the DirectSound. 7. Play the sound buffer. timer checks where the current play After you’ve used it a while, I 8. Release the sound buffer. position is with the GetCurrentPosition think you’ll agree that Microsoft really 9. Release the DirectSound object. member function. If DirectSound has did a great job on DirectSound: it is a

46 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com AUDIO

Playing with Waves

ou’ve just blown away a Basic Wave Theory the amplitude of the wave at a specific room full of bad guys. You The algorithm for mixing two waves isn’t point in time with a number from -127 then hear a door open and a very difficult. Sound waves are additive: to 128. To mix two such waves into a squad of gurgling demons two waves played at the same time pro- third buffer, the mixer steps through behind you. A breeze carries duce a combined wave that is their sum, each wave’s 11,025 samples and adds snatches of tinny post-apoc- as approximated in Figure 1. The goal of them together, storing the result in a alyptic singing forced a software mixer is to allow a single wave third 11,025-byte block to be played out through a wheezing old FM synthesizer to play multiple waves simul- through the speaker. This is the elemen- Yradio. “The population is greatly taneously, so its job is to perform the tal mixing operation: adding two waves. decreased…” You head toward it, hop- addition itself before playing the com- You can perform other operations ing to find a friend. bined wave, instead of leaving the job to on sampled sound data to produce special There’s no substitute for the plea- natural physics. effects. You can change the volume of a sure audio gaming experiences bring to Electronic sound devices usually sound by multiplying or dividing every players’ ears. Without sound, games lack play digitally sampled waves. A digital sampled value by a constant. You can essential life-saving audio cues, humor, description of a sound wave is created by fade a sound by dividing each sample character, and magic. I’ve spent much recording the amplitude of a wave input value by a number that increases or time discussing cross-platform graphics at a constant frequency using a specific decreases across the wave. You can create and user interaction, and it’s about time number of bits to measure the wave at an echo by mixing a wave with itself with I rounded things off with a bit of audio. each time slice. Today’s hardware gener- a slight delay. You can play waves back- In this article, I will leave you with ally handles 8 or 16 bits per sample at wards, slow them down, distort them, or a short demo that runs without change frequencies of 11,025, 22,050, or 44,100 do whatever mathematical transforma- on top of a small core of Windows- samples per second, with higher frequen- tions you like. and Macintosh-specific code. You will cies and resolution producing a more Usually, a game has simple run-time see how to implement a simple Play- accurate replica of the original while needs: it has to mix waves with different Wave function that will allow up to requiring additional memory and pro- starting and ending times into a single four simultaneous sounds to play using cessing power. continuous audio stream. When you fire system services, in this case the Macin- A one-second, 8-bit wave sampled two shots in quick succession, you want tosh Sound Manager and Windows’ at 11,025 hertz is represented by a block to hear the second even though the first DirectSound. of 11,025 bytes, each of which describes has started playing, and you still want any active background music or other noises to play through. Generally, a game doesn’t have the luxury of mixing the two waves together before playing them as one combined

+= wave because they don’t start or end at predefined times. Someone has to mix new sounds into an active audio stream by mixing in the new wave starting just after the point from which the sound Figure 1. Two waves playing simultaneously, approximately. hardware is playing. There are other things a mixer has to worry about, too. For instance, the

48 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Jon Blossom mixer doesn’t have infinite memory at its half, essentially restricting each wave to disposal, so an application may ask to the -63 to +64 range to guarantee the play a sound that’s too large to fit in the sum to fall between -127 and 128, or buffer the mixer is using, so the mixer you can continue as usual, replacing any DirectSound and may have to break the sound into pieces. sum greater than 128 with 128 and less Or maybe the application wants to mix than -127 with -127. I suppose you can two waves sampled at different rates or always ignore the problem, too, but I with different resolutions. A general pur- don’t recommend that option. Sound Manager handle pose mixer has to convert them to a com- We know already that dividing sam- mon format. ples in a wave by a constant (in this case Books have been written on the 2) lowers the volume of the wave. So the process subject of signal processing, and numer- basically, the first technique is to turn ous software mixers and sound editing down every wave to be mixed so that tools abound. You can dig into them, but they’re guaranteed never to exceed the of mixing audio I have neither the space, the time, nor the maximum volume. You divide each sam- expertise to write another volume on ple by the number of waves played before sampling theory. As long as you under- adding them together. That’s the same as stand the basics of what your wave mixer adding them all together and dividing by differently. Knowing does, you don’t have to deal with the the number, so I call this averaging the gritty details. Unless you want to. waves. I’m going to break my impartial reporter front for a second to say that this each platform Pitfalls is the wrong way to mix waves for games! Both DirectSound and Sound Manager The second technique guarantees cope with playing whatever sound in that the waves you play will always be whatever format of whatever length you played at the expected volume, but if the will prevent your throw at them. You can take their abili- sum of all the waves played exceeds the ties for granted. However, it’s important limits of the digital representation, the to understand the basics of wave mixing highs and lows will get chopped off. If game‘s sounds from in order to understand a fundamental you know you’re going to be mixing four performance difference between the two. sounds, you may choose to author your Programmers who write wave mix- sounds in a reduced range so they never ers must inevitably answer the following distort, but that’s up to you. This mixing unexpected clipping question: What happens when the sum technique is called clipping the waves of any two samples from the waves to be because it clips off the highs and lows. mixed exceeds the resolution of the sam- Figure 2 shows these two mixing as well as ple? How do you mix two 8-bit samples methods at work. Looking at these dia- when their sum is greater than 128 or less grams, you can see how averaging dis- than -127? A byte simply can’t handle torts the shape of the wave, squashing it average to low that information. Digital technology has into silence. When you’ve only got let you down. eight bits to describe a sound sample, At this point, you have two basic you don’t want to throw any of them choices. You can divide every sample in away by division! On the other hand, volumes.

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 49 AUDIO

Listing 1. PlayWave for the Macintosh ChannelHeader[ChannelToUse].length = SampleSize; Global channel information initialized by BeginSound ChannelHeader[ChannelToUse].loopStart = SampleSize; SoundHeader ChannelHeader[4]; ChannelHeader[ChannelToUse].loopEnd = SampleSize; SndChannelPtr pChannel[4]; // Allow only 11025Hz samples void PlayWave(int SampleSize, int SampleRate, // This is just to save space. The code on short BitsPerSample, short ChannelCount, // ftp.mfi.com allows 22050 and 44100 as well char unsigned* pSample) ChannelHeader[ChannelToUse].sampleRate = rate11025hz; { ChannelHeader[ChannelToUse].baseFrequency = rate11025hz; // Look for a channel to use to play this sample // Allow only 8-bit samples int ChannelToUse = -1; // Again, see the code on the ftp site for (int Count = 0; Count < 4; ++Count) // The stdSH code indicates 8-bit samples { ChannelHeader[ChannelToUse].encode = stdSH; // An available channel is // Set up the sound data to indicate // recognized by a null sample pointer // that the channel is playing if (pChannel[Count] && !ChannelHeader[Count].samplePtr) ChannelHeader[ChannelToUse].samplePtr = { (char*)pSample; ChannelToUse = Count; // Play the sound! break; SndCommand Command; } Command.cmd = bufferCmd; } Command.param1 = 0; if (ChannelToUse > -1) Command.param2 = (long)&ChannelHeader[ChannelToUse]; { SndDoCommand(pChannel[ChannelToUse], &Command, false); // Found an unused channel... // Queue up a callback to reset the channel // Set up buffer information // header when finished. The command gets passed

50 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Listing 1. Continued from p. 50 communication pipes finished playing the sample. // as an argument to the callback function. to the Sound Manag- That’s all it takes. Every time you // In this case, param2 will contain a pointer to er. They’re essentially send a bufferCmd, the Sound Manager // the memory to be zeroed when the wave terminates. queues of commands mixes all the active sounds into one audio Command.cmd = callBackCmd; to be processed, stream and plays it out the speaker. Command.param1 = 0; including commands For our purposes, we’ll allow four Command.param2 = to play a buffer from sounds to be played at once. The Begin- (long)&ChannelHeader[ChannelToUse].samplePtr; memory, loop over a Sound function creates four sound chan- SndDoCommand(pChannel[ChannelToUse], &Command, false); specific piece of a nels using SndNewChannel, registering the } sample, or perform SoundCallBack function as the target of a } other simple sound callBackCmd command for that channel. It pascal void SoundCallBack(SndChannelPtr pChannel, SndCommand* SoundHeader pCommand) operations. initializes a structure to { To play a sam- describe the sound wave installed in each // This function gets called when we queue up a ple from memory, we channel. Each header initially contains // callBackCmd above, to indicate that the sample have to set up a chan- null as the pointer to its sound, indicating // has finished playing. nel, initialize a the channel is unused. EndSound cleans up // bufferCmd command this work when we’re done playing. // The command's param2 points to the that points to the The PlayWave function, shown in // ChannelHeader.samplePtr of the channel that finished, wave data to be Listing 1, implements the heart of the // which we zero to indicate that it is no longer playing. played, and call Snd- system. It searches the four channels to *((Ptr*)pCommand->param2) = 0; DoCommand to pass the find an unused one, as indicated by a } command down the ChannelHeader whose sample pointer is pipe. By following null. If it can’t find one, it refuses to play. clipping can distort the tips of the wave, that with a callBackCmd, we can have the If it does find a free channel, Play- creating harsh highs and lows like the Sound Manager call us back when it’s Wave fills in the associated ChannelHeader ones you might hear escaping speakers that have been pushed to a volume they can’t support. For reasons I can neither explain nor imagine, Apple decided that Sound Manager should average waves, even if mixing them normally wouldn’t cause clipping. This guarantees you will never hear clipping from waves Sound Manag- er produces, but it also guarantees that individual sounds drop in volume as other sounds begin to play. DirectSound clips waves that exceed the playback resolution. This guarantees that your sounds will be played at full volume, but it also leaves your sounds susceptible to clipping.

Playing the Mac Now we’re going to make some noise, starting with the Macintosh. Two plat- forms worth of code is too much to fit in one article, so I’ve only printed the high- lights here. Check out the Game Develop- er web site for the full source code. In spite of my exhortation against mixing waves by averaging (shudder), I’m going to show you a simple way to use the Sound Manager. Sound Channels are the essential

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 51 AUDIO

Listing 2. PlayWave for DirectSound // Set up buffer information // Global channel information initialized WAVEFORMATEX WaveFormat; // by BeginSound WaveFormat.wFormatTag = WAVE_FORMAT_PCM; static LPDIRECTSOUNDBUFFER pChannel[4]; WaveFormat.nChannels = ChannelCount; static LPDIRECTSOUND pDirectSound = 0; WaveFormat.nSamplesPerSec = SampleRate; void PlayWave(int SampleSize, int SampleRate, WaveFormat.wBitsPerSample = BitsPerSample; short BitsPerSample, short ChannelCount, WaveFormat.cbSize = 0; char unsigned* pSample) WaveFormat.nBlockAlign = WaveFormat.nChannels * { (WaveFormat.wBitsPerSample / 8); // Look for a channel to use to play this sample WaveFormat.nAvgBytesPerSec = WaveFormat.nBlockAlign * int ChannelToUse = -1; WaveFormat.nSamplesPerSec; for (int Count = 0; Count < 4; ++Count) // Set up a DirectSound buffer { DSBUFFERDESC BufferDesc; if (!pChannel[Count]) ZeroMemory(&BufferDesc, sizeof(BufferDesc)); { BufferDesc.dwSize = sizeof(BufferDesc); // This channel isn't in use BufferDesc.dwFlags = DSBCAPS_STATIC | DSBCAPS_CTRLDEFAULT; ChannelToUse = Count; BufferDesc.dwBufferBytes = SampleSize; break; BufferDesc.lpwfxFormat = &WaveFormat; } // Create a new buffer using the settings for this wave else HRESULT DSReturn = { pDirectSound->CreateSoundBuffer(&BufferDesc, DWORD Status; &pChannel[ChannelToUse], 0); HRESULT DSResult = if (DSReturn == DS_OK && pChannel[ChannelToUse]) pChannel[Count]->GetStatus(&Status); { if (DSResult == DS_OK && // Lock the buffer and copy in the data !(Status & BYTE* pData; (DSBSTATUS_PLAYING | DSBSTATUS_LOOPING))) DWORD DataSize; { if (pChannel[ChannelToUse]->Lock(0, SampleSize, // This channel has finished playing - &pData, &DataSize, 0, 0, 0) == DS_OK) // it's OK to free it and use it now { pChannel[Count]->Release(); memcpy(pData, pSample, SampleSize); pChannel[Count] = 0; // Unlock the buffer ChannelToUse = Count; pChannel[ChannelToUse]->Unlock(pData, break; DataSize, 0, 0); } // Actually play it! } pChannel[ChannelToUse]->Play(0, 0, 0); } } if (ChannelToUse > -1) } { } // Found an unused channel... }

with the sample characteristics, points it The Well-Tempered PC trol and sets up DirectSound for exclusive at the specified wave data, and sends a The Sound Manager requires you to cre- audio access through that window before bufferCmd command to the appropriate ate channels only for the sounds you want creating the primary sound buffer. End- channel to start the wave playing. In the to mix, implying a specific playback buffer Sound reverses all that and frees the addi- listing, I’ve restricted PlayWave to 8-bit into which all channels are mixed. But tional buffers created in the process of 11,025Hz samples, but the code avail- DirectSound has no such default. A pri- playing waves. able on the Game Developer web site mary buffer represents the sound moving Once the system is set up, the Win- allows for others. through the hardware, and the application dows PlayWave implementation follows a Before leaving, PlayWave queues up must create a primary buffer before it can pattern similar to the one described for a callBackCmd command, which will play any sound through the hardware. Sound Manager. Specifically, it looks for result in a call to SoundCallBack when the The Windows BeginSound implemen- an unused channel among the four wave has finished playing. SoundCallBack tation handles the set-up of the primary allowed, creates and sets up a buffer for zeroes the sample pointer in the appro- buffer. Because a primary DirectSound the requested sample, and calls Play. priate ChannelHeader, making the channel buffer must be associated with a window, Listing 2 shows the source code for once again eligible to play a wave. BeginSound creates a simple static text con- this function. Notice that every call to

52 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com AUDIO

PlayWave creates a new DirectSound buffer to hold the wave you’re playing. Direct- Sound doesn’t play waves directly from memory like Sound Manager does, so PlayWave has to lock the buffer, copy in the wave data, and unlock it. That may be Method 1: Averaging (Macintosh Sound Manager) time-consuming and may even involve downloading a wave to the sound hard- ware. A better system could avoid that by keeping waves to be played in preallocated and precopied DirectSound buffers.

The Echo Chamber

I’ve included a demo that uses the stan- Method 2: Clipping (DirectSound) dard C file package to open a file called sample.wav, assumed to be in the .WAV Figure 2. Two mixing methods. file format used by Windows and read in sampled wave data. Then, it calls Play- Wave eight times with that data, leaving the run-time format. I’ve declared Swap16 made me decide to leave this demo in 8- 3/4 of a second between each call, waits and Swap32, which swap bytes into bit land. another five seconds, and terminates. Motorola format on a Mac and leave bytes I urge you to compile and run these Since .WAV is a PC format, con- intact on a PC. For a 16-bit sample, every on a Mac and on Windows 95 if you can. taining data in Intel byte ordering, the 16 bits of the wave data will have to be You’ll immediately hear the difference demo uses two functions to adjust them to swapped around as well, a factor that between averaged and clipped mixing. The

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 55 AUDIO

Listing 3. Code to Play a Wave Eight Times fread((char*)&BitsPerSample, 1, 2, pFile); // The simple Wave Mixing API BitsPerSample = Swap16(BitsPerSample); int BeginSound(void); // Skip whatever's left void EndSound(void); if (Size > 16) void PlayWave(int SampleSize, int SampleRate, fseek(pFile, Size - 16, SEEK_CUR); short BitsPerSample, short ChannelCount, } char unsigned* pSample); else if (Tag == 0x61746164) // The 'data' tag // Byte-swapping functions { short unsigned Swap16(short unsigned value); // Allocate space and read in the wave long unsigned Swap32(long unsigned value); pSample = (char unsigned*)malloc(Size); // The demo if (pSample) void DemoMain(void) { { SampleSize = Size; int SampleSize =0; fread((char*)pSample, 1, Size, pFile); int SampleRate =0; } short BitsPerSample =0; } short ChannelCount =0; else char unsigned* pSample =0; { // Load a wave file // An unknown tag - just skip it FILE* pFile = fopen("sample.wav", "rb"); fseek(pFile, Size, SEEK_CUR); if (pFile) } { } // We're just going to assume this file is valid fclose(pFile); // Skip the 'RIFF' tag and file size (8 bytes) } // Skip the 'WAVE' tag (4 bytes) // Now play the wave! fseek(pFile, 12, SEEK_SET); if (pSample && BeginSound()) // Now read RIFF tags until the end of file { unsigned long Tag; long unsigned Time; unsigned long Size; // (Attempt to) play 8 times while (!feof(pFile)) int PlayCount = 0; { while(PlayCount < 8) // Read, watching for file end { if (fread((char*)&Tag, 1, 4, pFile) == 0) PlayWave(SampleSize, SampleRate, BitsPerSample, break; ChannelCount, pSample); Tag = Swap32(Tag); ++PlayCount; fread((char*)&Size, 1, 4, pFile); // Wait 3/4 of a second between plays Size = Swap32(Size); Time = GetMillisecondTime(); if (Tag == 0x20746D66) // The 'fmt ' tag while (GetMillisecondTime() - Time < 750) { ; // 16-bit PCM flag - assume PCM format } fseek(pFile, 2, SEEK_CUR); // Wait 5 seconds then quit // 16-bit Channel Count Time = GetMillisecondTime(); fread((char*)&ChannelCount, 1, 2, pFile); while (GetMillisecondTime() - Time < 5000) ChannelCount = Swap16(ChannelCount); ; // 32-bit Sample Rate EndSound(); fread((char*)&SampleRate, 1, 4, pFile); } SampleRate = Swap32(SampleRate); // Clean up // Skip Average bytes per second - (4 bytes) if (pSample) // Skip padding - (2 bytes) free(pSample); fseek(pFile, 6, SEEK_CUR); } // 16-bit Bits Per Sample

Sound Manager makes the demo sound original volume by the Sound Manager ume on the channels as new waves are like an echo chamber, repeating the wave and clipped to distortion by DirectSound. mixed in. Or you can write your own over and over at progressively lower vol- There are ways to combat the Sound mixer that plays through a single Sound umes while the DirectSound demo pro- Manager’s dynamic volume adjustments. Manager channel, but I won’t be held vides eight crisp new shots. Try increasing You can guarantee that four sounds will responsible for the results! ■ the number of channels allowed to 8, swap always be playing by forcing the unused in a loud wave, and listen as all your channels to loop over a buffer full of Jon Blossom can be reached through sounds are reduced to one-eighth their zeroes. You can artificially turn up the vol- Game Developer magazine.

56 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com ARTIST‘S VIEW

Players Bored? Storyboard!

David Sieks ach of us interprets differently ryboarding is also useful in mapping out what we read, so that even the gameplay routines, such as character author cannot really know what movement cycles. The information cap- A low-tech tool can pictures his or her words might tured on the storyboard then serves as a paint in the reader’s mind. This visual shorthand for the artists who must is a wondrous, pseudo-magical bring the animation to completion. thing about the written word as Though indispensable as a tool for a means of creative expression. collaboration, the storyboard is of equal EIt’s also the reason a written script gener- importance to the lone artist. Its useful- help pack your high- ally proves insufficient as a tool for cine- ness is not just to share a visual concept matographers, animators, and game but to plan the whole sequence out ahead developers, who are working largely with of time. As I’ve noted before, it is tempt- graphical concepts. When the visual ele- ing for the artist to plunge into an anima- ment is this important to the end result, tion, but planning ahead is crucial to it is crucial that everyone involved has the achieve the best possible results. Don’t same picture in mind. In most cases, the assume that at some point later in pro- tech animations with script remains a necessity, but the very duction you’ll work out those issues left flexibility that leaves its phrases open to unresolved when you began. Animation personal interpretation makes text too is not an improvisational art. It’s much inexact for the planning and sharing of easier to make changes before you begin visual concepts. This is where the story- animating. The storyboard is the place board comes into play. for that to happen. visual power. Don‘t In case any reader is unfamiliar with the term, a storyboard is a graphical rein- Keep It Simple terpretation of the script, using a series of Storyboarding isn’t rocket science, but rough sketches to convey setting and some approaches are generally more use- action from scene to scene, sometimes ful than others and some fairly well- even from one movement to the next. established misconceptions need to be underestimate the Filmmakers and animators have used avoided. In this column, I am talking these visual devices for decades. As digi- about the storyboard as a rough tool for tal graphics grow in sophistication and planning and sharing visual information. importance, developers too rely increas- I’m not concerned with presentation- ingly on this tool to communicate and quality storyboards often used to pitch a plan a game’s visual element. project or sell an idea. When called for, power sketches can In general, the storyboard is a visu- such presentation storyboards are created alization aid. It helps establish the setting easily enough by making a prettified copy and the flow of action and pinpoints the of your working storyboard. It’s counter- positions of “actors” as well as the van- productive, for several reasons, to add tage point of the viewer. During game polish to your sketches prior to this. development, the storyboard is used to The first reason is that the story- plan in detail the cinematic sequences board should be considered a work in bring to your projects. used for game intros and cut scenes. Sto- progress, not a work of art. It holds every

58 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com Storyboarding has long been a staple of TV and movie production. Artists at Tom Snyder Productions have just three weeks to turn out each new half- hour episode of the award-winning Comedy Channel animated sit-com “Dr. Katz: Professional Therapist.” Animator Mark Usher observes that the show’s stock characters, sets, and a signature, minimalist animation style they call Squigglevision mean that storyboards can be quickly created by sketching in changes over preexisting frames from earlier episodes. The storyboard also guides the editor in piecing together the work of several artists for the final edit. aspect of a sequence up for scrutiny before Realm of Creative Endeavor. Watch tures what is dynamic in the scene: spend investing time and effort in animation. It where you step: there are unexploded egos minimal time and effort on things that is successful when it elicits change: all around. remain static. changes represent a problem or weakness The final reason to build the story- In keeping with the quick and dis- discovered and fixed at an early stage or a board from quick sketches is something posable nature of storyboard sketches, good idea replaced with an even better else I’ve talked about in this space before: artists probably do not want to use those one. Since, due to these changes, many flow. Anything that slows down the storyboard layout sheets with columns of sketches may need to be scrapped and process of translating written script to neat preprinted rectangles on each page. reworked, it’s best not to have invested visual information threatens to stifle the Avoid these because it makes changes unnecessary time and effort into making creative flow. When storyboarding for more difficult when, for example, there are them look pretty only to discard them animation, you are planning something eight sketches on a single piece of paper later. that ultimately will move and have a pal- and you need to replace two of them. It Which leads to the second reason to pable pace to it. You need to convey that also makes it near impossible for the artist keep detail to a minimum in storyboard dynamism even in the static panels of the to remain unconcerned about the “quality” sketches: ego. A storyboard that survives storyboard. If you get bogged down in the of the image while drawing, especially review without changes probably wasn’t details of a single drawing, you lose that once there are already a couple of accept- looked at critically enough. Suggested momentum: the end result is likely to be able sketches on the page and any slip-up changes are not a personal indictment of disjointed and unsatisfactory. Keep it sim- threatens the work already done. the artist’s talent. This is hard for artists to ple and energetic. Instead, use single sheets of small accept, though, if they have prepared a Another worthwhile observation paper—numbering them, if necessary, to storyboard filled with painstaking draw- about detail, or lack thereof—it is a good keep them in sequence. Group the sheets ings. Better to think of the storyboard as practice to excise repetitive information on a large board afterwards or as you visual shorthand and keep detail to a min- from storyboard sketches. To establish work, securing them with tacks or reposi- imum. A lovingly detailed storyboard setting, you will want at some point to tionable adhesive. With single sheets, it is sketch is akin to a beautifully carved orna- indicate the background or portray the easy for the artist to discard a drawing that mental wooden matchstick: it’s so pretty general color scheme. But it is unneces- isn’t coming out right or to implement a you can’t bear to use it as intended. sary to duplicate this information in different approach to the scene. Also, on a An aside to those working with a sketch after sketch if it has not changed storyboard made up of single sheets it is a storyboard artist: you have entered the from one to the next. Storyboarding cap- simple matter to remove sketches during a

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 59 ARTIST’S VIEW

Terra Nova: Strike Force Centauri, the new title from Looking Glass Technologies, features numerous cut scenes blending live action video with computer graphics sets and actors. By planning these scenes on the storyboard, artists were able to focus design and modeling efforts where they were needed and not waste time on areas that would go unseen. The storyboard also proved useful on the set for staging actors around virtual props that had not yet been created. Pictures courtesy Looking Glass Technologies Inc., Cambridge, Mass. Terra Nova, Looking Glass, and the dis- tinctive logos are trademark of Looking Glass Technologies. meeting to mark areas that need further However, you need not dwell on the ever, don’t let the flow from sketch to attention. details of characters, which should already sketch slow down excessively by indicat- The storyboard needs to consist of a be worked out on model sheets (for more ing actions in great detail. More than one series of sketches that establish setting on model sheets, see “A Question of sketch can certainly be used to express a and action and guide the creative team Character” in the Feb./Mar. issue). complex action, but not too many more. through the task of animating the Of course, it is important to register For the purposes of the sequence story- sequence. Each change of viewpoint must actions as well: movement within the board, your aim is to capture the extremes be shown; each new character that scene is one chief reason we require a sto- of the movement and leave it at that. A appears onscreen must be indicated. ryboard rather than a single sketch. How- separate action storyboard can show in

60 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com ARTIST‘S VIEW

detail how a character walks or hops or giant robots, or from down at ground the sequence. Always be wary of the easy falls down. level to emphasize their great size, or way out: avoid the obvious, hold cliché at Explanatory text will sometimes be distorted by a wide-angle lens, or with a bay. Whether the aim of the sequence is unavoidable on the storyboard, but keep series of quick takes from all these angles to amuse, frighten, or thrill, look for it to a minimum. There will be a written and more. opportunities to show the audience script of some sort to start with. The How you tell the story affects how something different and unexpected. storyboard is a complement to this, not a the audience perceives and responds to The storyboard phase is the opportunity replacement for it; nor should a story- it. What is shown, what is only implied, to consider all these things, to try out board repeat most of the script. The what angles you use, what actions or different ideas, and to settle on an action depicted in the storyboard should moments are emphasized—these deci- approach before getting down to the be self-explanatory. If not, it’s probably sions contribute to the overall impact of work of animating. worth reconsidering the depiction you have chosen. Text that indicates the accompanying dialogue or narration, however, can help communicate the sequence’s pace and is routinely included in a separate block below each sketch.

Consider This Even more important than the form the storyboard takes or the appearance of its individual sketches are the considera- tions that go into taking a written script and turning it into an animation. The storyboard will act as the animators’ map on this journey. The challenge is not just to convey the plot and action described in the script, but to settle upon the very best way to tell that story visually. In laying out the storyboard, one is dealing with the foundations of good visual storytelling. Great modeling, ren- dering, and fluid animated movement can seem strangely hollow if the story is not told with verve and style. The groundwork for these elements is your storyboard layout. Your task may be to simply make a splash sequence for a fighting robot game: the script calls for the robots to stomp around, fly through the air, and shoot rockets at each other, and then the title pops up. Simple enough, on the surface. But how can you make that compelling? You want to transport your audi- ence, cast a spell over them, and even for a few moments take them somewhere beyond their computer screen—make them believe and care about what they see on that screen. You don’t have to add to the plot or action beyond what is pre- sented in the script, but you should chal- lenge yourself to make the most of the material. Maybe you show the action from inside the cockpit of one of the

http://www.gdmag.com GAME DEVELOPER • JUNE/JULY 1996 61 ARTIST’S VIEW

When the storyboard is laid out in thing interesting about every shot in the need to be created, in what degree of front of you, take a critical look at every- sequence? If not, you either eliminate detail? What texture maps, back- thing that makes up the sequence: justify the shot, or you find a way to give it grounds, and special effects are called the presence of each sketch. Your more punch. Cut scenes within games for? The answers depend on how the sequence will be composed of a number are by necessity too short to support any sequence has been laid out in the story- of “shots.” Even if it all takes place in dead weight. board. Practical limitations of budget one room, it is shown first from one With all that in mind, it is often and deadline may mean that the best angle, then another; this action occurs, important to plan an animation with an way to tell the story from a creative then that. Each is a shot, and your story- eye toward economy. How long will it standpoint just isn’t realistic from a board must establish every shot. Do they actually take to animate the sequence as production standpoint. all really need to be there? Is there some- storyboarded? What 3D models will The storyboard provides you with an opportunity to realize limitations ahead of time and plan around them. For example, one might frame shots so that it’s only necessary to model the head and shoulders of a character rather than the entire body. Position certain figures at a distance, or show them in stark sil- houette so that less-detailed models can be used. Or, as I suggested in my article on lighting in the previous issue, use a cast shadow gobo to represent a figure and thereby avoid actually modeling it at all. A carefully considered storyboard helps you balance at the earliest possible stage the twin demands of what-looks- best and what-time-allows. Even if there won’t be any fancy animated cut scenes in your game, the storyboard is a valuable tool for mapping character movement routines or action sequences. You’ll find that most consid- erations outlined above will still apply. Make the movement dynamic for the time being, keep detail to a minimum, and above all don’t be boring. Even if you’re just animating a running sprite for a side-scrolling game, use storyboarding to lay out different approaches and find a way to make that run look interesting. Storyboarding will probably be the least exotic tool you use in creating digi- tal animation, but it will also prove one of the most valuable. I doubt any artist who has learned to appreciate the oppor- tunities it provides for planning anima- tions, for optimizing animations, and for sharing visual concepts with a team would trade storyboarding for all the special effects plug-ins ever made. ■

Dave Sieks is a contributing editor to Game Developer. You can contact him at [email protected].

62 GAME DEVELOPER • JUNE/JULY 1996 http://www.gdmag.com