Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
What's on the CD-ROM BIOGRAPHIES CHAPTER 1—INTERNET GAMING: THE FUNNEST FRONTIER GETTING THE WORD OUT PLAYING BY EMAIL ALL I NEED IS A GOOD CLIENT IRC-BASED GAMES MUDS, MUSHES, MOOs, AND ALL THE OTHER M WORDS DEDICATED CLIENT GAMES KALI, HTHP, AND THE GANG IT’S A WEBOLUTION: THE EMERGENCE OF THE WORLD WIDE WEB PLAYING THE WEB NETSCAPE AND PLUG-INS JAVA
CHAPTER 2—OUR FIRST GAME DONUTS DESIGNING DONUTS A FIRST CUT PICTURES OF ACTORS MAKING THE DONUTS JOINING TOGETHER GIF FILES THE SPACESHIP THE JELLY-SQUIRTS EXPLOSIONS SPLITTING DONUTS ADDING MORE EXCITING GAME PLAY ADDING SOUND SUMMARY
CHAPTER 3—A GAME FRAMEWORK WHAT IS A FRAMEWORK? THE GAMEWORKS FRAMEWORK FEATURES OF GAMEWORKS ACTORS ON A STAGE COMMUNICATING THE DESIGN THE CORE FRAMEWORK THE STAGEMANAGER THE ACTORS THE ACTORMANAGER THE GAME EXTERNAL CONNECTIONS ROUNDUP TECHNIQUES USED IN THE FRAMEWORK MANAGERS AND HANDLERS OBSERVERS SUMMARY
CHAPTER 4—PAINTING ACTORS AND THE STAGE PAINTING ACTORS AND THE STAGE GRAPHICS DRAWING IMAGE DRAWING USING IMAGES NON-RECTANGULAR IMAGES GAMEWORKS IMAGE SUPPORT IMAGE HANDLERS SINGLEIMAGEHANDLER MULTIPLEIMAGEHANDLER IMAGE SHARING LOADING IMAGES DIGGING DEEPER READY FOR SOME MORE? THE IMAGEMANAGER PRELOADING IMAGES CREATING A TILED IMAGE SPECIAL EFFECTS IMAGE FILTERS FADING OTHER EFFECTS THE BACKDROPMANAGER SUMMARY
CHAPTER 5—INPUT HANDLING COMPONENT EVENT HANDLING THE HANDLEEVENT METHOD EVENT OBJECTS TYPES OF EVENTS HOW A COMPONENT RECEIVES AN EVENT EVENT THREADING GAME FRAMEWORK EVENT HANDLING AN EVENTMANAGER KEYBOARD EVENTS DECODING KEY VALUES MOUSE EVENTS DETECTING A SINGLE MOUSE CLICK DETECTING A MOUSE DOUBLE CLICK SUMMARY
CHAPTER 6—MOVEMENT MOVING AN ACTOR DRAG-AND-DROP DRAG-AND-DROP MANAGER DETECTING INPUT STARTING THE DRAG THE MOVEMENT BASICS INTRODUCING DROP SITES DROPPING THE ACTOR IMPLEMENTING THE DROPSITE INTERFACE SMOOTH MOVEMENT DOUBLE-BUFFERING SPEEDING UP THE REDRAW FIXED-ACTOR CACHING MORE OPTIMIZATION
LAYERING PUTTING IT ALL TOGETHER SUMMARY
CHAPTER 7—CLOCKS INTRODUCING THE CLOCK IMPLEMENTING THE CLOCK CLASS HOW MANY CLOCKS? DEFAULT GAME CLOCK CHANGING THE CLOCK SPEED MORE ON CLOCK WATCHERS A TIME ELAPSED LABEL A COUNTDOWN TIMER FANCY DRAG-AND-DROP RETURNS SUMMARY
CHAPTER 8—WORKING WITH SPRITES MOVEMENT A CALCULATED MOVE IMPLEMENTING MOVEMENT BOUNDARY CONDITIONS COLLISION DETECTION IMPLEMENTING BOUNDING-BOX COLLISION DETECTION ACTORMANAGER UPGRADES MOVEMENT PERFORMANCE REVISITED TALK DIRTY TO ME IMPLEMENTING DIRTY RECTANGLES USING DIRTY RECTANGLES SUMMARY
CHAPTER 9—HIGH SCORING A SCORE MANAGER IMPLEMENTING THE SCORE MANAGER A SIMPLE SCORE LABEL A WEB HIGH SCORE SERVER WHAT WILL THE SERVER DO? INDEPENDENT OF A GAME SECURE SCORES USEFUL USER REPORTS DESIGNING THE HIGH SCORE SERVER SERVER CONFIGURATION COMMANDS THE HIGH SCORE DATABASE HIGH SCORE TABLE SECURITY IMPLEMENTING THE HIGH SCORE SERVER THE GENERIC SERVER PART HIGH SCORE PROCESSING IMPLEMENTING THE CLIENT OTHER USEFUL FEATURES WEB USER INTERFACE USER-DEFINABLE HIGH SCORE CALCULATION AUTOMATIC EMAIL SUMMARY
CHAPTER 10—THE HILLS ARE ALIVE: SOUNDS IN JAVA GAMES PLAYING SOUNDS IN JAVA
LOOPING, LOOPING,… PERFORMANCE CONSIDERATIONS: MULTITHREADING SOUNDS SUMMARY
CHAPTER 11—Seven Come Eleven RANDOM EVENTS A DICE CLASS ROLLING THE DICE INFORMING OBSERVERS SHOWING THE TOSS PUTTING IT ALL TOGETHER THE DICE CLASS IN ACTION DICEOBSERVERS MAKING IT RUN EXTENDING THE DICE CLASS BEFORE WE SHUFFLE OFF ONE MORE THING SUMMARY
CHAPTER 12—Going It Alone: Java Solitaire CARD STACKS ALTERNATECOLORDESCENDINGCARDSTACK SAMESUITASCENDINGCARDSTACK STOCKPILECARDSTACK FACEUPSTOCKPILECARDSTACK SOLITAIREGAME MAKING IT MOVE ACDDRAGDROPMANAGER AFTER THE MOVE SUMMARY
CHAPTER 13—MOVIN’ AND THINKIN’: GIVING YOUR GAMES SOME SMARTS THE ENEMIES OF PIXEL PETE MAKING PURSUERS MORE INTERESTING REAL ARTIFICIAL INTELLIGENCE MINMAX IN THE GAME FRAMEWORK THE GAMEMOVEMANAGER TICTACTOEGAMEMOVEMANAGER SUMMARY
CHAPTER 14—AUTOMATED PLAYERS AND WEAPONS MAZEWARS THE MAZE WORLD SO MANY ACTORS, JUST ONE APPLICATION BRINGING IT ALL TO LIFE THE ULTIMATE OPPONENT YOU MAY BE FAST, BUT NOT THAT FAST WEAPONS HOW DO THE WEAPONS WORK? THE WEAPONGAMEBRIDGE CLASS INTERFACING WITH WEAPONS SUMMARY
CHAPTER 15—NETWORK GAME PROGRAMMING APPLICATION CONNECTION TOPOLOGY THE STAR TOPOLOGY
THE INTERCONNECT TOPOLOGY INPUT/OUTPUT MODEL INTEGRATING NETWORK INPUT INTO AWT INTEGRATING NETWORK INPUT WITHOUT EXTENDING AWT NETWORK OUTPUT MODEL NETWORK DATA TRANSFER MODEL NETWORK ERROR MANAGEMENT GAME STATE MANAGEMENT WHO IS MANAGING YOUR GAME STATE? TIMING ISSUES FOR PLAYER FEEDBACK THE CLIENT GAME STATE VIEW HUMAN FACTORS INCREASE THE INTERACTION BETWEEN PLAYERS BEWARE THE ETERNAL DOWNLOAD START THE GAME ON TIME KEEP THE USER INTERFACE RESPONSIVE PLAYERS LEAVE GAMES MAYBE THE MEEK WON’T INHERIT THE EARTH SUMMARY
CHAPTER 16—DOMINATION INTRODUCING...DOMINATION THE DOMINATION ENVIRONMENT IMPLEMENTING THE DOMINATION GAME THE GAMEEVENT CLASS THE DOMINATIONEVENT CLASS THE GAMELAYER CLASS CODING GAME LOGIC WITHIN A GAMELAYER CONNECTING TO THE NETWORK THE DOMINATION APPLET THE DOMINATIONSERVER APPLICATION DOMINATION AT WORK SUMMARY
CHAPTER 17—EXTENDING DOMINATION EXTERNAL GAME DESIGN INTERNAL GAME DESIGN THE UNIVERSAL BUSY SIGNAL GAME WATCHING INTER-PLAYER CHAT DECOUPLING THE USER INTERFACE AND NETWORK DEALING WITH NETWORK ERRORS SENDING A REGISTERED GAMEEVENT RECEIVING A REGISTERED GAMEEVENT AND DETECTING ERRORS MANAGING THE REGISTERED GAMEEVENT CACHE SUMMARY
CHAPTER 18—NETWORK MAZEWARS MAZEWARS NETWORK ARCHITECTURE CONNECTING TO A MAZEWARS GAME THE MAZEWARSSERVER APPLICATION LIFE BEFORE THE WELCOME DIALOG BOX CREATING AND JOINING GAMES A BRIEF NETWORK REVIEW SENDING THE ACTOR DATA INTEGRATING EXTERNAL GAME INFORMATION
KILLING IN A NETWORKED MAZE IS IT LIVE OR IS IT A REMOTEACTOR? SUMMARY
CHAPTER 19—NETWORK MAZEWARS REFINED LIFE IN THE NETWORK LANE SMOOTHING THE FLOW LOCALLY ANIMATING REMOTE ACTORS REMOVING THE BROADCAST DELAY INTER-SESSION LATENCY LIVING WITH LATENCY USING LATENCY DATA SUMMARY
CHAPTER 20—SQUEEZING THE LAST DROP: JAVA OPTIMIZATION SHOULD I OR SHOULDN’T I? DELIVERY NETSCAPE MICROSOFT OTHER VENDORS CODING FOR PERFORMANCE MORE TECHNIQUES AND A WARNING OPTIMIZING FOR SIZE OPTIMIZING FOR SPEED HOW ARE YOU DOING? SUMMARY
CHAPTER 21—FRED GAMEPLAY THE ARCHITECTURE THE FRED SERVER THE PLAYER THE RENDERING ENGINE HOW DOES RAYCASTING WORK? IMPLEMENTING THE ENGINE CASTING A RAY HOW TO DRAW A STATIC OR MOVING OBJECT GRAPHICS OPTIMIZATIONS WHEN TEXTURE MAPPING ISN’T AN OPTION OPTIMIZING FRED’S NEXT VERSION CONSISTENCY AND LATENCY TO SYNCHRONIZE OR NOT TO SYNCHRONIZE REACHING A COMPROMISE FURTHER OBSERVATIONS DYNAMIC LOADING IN JAVA SECURITY ISSUES SUMMARY AND ACKNOWLEDGMENTS
Appendix A Appendix B Index
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Table of Contents
What’s on the CD-ROM The companion CD-ROM includes complete source code for the GameWorks game programming framework and the GLE network gaming toolkit, as well as files for the coding examples in the book, a load of ready-to-play Java games, and shareware tools. • The GameWorks game programming framework gives you ready to use classes for double buffered animation, actor management, collision detection, scorekeeping, drag-and-drop, sound effects, playing cards, dice, Min-Max, and more. • The GLE (GameLayer/GameEvent) toolkit gives you a ready-to-use consistent architecture for Applets and servers. • Original Java games, including Domination, a game of world conquest; MazeWars, a game of survival; and Donuts, a game of delicious but deadly invaders. • The coolest Java games from around the Net, including Fred, a 3D thriller; Europa, a game of strategy and power in space; and Iceblox, the penguin maze game. See the readme files in each folder for acknowledgments, descriptions, copyrights, installation instructions, limitations, and other important information. Requirements Software: The Java Developer’s Kit available at: http://www.javasoft.com Hardware: The Java material can be run on any platform that supports the JDK. The shareware tools are platform specific to Microsoft Windows 3.1 or Microsoft Windows 95.
Table of Contents
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Table of Contents
To Mary, with Love. Neil Bartlett As a tree of life among the thorns, so is my beloved. Steve Simkin To Heather, my partner and friend. Thanks for living with, and without, me through all the long days. Chris Stranc
BIOGRAPHIES Neil Bartlett Neil Bartlett is co-author of the Java Programming EXplorer. He is a judge with JARS (the Java Application Rating Service) and a member of the Toronto Java Users Group. He runs Great Explorer Software Consulting Ltd. His email address is neilb@the-wire. com.
Steve Simkin Steve Simkin runs a small software consulting firm, specializing in object oriented financial and manufacturing applications. He is a member of the Toronto Java Users Group.
Chris Stranc Chris Stranc is a software engineer at Nortel. He focuses on the design to manufacturing process, with a special gift for graphical interfaces to PCB designs. He is a member of the Toronto Java Users Group.
Acknowledgments Neil Bartlett Thanks to my wife, Mary. She alone knows the sacrifices of writing a book. Thanks to Denise Constantine, our project editor, and Jenni Aloi, our copy editor. Never was “can you have that done within the hour?” or “that chapter needs re-writing” said with such friendly tones. You were both superb. The true professional face of Coriolis. Thanks to Doug Ierardi for letting us into the secrets of Fred. Doug, I appreciate the time and effort you put in to coordinating and to writing your chapter. Thanks also to the rest of the guys: Cavit Aydin, Steve Deng, Craig Kawahara, Susanto Kolim, Ta-Wei Li, Ferry Permadi. Fred is such an ambitious project. It takes a great team to pull it off. Thanks to my brother, Scott, for the ‘jelly squirt’ idea. It rounded out the Donuts game very nicely. A big thank you to
[email protected] for the card GIFs. Thanks to Alex Leslie and Gus Bazos for early work on network programming.
Thanks to the people whose ideas have greatly influenced my framework design: Mark Tacchi for the excellent Gamelet framework; John Vlissides, Richard Helm, Eric Gamma, Ralph Johnston and James O. Coplien for OOP Patterns; Andre LaMothe (I love your game books); and James Tauber and the rest for advanced-java. Thanks to Steve and Chris for being solid. Steve, it’s always a delight to work with you. You always produce—no worries. Chris, you did an incredible job in a very short time. It was a joy working with you. What do you think of book-writing now?
Steve Simkin This one is for the mothers. During the writing of this book, my wife and I both started new jobs and I launched a consulting firm. This on top of the usual busy-ness of September, what with school buses, hockey tryouts, and the autumn marathon of Jewish holidays. This book would never have been written on time without the help of our two mothers, Lynn and Geri, who babysat, cooked, and cut intricate laminated classroom decorations into the wee hours. Thanks! It’s been a pleasure working with Denise and Jenni. Denise, thanks for reminding me of those delivery dates I rashly committed to (“So what if I came up with that date myself? Didn’t I tell you not to trust my estimates?”). Jenni, no one has ever pointed out quite so graciously that my train of thought never left the station. Neil and Chris, as always, it’s been a pleasure. But the next time we work together, let’s try for business hours. Five a.m. is wearing a little thin.
Chris Stranc What a time to run out of words. I would like to thank my family for being so patient with me. To Samantha, please, don’t banish me down to the study again. My work is done. To Colin, no more will you wake me with a gentle thwack of your bottle. I plan to be a lot more alert at 6:00 a.m. Finally, to Heather. My heartfelt thanks for your endurance through the summer that was not. It’s been an education and a pleasure working with the folks at Coriolis. Denise, is it true all your authors work ‘till 3:00 a.m., or was that a scare tactic? Jenni, thanks for your patience and your humor. Neil and Steve, what an experience. Thanks for inviting me to join in the fun. To think, I was looking to add a little excitement into my life. What a surprise I had in store. Finally, to my friends at the CAMEng department. Thanks for your patience, as I yawned though our staff meetings, slept though our lunches, and occasionally even missed running in the morning.
Table of Contents
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 1 INTERNET GAMING: THE FUNNEST FRONTIER STEVE SIMKIN
T
he competitive urge has been with us since the dawn of human consciousness. No sooner had humans become thinking beings
than Eve placed a bet with a snake regarding the consequences of eating the fruit of the Tree of Knowledge. That first game was played for particularly high stakes, and despite its outcome, we’ve been gaming ever since. We just can’t turn down a chance to test ourselves against each other, or ourselves, or even a machine. Luckily, since Eve lost her wager, we’ve learned to lower the stakes of gaming. We substitute chess boards and card tables for battle fields and gladiatorial arenas. These new arenas become worlds in their own right, allowing us to insulate our “real” lives from the fallout of lost games. Our self-esteem, and maybe our wallets, may take a hit when we get trounced, but for the most part, the damage is sustainable and only serves to motivate us to do better the next time. Humankind’s ability to enclose gaming in a safe realm of existence demonstrates two traits that coexist improbably in our species. On the one hand, the knowledge that being checkmated will never actually hurt us allows us to have fun while playing chess. On the other hand, in the course of play we forget our actual safety sufficiently that our most primitive fighting instincts are activated. We savor each capture of our opponent’s pieces and wince at each loss, as if our survival were really at stake. At least, I do. The gaming arena becomes very, very real. This human ability to compartmentalize experience, to allow a game to come alive safely within an insulated area of our consciousness, is made possible by two human attributes. The first is our ability to abstract. The connection between my Risk set’s “Armies” and the real thing is tenuous, at best. But something in our brains enables us to endow pieces of plastic, wood, or ivory with identities, ascribing to them capabilities and limitations that determine their behavior. The second attribute that contributes to gaming is our drive to progress technologically. As our capabilities in the fields of manufacturing, communications, and transportation have grown, so have our opportunities to create more sophisticated games, to distribute them, standardize them, and discuss them. We can compete against increasingly far-flung opponents. Finally, as the world becomes more complex and real conflicts threaten devastation on a scale previously unimaginable, games that safely imitate the world have followed suit. If a chessboard can become an intensely competitive, living world, what can one say about Duke Nukem? Of course, much of the quantum leap in the intensity of the gaming experience is made possible by computers. Even the creakiest, most ancient mainframe could grind out calculations fast enough to allow simulations never attempted before. As graphical interfaces replaced command lines in home computers, computerized games crossed another reality barrier. The gaming experience became ever more absorbing. Yet another barrier was crossed as computers were connected in networks. Suddenly, two human opponents could battle it out from different locations, probing each other’s strengths and weaknesses. In this physically safe, yet realer-than-ever world, competitive juices started to flow as never before. The next step forward came as Internet connections became more widely available. For gamers, the Internet may represent the most important advance yet. The Net gives us meeting places, gameboards, automated arbitration, and a limitless forum in which to issue challenges, argue strategy, publish rules, and comment on each other’s play. But for the true gamer, ever in search of the next conquest, the greatest attraction of the Net must surely be its endless supply of prospective opponents. As an increasing percentage of the world’s population gets wired, even the most fanatic Connect4 hustler can never hope to keep up with the number of new players
ready to take him on. And if he does somehow tire of mere human competition, an army of well-programmed computers stands ready, day or night, to humiliate him on any number of electronic battlefields. One of these machines is the reigning world champion in checkers! But has success made him haughty, too proud to waste his time trouncing mere mortals? Not on your life! If you like showing off your scars, and want to brag to your grandkids that you went a few rounds with a world champion, go ahead and call on Chinook at the University of Alberta (see the Artificial Intelligence sidebar for more information on Chinook). Where, but on the Net, can you find world champions on demand? The development of Internet gaming is certainly a subject worthy of discussion. By analyzing how gamers are using the Net to enhance their play, we can learn how technological innovation has repeatedly addressed the very human needs of contestants around the world. We’ll see that each technical advance has solved a particular problem, but that no single set of solutions has given players everything they need in a gaming environment. The rest of this book suggests how Java can be used to create a complete gaming environment for networked opponents. The survey that follows is not a comprehensive examination of Internet gaming. That would require a doctoral thesis. The Internet teems with gaming activity of all kinds. In fact, one article I read while preparing this chapter claims that 10 percent of all Net traffic is generated by MUDs (Multi User Dungeons) alone! No source for the statistic was given, but even if it’s an exaggeration, it suggests that lots of people are having lots of fun in Cyberspace! If you’re interested in getting a piece of this action, take a look at my resource listing in Appendix B at the end of this book. There, you’ll find a list of enough resources to keep you playing happily for years. Before we move on, I must make one minor disclaimer: In writing this chapter, I assumed that anyone reading this book is already equipped with a World Wide Web browser. Consequently, even when describing pre-Web gaming technology, I tell you how to access it on the Web whenever possible. Keep life simple.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
GETTING THE WORD OUT In any sphere of knowledge, the simplest use of the Internet is to disseminate information. Gaming is no exception. People frequently publish rules and lists of FAQs (Frequently Asked Questions). For example, go to ftp://ftp.halcyon.com/pub/go to download the rules for Go, in ASCII or Postscript format. Or try http://doomgate.gamers.org/docs/FAQ/doomfaq to learn everything you could possibly imagine knowing about Doom. These efforts help to standardize knowledge within a gaming community. Standardization is especially important now that global play is possible. In the pre-Net period, regional variations were natural and actually helped to promote a game’s popularity. In their travels, traders and soldiers may have encountered these local flavors, but it was always clear that in Burgundy, even foreigners played Burgundian hopscotch. But now that a Zambian and a Norwegian may be playing Abalone through the good offices of a server in Cleveland, we can’t rely on the players themselves to agree about how to play. Nothing kills the fun of a good contest faster than bickering over rules, and an up-front agreement to abide by the formulation of an online FAQ can prevent a bitter, premature termination later. In addition to standardizing popular games, FAQs can help to promote less well-known ones. For example, the authors of a site called Rules for Medieval and Renaissance Games are trying to revive games that have fallen into undeserved obscurity. For example, Lady Gunnora Hallakarva, an eighth-century Finn who somehow manages to maintain a Web site, describes a board game called hnefa-tafl (“King’s Table”). Hnefa-tafl re-creates a famous battle between the Muscovites and the Swedes. Only the Swedes have a king. The Swedish king starts the game at the center of a 15-by-15 ruled board, and the Swedes win the game if their valiant king manages to escape to the periphery of the board before being surrounded on all sides by Muscovites. Any takers? Usenet newsgroups provide another forum for straightforward exchange of ideas about games. They allow devotees to ask questions, trade opinions, recommend gaming associations, or engage in any other form of discussion that interests them. Most of the gamerelated newsgroups are in the rec.games hierarchy. Recently, I dipped into some of these groups, choosing postings at random. Among the topics discussed, I found a passionate argument on the relative merits of the PSX and Jaguar video game machines; an extended discussion of algorithms for shuffling cards; an esoteric examination of spell-casting in Dungeons and Dragons; and profound disagreement about the effects of virus grenades on harlequins in a miniatures game called Warhammer. If you enjoy shooting the breeze about these or similar subjects, open up your news reader and start posting. Figure 1.1 shows a typical posting to a game-related newsgroup.
Figure 1.1 Angst in the dungeon. Electronic mail is a great way of keeping groups of people up-to-date on events of shared interest. For example, Jim Burgess (
[email protected]) publishes an email magazine called The Abbysinian Prince, which documents Diplomacy games in progress for the entertainment and analysis of the huge Diplomacy community worldwide. So far, we’ve seen how the Internet is used to communicate about games. What about using the Net to actually play games?
PLAYING BY EMAIL
I’ve never enjoyed computerized poker. No matter how clever the algorithm and the graphics, electronic versions of the game always seem pale. Bluffing seems a particularly human activity, and no machine simulation can replace a skilled poker face. No doubt about it, human interaction is necessary to satisfying poker. There are other games, however, which are so inherently intriguing that direct contact between the opponents can be safely removed without critically damaging the game. If the game consists of a series of static states that can be captured and expressed by an agreed notation, the players can be physically separated and still have a good time. The only thing missing is a mechanism for players to communicate their moves to each other. Chess is a perfect candidate for this kind of long-distance play. Even when played FTF (Face-To-Face), the game consists of long periods of purely mental action interrupted by brief, subtle physical actions. These actions are easily and clearly captured by a longestablished notation. Best of all, the static states between each move are themselves intriguing, independent of the human beings who happen to be playing the game. Long before the use of computers became widespread, there was a well-developed culture of playing chess by mail. The use of surface mail to play chess and other games is known as PBM (Play-By-Mail). When electronic mail (email) became widely used in the academic and military communities, it seemed natural to transfer PBM to the new medium. PBM quickly became PBEM, and a new form of long-distance play was born. PBEM has a number of advantages over its predecessor. The most important one is speed. If they want to, opponents can play PBEM chess almost as quickly as the FTF version. In addition, computerized message trading allows for more convenient archiving of a player’s history. No more shoe boxes full of postcards! If playing chess by email appeals to you, you can be matched with an opponent at http://www.pi.net/game/internet/ chessonthenet, the automated chess matchmaking service shown in Figure 1.2.
Figure 1.2 Your checkmate is waiting for you at Chess on the Net. While PBEM chess works smoothly, PBEM presents problems for other kinds of games. For example, how to play games that use dice? Or military simulation games that depend on simultaneous moves by more than one player? Unless there is complete trust among all participants, these situations stretched the ingenuity of regular PBM fans. I’ve even heard of agreements to base dice rolls on the last two digits of the closing Dow Jones Industrial Average. As for games involving simultaneous moves, the solution was usually to nominate a Game Master (GM), who may or may not be one of the players. Players would mail their moves to the GM, who would release the results once he received every move. If the GM was one of the players, there was still a trust problem; if he wasn’t, there was a greater chance that he would lose interest and resign in the middle of the game, bringing it to a crashing halt.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
With the move to electronic mail, simpler solutions to these problems became possible. For example, computer programs could substitute for dice. These programs run on machines called “dice servers.” Just mail the dice server a message describing the number of dice used by your game, the number of sides on each die, and the email addresses of everyone who should be informed of the results of the “toss.” The server uses this information to generate random numbers appropriate to your game, and sends an identical email message containing these numbers to all the listed recipients. You can use one dice server by sending requests for dice rolls to
[email protected]. The body of the email message should be formatted as follows: #P #P #S #D #R #L #C #C #T
<email addresses of recipients of results of roll> <subject line for message containing result of roll>
For a Web-based front end to an email-based dice server, take a look at http://www.irony.com/mailroll.html. This site provides a convenient form for entering the data required by the server. As for the Game Master, he can be dispensed with thanks to encryption software. Encryption programs allow players to send their moves to each other in a coded form. When all players have received each other’s moves, they send out a second message containing a key that can be used to decode the message describing their move. By removing the dependence on a human GM, encryption software actually improves the game, making it smoother and less vulnerable to the whims of a non-contestant. The most popular encryption software is called PGP (for Pretty Good Privacy). It is simple to use and widely available on the Net. For the latest version of PGP, go to http://www.netlink.co.uk/users/hassop/pgp.html. Entire categories of games previously inaccessible to PBM gamers are now available, thanks to PBEM. For example, computerized dealers manage Internet versions of collectible card games, such as Middle Earth, based on Tolkien’s characters from The Hobbit and The Lord of the Rings trilogy. You can see one in action at http://www.ftech.net/~pbmweb/allsorts/me. Note that this is a commercial game. If you want to play, you’ll have to pay. Of course, before you can use email to battle your distant opponent, you need to find her. After all, how can you possibly know that a woman in Nepal is as anxious for a game of Firetop Mountain as you are? Here again, email combined with a little software can help. Try subscribing to PBMserv, the Net’s premier Play-By-Email opponent brokerage service. You can subscribe by sending a message to
[email protected], using the signup command. For instructions on signing up, go to http://eiss.erols.com:80/ ~pbmserv. There, you will also find a list of games supported by the server. Once you’ve signed up, you will quickly find yourself bombarded with challenges to play more games than you ever knew existed, all facilitated by the kindly machines at PBMServ. If you don’t get the invitation you’re looking for, issue your own challenge by sending a message to
[email protected], using the broadcast command. PBMServ subscribers get tens of messages a day, like the ones shown in Figure 1.3.
Figure 1.3 Typical PBMServ broadcast messages. Can’t find a human being to take you on? Not to worry. For some games, PBMServ can refer you to a computerized opponent. Personally, I don’t get much satisfaction from playing against an automated opponent. When I lose (which is most of the time) I feel doubly humiliated at being beaten by a mere machine; and when I win, there’s no one to gloat to. But computers do have the advantage of availability, and they tend to be gracious in victory, which is more than I can say for myself. Paul Colley’s Abalone server, at http://www.qucis.queensu.ca/home/colley/ai-aba-faq.html, is ready to accept your challenge anytime. Thanks to the ingenuity of some dedicated programmers, Internet gamers overcame many obstacles to playing games through email. They developed notations, automated dice rollers and card dealers, match-making services, and even skilled players. Nevertheless, the range of games that can be played be email remains limited. For one thing, email is only appropriate to games that are made up of discrete events. Play proceeds either by alternating turns between opponents, or by waiting until every player has taken a turn, allowing all of them to examine each other’s moves and respond. Either way, play can be stalled by players who go out for coffee or take a spontaneous trip to Hawaii. The problem is compounded in multiplayer games. The edge comes off a game very quickly when five players end up waiting for a sixth to make her move. Email-based games also limit the nature of activity outside the playing field itself. For me, much of the excitement of FTF gameplaying comes from chatting with my opponent across the board. Of course, I could always exchange email messages in parallel with mailing my moves, but there’s little satisfaction in that. Jokes that might produce a grin when tossed off on the spur of the moment are usually not worth the effort of committing to ASCII and would certainly go stale over the course of transmission. When the game has more than two players, the situation becomes hopeless. By the time a few comments have been traded, it becomes impossible to follow the threads of discussion. Banter doesn’t translate well to email. The solution? Read on.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
ALL I NEED IS A GOOD CLIENT In the bad old days of pre-Web history, every Internet protocol required a dedicated client program. There were discrete applications for Usenet newsgroups, email, FTP, and Gopher, each with its own set of arcane keyboard commands. This arrangement preserved the Internet as the domain of those few willing to master its esoteric rules. Some of us liked it that way. Pathetically, it made us feel superior. At any rate, in that fragmented environment, Internet games were characterized by the protocol they used. Players ran them within the protocol-specific client programs. As game programmers learned to make their creations more sophisticated, they started to soup up the client programs themselves. Eventually, they began to write dedicated game applications which happened to know how to communicate over the network. At its most extreme, this trend resulted in client programs which only knew one game. IRC-BASED GAMES Internet Relay Chat (IRC) addresses both limitations of electronic mail. IRC is a technology that allows multiple users to connect to a single host machine. The IRC host provides the equivalent of different rooms (called “channels”) in which participants can enter comments. Everybody connected to a channel can see the comments entered by everybody else. This has the effect of simulating chat among everyone on the channel. To participate in IRC, you need an Internet client with chat facilities. I recommend one Windows client in Appendix B at the end of this book. By the time you read this, your Internet browser may also include chat capabilities. IRC allows the administrator of the host machine to install programs (called “bots”) on some channels. Among other uses, these bots can serve as non-stop moderators of ongoing games, whose participants join and leave at will. For example, the qnet standalone machine at irc.qnet.com features a Boggle game that has been running uninterrupted for years. To get in on the fun, just join the #boggle channel. The Boggle bot, officially known as Bogbot, recognizes formatted commands as plays in the game. Everything else is assumed to be a comment and gets displayed to all the other players. This allows the game to be a social as well as a competitive event. Once you’re comfortable with a few basic IRC commands, you’ll find online chat as quick-paced and engaging as its FTF counterpart. In fact, after several visits to Bogbot, I’m convinced that to some of the regulars, the companionship is far more important than the game itself. And why not? By their nature, IRC-based games are indifferent to the participation of any particular player. Bogbot rolls merrily on, launching a new game every five minutes, whether there are any players out there or not. Figure 1.4 shows the climax of another frantic round of Boggle.
Figure 1.4 Bogbot calls another one. These two features make IRC a perfect medium for games like Boggle, or a continuous trivia quiz. Players are free to banter and play simultaneously, in whatever proportions they like. They can also join or leave the game at any point, with no negative impact on the other players. IRC games are like a continuous party, where each guest can set her own level of participation. While IRC-based games overcome two of the drawbacks we associated with email-based games, they have severe limitations of their own. First of all, most games are not adaptable to IRC. An IRC game must be run by a bot, who can’t do much more than ask questions and analyze answers. This is perfect for Boggle, where Bogbot just has to generate a matrix of letters, and grade the responses, keeping track of each contestant’s points. Structurally, a trivia quiz is almost identical to Boggle. But any game whose
participants can effect the actual state of play (which, of course, is most games) is hard to port to IRC. Also, IRC games are of necessity text-based. Again, this makes Boggle, with its matrices of letters, a perfect candidate. Boggle also exemplifies the most severe limitation of IRC-based games: boredom. At the end of the day, Boggle contestants play and interact within a world of endless four-by-four matrices of letters. Hardly the setting to satisfy one’s craving for exploration and personal growth. Still, the multidirectional conversational technology represented by IRC represents an important step forward in enhancing the Internet gaming experience.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
MUDS, MUSHES, MOOs, AND ALL THE OTHER M WORDS MUDs, or Multi User Dungeons, date back to 1979. Roy Trubshaw, a student at Essex University in England, wrote a program that simulated a series of contiguous locations where people could move around and chat. The program was a hit, and Trubshaw quickly added two features that came to define MUDs. First, he recast the program as a multiplayer adventure game. Initially, the premise was very simple: Players earned points by collecting treasures and maneuvering around obstacles on their way through a series of rooms. Any player who earned enough points was promoted to wizard. Even this meager storyline was enough to attract anyone who could get a network connection to the Essex computer. Other programmers immediately set to work enhancing Trubshaw’s idea. The second feature Trubshaw added to his original program was a Database Definition Language. This allowed him to separate the description of his dungeon from the program itself. Varying dungeons could be defined for the same program, simplifying the creation of increasingly elaborate MUD worlds. This feature also paved the way for an important enhancement to later MUDs: programming languages built right into the game. These languages allow players to add rooms and objects to the dungeon during the course of play. By allowing changes to the layout of the game, MUD programmers also changed the nature of the game itself. MUDs moved away from fixed-premise adventures and towards social, dynamic world-building activities. Figure 1.5 shows the welcome screen of a typical MUD.
Figure 1.5 Welcome to Avalon. MUDs and all their children extend the IRC experience in several ways. First, they are played within worlds that are from the outset more interesting than the Boggle-world of IRC. Second, most MUDs include a Database Definition Language that allows the players to add new objects during the game. Thus, the MUD-world itself evolves and becomes enhanced in the course of play. Players adopt roles far richer than in simple board games, and the interactions among them are, consequently, more sophisticated. MUDs are true laboratories of human behavior, and I suspect that some of them are actually used as such for psychological research. MUDs are not for everybody, however. For one thing, entry into their captivating worlds comes with a steep learning curve. The very richness of their scenarios filters out those not willing to study the physical, historical, and psychological geography of their playing fields. Second, their text-based nature keeps the barrier to entry higher than it would be for an equivalent game with good graphics (although, I suspect many MUD aficionados like it that way). Similarly, the slow pace of MUD-based games ensures its appeal only to the cerebral end of the spectrum of Internet users. Lastly, let’s face it: Not everyone enjoys fantasy. Surely, there must be some way to play Internet games at a faster pace than email allows, with more intellectual engagement than Boggle demands, without having to put on a gnome suit. In our discussion so far, we haven’t even mentioned some of the most important people at any sporting event: The spectators! How the sense of competition is heightened, the satisfaction of a clever move sharpened, the thrill of victory sweetened, by the presence of an appreciative audience! The various approaches to Internet game programming respond, well, variously to the play-in-a-vacuum problem. Some PBEM servers circulate records of games-in-progress to their subscribers. This gives everybody in the club something to talk about over drinks, but hardly simulates the buzz of a game played in public before anxious onlookers. IRC-based games allow players to check out of competitor mode and comment on each other’s play. Taking time out from the game in order to crack a joke inevitably dulls the sharp edge of competition. This doesn’t disturb the players much, as IRC Boggle seems to be primarily a social event anyway. Which just reinforces what I said earlier about the danger of boredom in IRC games.
As for MUDs, the very notion of spectators and players is foreign to them. People play MUD-based games in order to assume a character and enter an exotic, engaging world, far removed from the drab, wretched “real” world they inhabit most of the time. The last thing they want is for a spectator to tap them on the shoulder electronically to question their behavior. It would destroy the MUD illusion entirely. So, no audiences in MUDville, please. Our search for an Internet gaming experience that combines intellectual engagement, communication among players, and a place for spectators continues.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
DEDICATED CLIENT GAMES Some game programmers escape the limitations of the traditional text-based Internet application interface by writing dedicated client game applications, which the player must download and install before playing. An outstanding example of this type of program can be downloaded from a site called Internet Gaming Zone (http://www.zone.com/index.html). The Zone client combines most of the desirable features of Internet gaming in a single attractive package. It hosts several games, matches players with human or automated opponents, facilitates chat among contestants, and provides a gallery for spectators. The down-side of the Internet Gaming Zone is that it requires participants to download and install a platform-dependent client program. The client will take up over three megs of your hard drive. While versions of the client are available for Mac and Windows 3.1, it is optimized for Windows 95. Gamers on other platforms are completely out of luck. The release notes for the latest version (1.6.0) point to another disadvantage of installed software. The notes consist of the single sentence: “No new features in this release except for several severe bug fixes to all components.” While you do get an announcement encouraging you to install the new release, every time you connect to the Zone, the announcement says nothing about “severe bugs.” Under the old model of installed machine-code, distribution remains a problem, even if the vehicle of distribution is the Internet. As we’ll see, there is a better way. Until recently, I would have added a security concern to my discussion of the Internet Gaming Zone client. I’m suspicious by nature and reluctant to invite unknown machine-code onto my system. But a couple of sources I trust recommended the Internet Gaming Zone, and I installed it. Since that time, the Zone has been purchased by Microsoft, which relieves my fears, at least on one level. Those reservations aside, however, you can have a lot of fun at the Internet Gaming Zone. It uses a village metaphor, with buildings representing the different games available. To play a game, you enter a building by clicking on it. Once inside, you are in a room full of tables. You can sit at an empty table and wait for someone to join you, or join a table where someone is already sitting. You can even select a game in progress to kibitz. Either way, the selected game is represented graphically in its own window, with smooth refreshes as each player takes a turn. At the bottom of each game window, as well as each game-room window, there is a chat area, where players and spectators can trade comments. Figure 1.6 shows the Zone in action.
Figure 1.6 Non-stop fun in the Internet Gaming Zone. The Zone combines many of the advantages of Internet gaming we’ve discussed, in a single, attractive application. If only it didn’t require its own, dedicated client. KALI, HTHP, AND THE GANG Another popular category of client programs consists of commercial local network games, which have been made playable over the Net. The challenge consisted of writing network software that would run over the Net while emulating IPX connectivity. These substitute protocols include KALI and HTHP (Head-To-Head Protocol). The results are impressive and have created a devoted Internet following for games such as Doom and Descent. In fact, the proprietor of a local computer shop told me he can’t keep Duke Nukem in stock. It seems that half the people in Toronto spend their evenings blasting each other over the Net! If that’s what you want to do, have a good time. But first you’ll have to buy a nice shrink-wrapped copy of the game and install it. Your fun will cost
you money, time, and disk space. And you’ll have to repeat the procedure for every single game you want to play.
IT’S A WEBOLUTION: THE EMERGENCE OF THE WORLD WIDE WEB If ever there were an Internet medium with the potential for a complete gaming experience, the World Wide Web is it! The Web combines graphic capabilities, communication facilities, and interactive software in attractive units called HTML (Hypertext Markup Language) pages, which can be viewed through client programs known as Web browsers. With the advent of the World Wide Web, the Internet gaming community has taken several giant steps forward. At its most basic, the Web serves as a launching pad for the venues of Internet play we saw earlier. Let’s take chess and bridge as examples. Drop by the chess page of The Maclin Times (an electronic newspaper run by Philip Maclin) at http://home.sprynet.com/sprynet/ pmaclin/chess.htm. There, you’ll find Phil’s analysis of recent championship matches, his suggestions for how to cope when your ChessBase database becomes unmanageably large, and his recommendations of the best chess links on the Web. On the sites listed, you’ll find plenty of news and analysis, chess-related software, tournament schedules, and online chess club newsletters, contests, and magazines from around the world. As for actually playing chess on the Net, Phil recommends a few sites. His favorite is the Internet Chess Club at http://www.hydra.com/icc. The ICC is a commercially run club (there’s a free trial period). It has its own news service and sponsors lots of events. But mostly, it facilitates Telnet-based chess games between human beings. As with the Internet Gaming Zone and Duke Nukem, if you want to join the club and play using a graphical interface, you’ll have to download one from the ICC and install it on your local drive. All this fun is starting to cost a lot of disk space! On the bridge front, the Great Bridge Links (GBL) page, located at http://www.cbf.ca/query/GBL.html and maintained by Jude Goodwin-Hanson of the Canadian Bridge Federation, promises links to “all that’s bridge on the Net.” In addition to the types of material listed by The Maclin Times, the GBL points to shopping malls for bridge supplies and books, as well as home pages for other bridge enthusiasts. In the “Play Bridge Online” section, Jude lists OKBridge (http://www.okbridge.com), which, to judge by the way people talk about it, is a Telnet-based bridge heaven-on-earth. If you’re connecting from a Windows-based machine, you may optionally use their graphical client. Jude also mentions a number of lesser machines that host bridge play, each with its own (you guessed it) platform-specific client to download and install.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
PLAYING THE WEB We’ve seen the World Wide Web as a repository of information and as a launching pad for Internet gaming using other programs as clients. But the Web is rich enough to allow browsers to serve as gaming clients in their own right. The simplest Web-based games provide a primitive graphical interface to server programs based on technologies we’ve already seen. But even these uses of the Web enhance the game experience by making it more realistic. An extreme example of this can be found at http://elf.udw.ac.za/~scrabble, where a clever program assembles HTML pages on the fly in order to represent the current state of Scrabble games stored in a database on the server! The Scrabble board you see when you connect to this sight is just a picture, assembled tile-by-tile, of a game being played using a very simple program to submit each move. But what a world of difference a picture makes! In addition to its database of games-in-progress, this Scrabble server allows spectators to address comments to the players. Once again, the underlying mechanism is email. True, the mailing lists I mentioned a couple of pages ago include the addresses of all players. Nothing prevents subscribers from sending their comments about the games to whomever they want. But HTML’s mailto tag makes the process so much simpler, lowering the barrier to banging out a spontaneous comment. Another element of successfully simulated gaming falls into place. But the Web allows far more than mere pretty pictures of email-based games-in-progress. Web pages can become the playing field itself. The Connect4 page pictured in Figure 1.7 is a good example. The Connect4 server at http://www.csclub.uwaterloo.ca/u/ kppomaki/c4 is running a program that plays each turn of the actual game. A CGI (Common Gateway Interface) script is responsible for drawing the Connect4 grid, interpreting your mouse clicks, submitting your move to the game-playing program, and interpreting the results. All this computation takes place on the server machine. When the server program decides what move to make, it informs the CGI script, which dynamically constructs a new HTML page and sends it to the client. Visually, the results are a little primitive, as the client, with a flicker, draws a whole new page for each turn. But they are still a huge improvement over the playing-by-email approach. This page demonstrates how the Web can provide an attractive interface to play against a computerized opponent. It also shows how even a simple Web game can provide a playing experience far more lifelike than other Net technologies can.
Figure 1.7 A CGI-based Connect4 site. Of course, CGI-driven games have their limitations. For one thing, they are hard to write. For another, they can represent only twodimensional playing fields, and crudely at that. They also burden the server with all of the game’s computations. Finally, while they greatly improve on the previously existing interfaces for playing against a computerized opponent, they have no mechanism for helping humans to play against each other over the network.
Java Perks: Artificial Intelligence For Internet gamers, another benefit of the World Wide Web has been to allow us simple people to enjoy the fruits of academic research into Artificial Intelligence (AI). In fact, it was Big Blue’s first-game victory over Gary Kasparov that prompted Time magazine’s recent cover story, “Can Machines Think?” The answer was predictably ambiguous and boiled down to, “It depends on how you define thought.” Personally, the breathless headline seems like a non-question, and the ensuing discussions so much wasted breath. It is undeniable, however, that within certain well-defined areas of behavior and interaction, computer programs have achieved remarkable imitations of human thought.
For the purposes of a book about multiplayer Internet games, the metaphysical question is irrelevant. The important point is that game algorithms are one of the most important arenas for testing attempts to capture patterns of thought. Whether or not AI researchers have succeeded in endowing machines with thought, they have unquestionably played a lot of great games along the way. Now, you can too. Thanks to the Web—and to some considerate academics—you can pit yourself against some very smart algorithms. The most dramatic example is Chinook, a program running on a computer at the University of Alberta. Chinook is the world champion of checkers, the only computer to hold a title in human competition. You can check out Chinook at http://web.cs.ualberta.ca/~chinook. Go ahead and challenge it. You can even ask it to lower its smarts to less than a world-class level. If you’re interested in the more academic side of the AI/games programming relationship, take a look at http://www.cs.vu. nl/~victor/thesis.html or http://phobos.cs.ucdavis.edu:8001. These sites provide discussions of the significance of games to AI, game server generalization, and other spicy topics. These pages are too abstract to help out those of us who just want to create a fun Web site, but they do exemplify the open information-sharing that makes the Web such a boon.
The World Wide Web’s presentation style and linking ability make it a natural for online adaptations of interactive fiction. Interactive fiction is a variation on early MUDs, in which the user/reader/player is presented with a scenario and asked to choose a course of action at each juncture in the story. By necessity, the predefined worlds of interactive fiction are more limited than the evolving worlds of advanced MUDs, but the convenience of navigation by clicking creates a smoother, more flowing experience than the text-based MUD navigation. If interactive fiction sounds interesting, stop by The Pit at http://ACVWJYRO.com/sommerv/ BackRoom/The_Pit.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
NETSCAPE AND PLUG-INS The Netscape browser, with its helper applications and plug-in file players, helps the World Wide Web to escape its text-and-CGIscript limitations. Each month, a site called The 40k MIRACLE Monthly at http://www.40kMiracle.com-/indexShocked.html offers a batch of small games and other Web-goodies that make minimal demands on system resources. One of the cuter offerings in the July issue was a game called CastleMouse, in which the player places a bunch of animals in a castle. Armed with knowledge of which animals are afraid of other animals, the player tries to combine the animals in a manner that will drive a frightened mouse into his hole. It may sound trivial, but it looks great. This is partly thanks to a Netscape plug-in called Shockwave, which is available from http://www.macromedia.com/shockwave. As an aside, the July 40k MIRACLE Monthly also features a celebrity trivia-based golf course. Not something I would have thought of doing myself, but there you are. If crossword puzzles are your thing, check out http://www.bdd.com/puzzl/bddpuzzl.cgi/puzzl to get a daily fix, along with the solution to the previous day’s puzzle. The puzzles are in PDF format, which you can read using a helper application called Adobe Acrobat. If you don’t have Acrobat, you can get it from http://www.adobe.com/acrobat. But before you download this application, double check that you really don’t have it on your system. Many software packages include a copy of Acrobat, and you may have installed it already. If you find it on your system, simply identify it to Netscape as a helper application under the General Preferences menu item. JAVA So far, Web gaming has been pretty tame. We’ve seen crossword puzzles, interactive fiction with fancy text-work, and CGI-based games with rudimentary graphics. Shockwave enhanced the multimedia experience, but required the download and installation of a large piece of software. We still haven’t found a way to work within our browser and endow the Web with all the qualities we look for in a gaming environment: A choice of opponents, good graphics, a chat mechanism, and viewing stands for the spectators. As the first platform-independent, distributed programming language understood by most browsers, Java could be the means to create the complete Web gaming environment. What’s so special about being platform-independent and distributed? Let’s think about writing a simple game that can be played across a network by a variable number of players, who may be using any combination of Macs, PCs, and Unix boxes. In order to find each other, users connect to a server machine that keeps track of games in progress, as well as who is waiting to start a new game against how many other players. As basic as this scenario sounds, its implementation in pre-Java technology would involve a major coordinated effort among programmers with many areas of knowledge. The programming team would have to write separate client programs with identical behavior for each platform on which the game would be played. Then, they would have to write a server program that monitors games in progress and connects players with each other. A special graphical interface would be required for each platform. The code that implements the game-playing logic would have to be carefully reviewed for portability. Obviously, memory management is handled variously on different platforms, but more subtle differences—such as the size of simple data types—can have equally profound implications. Finally, each client platform would require its own communication layer to the server program. This layer would be responsible for informing the server of each player’s existence and state. Is she trying to log on to the server, waiting for opponents to start a new game, making a move, or sending a withering comment to her opponent? Ideally, the output from the communication layer should appear identical to the server, so that the server can be indifferent to the platform with which it’s communicating. How does Java help? First of all, its platform independence allows you to write a single version of the client. Any client machine that understands Java will be able to run your game. You don’t even need to consider which platforms to target. If a platform is popular, rest assured that someone is trying to teach it Java. Obviously, the ability to write just one client frees you to concentrate more on the
substance of your game and less on the details of programming. But more importantly, it saves you from the many bugs that are inevitably produced by the porting effort itself. The distributed nature of Java programs also has a few important implications. First, it allows any machine on the Web to function as an application server. Java programs (called applets) are normally embedded in HTML pages. Any Java-enabled browser that connects to that page automatically loads the applet onto the client computer. No more preliminary stage of downloading and installing yet another application onto an overfull hard drive. The applet stays in memory until the user is done with it and disappears when the browser’s garbage collection frees the memory sometime later. The second implication of Java’s distributed nature is that the mechanisms for easy cooperation over the network between client and server are already part of the language. Just use the methods provided by the network package that comes with the Java Developer’s Kit and you’re on your way. Finally, users are guaranteed to run the latest release every time they load your program. Distributing fixes and enhancements becomes automatic. If you are new to Java programming, you can find a quick introduction in Appendix A: A Quick Java Tutorial. For a more thorough treatment of Java programming, try the Java Programming EXplorer, by Neil Bartlett, Alex Leslie, and myself, also published by The Coriolis Group. By now, you’ve had a taste of the forms Internet gaming has taken in the pre-Java era. I’m sure you’re just itching to start creating great multiplayer games of your own, but first, let’s take a quick survey of what’s been achieved so far in the short history of Java game programming. We’ll look at two broad types of games written in Java. Together, these Java games are a promising start to a new era of Internet gaming. Over the course of this book, you’ll learn the techniques you need to create your own exciting Internet games using Java.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Single-Player Games Several single-player Java games are notable for their quick load time and smooth graphics. They represent a quantum leap forward from the Web-based games we looked at earlier. As you’ll see, the comparison of Mike O’Brien’s Java Connect4 with the CGI Connect4 from the University of Waterloo is particularly revealing, and Karl Hornell’s Warp shows how Java can be used to write an attractive two-dimensional arcade game. Mike O’Brien’s Connect4 at http://server.snni.com/~mfo/java/connect4/index.html is fast and slick. It brings home Java’s advantage over CGI scripts for small games of this type. O’Brien’s version loads quickly, looks good, and responds immediately to the player’s moves. Unlike the CGI version, once it’s loaded onto the client machine, it doesn’t need to contact the host again for anything, and it certainly doesn’t need to construct new HTML pages dynamically and send them over the network to be completely redrawn. The graphics on this baby refresh rapidly and smoothly. Karl Hornell’s Warp (http://www.tdb.uu.se/~karl/java/warp.html) imitates a hand-held video game depicting a tank battling its way through a variety of animated enemies. The little hero is a touch over-sensitive to its controls, so steering quickly gets to be an exercise in over-compensation, which makes the game even more exciting than it needs to be. You can see the tiny tank in action in Figure 1.8.
Figure 1.8 The Warp battle rages. Iceblox, which is also authored by Hornell (http://www.tdb.uu.se/~karl/java/iceblox.html), is an amusing Pacman-style game in which an adorable penguin tries to extract golden coins from a maze of ice cubes while being chased by flames. My five-year-old is hooked, presumably by Iceblox’s realism. Figure 1.9 shows a dramatic moment in an Iceblox session.
Figure 1.9 A penguin in distress in Iceblox.
To get a good look at some other single-player games first hand, stop by Jason Gurney’s Java Boutique’s games page (http://weber.u. washington.edu/~jgurney/games), which offers Java versions of many popular games, including Asternoids (written by Ben Sigelman), a Rubiks Cube (written by Song Li), and Yahtzee (written by Dan Sideman). Gurney’s collection demonstrates that Java is up to the challenge of single-player, skill-and-strategy games.
Multiplayer Games Collectively, the multiplayer games I discuss in this section display all the features we’ve been looking for in a gaming environment. They have attractive graphical interfaces, clear displays of games in progress and those waiting to start, and allow chatting among players. One game, ichess even has viewing stands for spectators. Europa (http://www.cgl.uwaterloo.ca/~anicolao/Europa) by Jay Steele and Alex Nicolaou is a battlefield game that pits two armies against each other on a moon of Jupiter. The territory held by each army expands and contracts as they engage in direct confrontation, shoot at each other, or land paratroopers behind each other’s lines. The sound effects are realistic and smoothly coordinated with the action. Figure 1.10 shows Europa’s login screen, as well as a battle in progress.
Figure 1.10 Europa’s login screen and a battle in progress. Systemix Software Inc.’s ichess at http://www.ichess.com is a full-featured, Java-based chess server. It will match you up with opponents or let you kibitz a game in progress. It even looks good. Unearthed by Timothy Macinta (http://www.mit.edu/people/twm/unearthed) is a graphical MUD that lets you choose among five characters with different spell-casting powers and vulnerabilities. Unfortunately, I had trouble getting it to run under Netscape. Something to do with the way Netscape apportions memory to plug-in applications. Whatever the problem, I took Timothy Macinta’s advice and ran Unearthed under the appletviewer that comes with the JDK, which worked just fine. Figure 1.11 shows one of the Unearthed characters looking for action.
Figure 1.11 Would you want to meet this guy on a dark network? We’ve seen how every advance in Internet technology has been applied to enhancing the network gaming experience. In the early days of the Net, players were restricted to simple exchanges of turn descriptions or other information. As new protocols were proposed and accepted, gamers used them to achieve simultaneous communication among groups of players, and to create more complex, engaging settings. The World Wide Web replaced text-based command line interfaces with livelier graphics. Finally, Java enabled the creation of complete gaming environments, environments which combine graphics, opponent brokering, chat, and kibitzing galleries. Want to learn how it’s done? Read on.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 2 OUR FIRST GAME NEIL BARTLETT
L
et’s not waste any time in getting underway. Let’s design a game right off the bat. Of course, given that this is the beginning of
our long journey in learning about Java game programming, our game will be a very ordinary kind of game (we’ve got to start with the small stuff first). What I have in mind is a game called Donuts, a very thinly disguised version of Asteroids. I have chosen to start with such a simple, well-known game for a couple of reasons. One is that I want to show off how easy it is to write a game using GameWorks (one of two Java game frameworks we’ll be designing in this book). The other reason is to make explanations easier by not getting too caught up in the details of the game play. Asteroids is, for those of you living in a cave, your typical “blast the bad guys” game. GameWorks is a set of 50 Java classes that will help us to rapidly design 2D, single-user games. (If you’re interested in multiuser games, check out Chris’ GLE framework later in the book; for 3D stuff check out Fred, described in the last chapter of the book.) You will find the complete source and javadoc documentation for GameWorks on the accompanying CD-ROM. In Chapter 3, we will begin a tour of the GameWorks internals. For now, we look at GameWorks from a user perspective.
DONUTS Despite its simplicity and similarity to Asteroids, I had best explain the concept of Donuts. You see, there are all these donuts floating around in space (you did know that, didn’t you?). Your spaceship materializes in space and, armed with your deadly jelly gun, you must destroy the donuts—before they destroy you. Unfortunately, the donuts are not that easy to kill. The first sticky blast of the jelly gun simply splits a donut into two smaller donuts. A further hit splits the smaller donut into two even smaller donuts. Fortunately, a direct hit on one of the smallest donuts does the trick and the donut explodes in a spectacular cloud of smoke. You get 100 points for a big donut hit, 200 for medium donut, and 300 for a small donut. If a donut hits you, you’re toast. You get three chances to accumulate your best score before the donuts have you for breakfast. Like I said, it’s a very simple game.
DESIGNING DONUTS The GameWorks framework is built around a very simple concept: All games are considered to be actors moving around a stage. In Donuts, the stage is space and the actors are the donuts, spaceship, and jelly-gun squirts. As programmers, we are responsible for creating the actors. We tell them what to do, then leave them to do their own thing. If the actors hit each other, we tell them what to do when that happens, too. The cool thing about GameWorks is that we don’t concern ourselves with screen optimizations, movement, animation, clocking, or a host of other nasty issues. We just describe the actors and the stage, and let the game play itself. A FIRST CUT
Let’s get on and see how GameWorks does its stuff. As a starting point, we will display a couple of randomly moving, spinning donuts on the screen. We can then flesh out this starting point to a complete game. Take a look at Listing 2.1. Listing 2.1 Donuts spinning on the screen. class Donut extends Actor { Donut(Game aGame) { super(aGame); setImage ("images/donut.gif", 4, 32); x= (double) (Math.random()*512); y= (double) (Math.random()*512); vx= NewRandom.doubleBetween(8, 64); vy= NewRandom.doubleBetween(8, 64); } } class Donuts extends Game { public void init() { super.init(); backdropManager.setTiled("images/earthy.gif"); new Donut(this); new Donut(this); } } public class DonutsApplet extends GameApplet { public DonutsApplet() { super(new Donuts()); } } These 20 lines of code (a quarter of which just contain closing braces), create the screen shown in Figure 2.1. It has a jazzy background with two spinning donuts moving randomly around.
Figure 2.1 A couple of menacing killer donuts roam the screen. The donuts are descendants of the Actor class. The Actor class provides support for movement and animation. By setting the x and y position and the x and y velocities, we can predetermine how the donuts will move around the screen. The game class, Donuts, just sets a background image and constructs a couple of donuts. The DonutsApplet class interfaces the game to the outside world. To run this game, enter the following HTML code in a file and run it with the appletviewer: DonutsApplet
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
PICTURES OF ACTORS Actors are not static. They can move, rotate, spin, and gyrate. GameWorks uses a GIF file to describe each of the views of an actor in motion. The views are stored as a rectangular pattern of cells. For instance, the GIF file for a donut is shown in Figure 2.2. As you can see, it consists of 32 views of a donut each in a different phase of a spin. Given this GIF file, GameWorks will cycle repeatedly through the views, giving the appearance of a spinning donut.
Figure 2.2 The GIF file for a spinning donut. The reason for using a GIF file, rather than dynamically computing the spinning donuts, is performance. It is quicker to load prefabricated images, than it is to calculate and render the images at runtime.
MAKING THE DONUTS The first step toward animating the donut is to create a series of individual GIF files for each of the views. You have several options. You can draw each view individually, or you can draw a single view and then apply a series of transformations, such as rotations, using a graphics package. The choice is yours. For Donuts, I used a number of different methods. I drew the jelly-squirt using Fractal Design’s Dabbler with a Wacom ArtPad II pen. I drew the spaceship and then rotated the graphic using Paint Shop Pro. For the donuts, I used POV-ray, a ray-tracing tool. A ray-tracing tool is an image-composition tool that uses computed light rays to construct photo-realistic 3D objects. To generate the donut, I used the POV-ray script shown in Listing 2.2. I then used a tool called MCS (Motion Control Software) to spin the donut. All of these tools are on the CD-ROM. For more information on ray-tracing, see http:// www.povray.org. Listing 2.2 POV-ray source to create a 3D donut. global_settings { assumed_gamma 2.2 } #include "shapes.inc" #include "colors.inc" #include "textures.inc" torus { 7.0, 3.0
texture { pigment { bozo scale color_map { [0.0 0.4 color BakersChoc color BakersChoc ] [0.4 1.01 color Tan color Tan] } } } texture { finish { phong 1 phong_size 100 brilliance 5 ambient 0.2 diffuse 0.8 } pigment { wood turbulence 0.025 scale translate -50*y rotate 1.5*z color_map { [0.0 0.15 color SemiSweetChoc color CoolCopper ] [0.15 0.40 color CoolCopper color Clear ] [0.40 0.80 color Clear color CoolCopper ] [0.80 1.01 color CoolCopper color SemiSweetChoc ] } } } } light_source light_source light_source light_source light_source
{ { { { {
colour White } colour White } colour White } colour White } colour White }
camera { location up right look_at }
JOINING TOGETHER GIF FILES Now that we have our series of GIF files, we can join them together into a single GIF file. This is easier said than done. To my knowledge, there is no tool that will do this automatically. To solve this problem, I used a scripted, graphics-conversion tool called Piclab. A sample from the file I used on the donut images is shown in Listing 2.3. The code works by creating a graphics file of the correct final size, then successively loading in the image files and placing them in the correct place on the image. Calculating the correct positions can be tedious, so I have written a small Java program called MkPicLab to do this. Both Piclab and MkPicLab are on the CD-ROM. Listing 2.3 A sample Piclab script. set DISPLAY svga2 gload DON00001.GIF expand 180 360 gload DON00001.GIF overlay 0 0 gload DON00002.GIF overlay 45 0 … gload DON00032.GIF overlay 135 315 gsave donuts.gif
Note: You might have heard that a single GIF file can contain multiple images and animation images. This is true. However, Java does not provide support for these features. Here we are constructing one GIF image which is made up of each of our separate images.
We have one last job to do: set up the transparent pixels of the GIF file. Notice that the GIF cells are rectangular, but the shapes on
the screen are not. We will use transparent pixels to etch out the shape of the image from the rectangular GIF image. GIF files from version 1989a onwards support transparency. Most graphics packages, however, do not, so you need a tool called giftrans to convert background pixels to the transparent pixels. The background color of the images is set up so that it does not occur in the image itself. The following command converts background pixels in file1.gif into transparent pixels in file2.gif cmd> giftrans -T file1.gif > file2.gif The giftrans utility is also on the CD-ROM. You can get a full set of commands by just typing: cmd> giftrans
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE SPACESHIP Now, let’s take at look at our spaceship. The spaceship will be able to move under user control. The base code for the moving spaceship in shown in Listing 2.4. This class is by far the most complicated class in Donuts. Listing 2.4 The user-controlled spaceship. public class Ship extends Actor implements EventInterface { private int dframe=0; public boolean thrusting= false; Ship(Game aGame) { super(aGame); theGame.play("sounds/ship.au"); setImage("images/ship.gif", 4, 24); x = theGame.stageManager.size().width/2; y = theGame.stageManager.size().height/2; vx = vy = 0.0; } public void destroyActor() { super.destroyActor(); new Explosion(theGame, this); ((Donuts)theGame).decrementShipCount(); } protected void setCell() { image=images.incrementImageNumber(image, dframe); } public void setVelocity() { if (thrusting) { vx += Math.cos(image*2*PI/nImages + PIby2)*10; vy += Math.sin(image*2*PI/nImages - PIby2)*10; } else { vx *= 0.99; vy *= 0.99; } } public public public public
void rotate(int aVelocity) void thrust(boolean onoff) double getSpeed() { return double getAngle() { return
{ dframe = aVelocity;} { thrusting = onoff;} Math.sqrt(vy*vx + vy*vy);} image*2*PI/nImages+PIby2;}
protected void hit(Actor anActor){ String classname = anActor.getClass().getName(); if (classname.equals("Donut")) destroyActor(); }
public boolean keyDown(Event anEvent, int aKey) { switch(aKey) { case Event.UP: thrust(true); return true; case Event.LEFT: rotate(1); return true; case Event.RIGHT: rotate(-1); return true; } return false; } public boolean keyUp(Event anEvent, int aKey) { switch(aKey) { case Event.RIGHT: rotate(0); return true; case Event.LEFT: rotate(0); return true; case Event.UP: thrust(false); return true; } return false; } } Actors are automatically registered for events. To process an event, they implement an event handler method, such as keyDown or keyUp. For example, the ship rotation is controlled by the left and right arrow keys. When one of these arrow keys is pressed, the rotate method is called with the value 1; when the key is released the rotate method is called with the value 0. These values are used to control the image for the spaceship. This image display is performed in the setCell method. This method, which is automatically called 25 times per second by the framework, chooses which cell of the GIF image to display. The spaceship movement is controlled by pressing the Up arrow key. The longer the Up arrow key is held down, the more thrust will be applied. When the Up arrow key is released, the ship will gradually lose speed due to friction (okay, there is not much friction in space, but this is a game). The thrusting boolean variable is set to true when the Up arrow key is down. The setVelocity method, which is also called 25 times per second by the framework, uses thrusting to determine the next velocity to set for the ship. If thrusting is true, then it adds about 10 percent more speed to the ship by setting the vx and vy variables (the x and y velocities of the ship). If thrusting is false, then the velocity is reduced by 1 percent.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE JELLY-SQUIRTS Our next task is to add the jelly-squirts. As you know, the jelly-squirts destroy the donuts when they hit them. (We will ignore the splitting of donuts for the moment.) Adding jelly-squirts to our game involves two things: We need to make some changes to the ship to launch the jelly-squirts, and we need to deal with the jelly-squirts themselves. Listing 2.5 shows the additions to the Ship class. Whenever the Enter key is pressed, a JellySquirt actor is created, providing there are not more then five jelly-squirts currently on the screen. Listing 2.5 Adding support for jelly-squirts to the Ship class. public class Ship extends Actor implements EventInterface { private static int MAX_NUM_JELLY_SQUIRTS = 5; public int numJellySquirts= 0; public void addJellySquirt(JellySquirt j) {++numJellySquirts;} public void removeJellySquirt(JellySquirt j) {--numJellySquirts;} public void shoot() { if (numJellySquirts < MAX_NUM_JELLY_SQUIRTS) new JellySquirt(theGame, this); } public boolean keyDown(Event anEvent, int aKey) { switch(aKey) { … case EventManager.KEY_ENTER: shoot(); return true; } return false; } } The JellySquirt actor is shown in Listing 2.6. The jelly-squirt is constructed at the tip of the spaceship. A sound is heard when it is fired. The speed of the jelly-squirt is set to be faster than the ship. The jelly-squirt only has a limited range. We could try measuring the distance that the jelly-squirt has traveled, but it is easier to limit the time—set by lifeTime—that the jelly-squirt has to live. Once this time has expired, the jelly-squirt dies. Because the jelly-squirt has a constant speed, lifeTime has the same effect as limiting the distance that the jelly-squirt can travel. If it hits a donut, the jelly-squirt will destroy itself. This is done in the hit method. The hit method is called whenever two actors collide, once for each actor involved in the collision. To determine what was hit, the class name of the actor is queried. For the jelly-squirt, only donuts are of interest; a jelly-squirt will not destroy a ship or another jellysquirt. Listing 2.6 The deadly JellySquirt. class JellySquirt extends Actor { long startTime; long lifeTime = 1500; Ship ship; JellySquirt(Game aGame, Ship aShip) {
super(aGame); ship = aShip; theGame.play("sounds/jelly.au"); setImage ("images/jelly.gif", 4, 16); x = ship.x + ship.width/2; y = ship.y + ship.height/2; double vs = ship.getSpeed(); double as = ship.getAngle(); vx = Math.cos(as)*(vs + 150.); vy = Math.sin(as+PI)*(vs + 150.); startTime= theGame.clock.currentTickTime; ship.addJellySquirt(this); } public void destroyActor() { super.destroyActor(); ship.removeJellySquirt(); } public void tick() { super.tick(); if ((theGame.clock.currentTickTime-startTime) > lifeTime) destroyActor(); } protected void hit(Actor anActor) { String classname = anActor.getClass().getName(); if (classname.equals("Donut")) destroyActor(); } } To implement the donut’s destruction, we add a corresponding hit method to the donut: protected void hit(Actor anActor) { String classname = anActor.getClass().getName(); if (classname.equals("JellySquirt")) destroyActor(); }
EXPLOSIONS Just making donuts disappear when they are hit by jelly-squirts is not much fun. How about a fiery explosion instead? Everyone likes a good explosion. We will implement the explosion as an actor. If you think about it, this makes a lot of sense. The explosion needs to be animated to cycle through an expanding fireball sequence. Also, if a moving donut explodes, the explosion needs to move in the same direction and with the same speed as the donut. Take a look at Listing 2.7, our implementation of an explosion. The explosion is created with reference to an actor (the thing that just exploded). The explosion gets its location and velocity from the exploding actor. It plays a sound and loads the sequence of explosion images. In this implementation, all things that explode get the same explosion sequence. The explosion only lasts as long as the sequence of images takes to play out. After that, the explosion is removed from the game. Listing 2.7 An explosion as an actor. class Explosion extends Actor{ Explosion(Game aGame, Actor anActor) { super(aGame); theGame.play("sounds/explode.au"); setImage ("images/explosion.gif", 60, 60, 4, 16);
x = (anActor.x - (width - anActor.width)/2); y = (anActor.y - (height - anActor.height)/2); vx = anActor.vx; vy = anActor.vy; } public void setCell() { if (image == nImages - 1) destroyActor(); image = images.incrementImageNumber(image, 1); } }
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
SPLITTING DONUTS Listing 2.8 shows the Donut class with support for the splitting donuts. There are three type of donuts—LARGE, MEDIUM, and SMALL—given by the size variable. Donut uses a different image sequence for each of the different types. When the donut is hit by a jelly-squirt or a ship, the explode method is called. If the donut is LARGE or MEDIUM, it splits in two by constructing two smaller donuts and destroying itself. If the donut is SMALL, it creates an explosion and destroys itself. The ScoreManager records the current score and displays it at the top of the screen. An array called scores determines how much to add to the score. Listing 2.8 The final version of the Donut actor. class Donut extends Actor { public static final int LARGE=0; public static final int MEDIUM=1; public static final int SMALL=2; static final String[] suffixes= {"L", "M", "S"}; static final int[] scores={ 100, 200, 300 }; int size; Donut (Game aGame, Donut aDonut, int aSize) { super(aGame); size = aSize; setImage ("images/Donut"+suffixes[size]+".gif", 4, 32); if (aDonut == null) { x = Math.random()*theGame.stageManager.size().width; y = Math.random()*theGame.stageManager.size().height; vx = NewRandom.doubleBetween(8, 64); vy = NewRandom.doubleBetween(8, 64); } else { x = aDonut.x; y = aDonut.y; vx = aDonut.vx * NewRandom.doubleBetween(0.5,1.5); vy = aDonut.vy * NewRandom.doubleBetween(0.5,1.5); } image=images.setImageNumber(NewRandom.intBetween(0,nImages-1)); ((Donuts)theGame).addHazard(this); } public void destroyActor() { super.destroyActor(); ((Donuts)theGame).removeHazard(this); } public void explode() { switch (size) { case LARGE: theGame.play("sounds/explode4.au"); new Donut(theGame, this, MEDIUM); new Donut(theGame, this, MEDIUM); break; case MEDIUM:
theGame.play("sounds/explode4.au"); new Donut(theGame, this, SMALL); new Donut(theGame, this, SMALL); break; case SMALL: new Explosion(theGame, this); break; } theGame.scoreManager.add(scores[size]); destroyActor(); } protected void hit(Actor anActor) { String name = anActor.getClass().getName(); if (name.equals("Ship") || name.equals("JellySquirt") ) explode(); } }
ADDING MORE EXCITING GAME PLAY The final version of the Donuts game class is shown in Listing 2.9. It controls all the high-level activities: starting a game, resetting levels, and determining when a game is over. Donuts records the number of donuts that are on screen. If the player destroys all the donuts, the next level is started after a two-second delay—with one more donut than the previous level. Tracking the donuts is done by addHazard and removeHazard. These methods are called by the Donut actor when it is constructed (addHazard) and destroyed (removeHazard). A similar tracking is performed on the number of ships. Each time a ship is destroyed, the count of the number of ships is decreased. When the count reaches zero, the game is over. Game-start and game-over processing are provided by the Game base class. A button is activated when a game is over. When the player presses this button, a new game is started. Listing 2.9 The complete Donuts game class. class Donuts extends Game { static int NUM_SHIPS_PER_PLAYER = 3; static int NUM_DONUTS=2; int numShips; int numDonuts; int numHazards; public void init() { super.init(); backdropManager.setTiled("images/earthy.gif"); } public void startGame() { super.startGame(); numShips = NUM_SHIPS_PER_PLAYER; numDonuts = NUM_DONUTS; newLevel(numDonuts); } public void decrementShipCount() { if (--numShips > 0) new RestartDonuts(this, this.clock, 2000, numDonuts); else gameOver(); } public void addHazard(Actor anActor) { ++numHazards;
} public void removeHazard(Actor anActor) { if (--numHazards == 0) new RestartDonuts(this, this.clock, 2000, ++numDonuts); } protected void newLevel(int aNumDonuts) { numHazards = 0; actorManager.removeAllActors(); eventManager.removeAllNotifications(); new Ship(this); for (int i=0; i < aNumDonuts; ++i) new Donut(this, null, Donut.LARGE); } }
ADDING SOUND Donuts is a very noisy game. The game begins with Homer Simpson drooling over donuts, then there are sounds associated with jelly being squirted, and with donuts being hit and destroyed. Adding sounds is simple, as long as the sounds are in the correct format (see Chapter 10 for more information). I used a sound editor called GoldWave (a gratuitous screen capture is shown in Figure 2.3) to edit the sounds. Then, I put the audio files in the sounds directory. The Game class’ play method is used to play a sound. GoldWave is on the CD-ROM.
Figure 2.3 GoldWave in action.
SUMMARY Okay, that’s it! Donuts is complete. The final version is on the disk. It weighs in at around 250 lines of code. Most games of a similar complexity are around 750-1000 lines, so Donuts is shorter by a factor of 3 or 4. Also, under the covers, GameWorks provides some fast screen optimizations, which many of these other games do not possess. Now that we have seen how GameWorks can be used to write arcade-style games, I hope you are impressed by the performance and the ease of programming the framework provides. GameWorks provides a lot of useful facilities for free. You might be inclined to believe that it was developed specifically for Donuts, but that is not the case. Actually, the initial motivation was for card games, but it proved its use beyond that. Now, let’s move on and see how GameWorks itself is designed.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 3 A GAME FRAMEWORK NEIL BARTLETT
I
n Chapter 2, we saw the GameWorks framework in action. We looked at it from a user perspective. We saw how the end-user
programmer can use it to design and build a game. In this chapter, we’ll start to take apart GameWorks, bit by bit. We’ll examine how it is constructed and the techniques and general principles used in its construction. This chapter focuses on the core classes and a framework overview; subsequent chapters, up until Chapter 12, fill in the details. In Chapter 2, we did not define the word frameworks very well. In this chapter, we are going to set that straight. If you don’t know what a framework is, trust me, you will know all about them by the end of the chapter. This book is written around two frameworks: one for designing single-user games, the other for designing multiuser, networked games. By the end of the book, you will have seen two complete frameworks put together. Most of the code that we design, build, and discuss ends up somewhere inside these two frameworks. The main motivation for using frameworks is that writing games is a lengthy, time-consuming business, filled with such generic programming issues as fast screen updates, network consistency, and image manipulation. If we can solve these issues once and for all, we can significantly reduce the time required to write a game. Once we have our frameworks in place, we can use them to rapidly build high-quality games.
WHAT IS A FRAMEWORK? Frameworks are like repositories of knowledge—knowledge about a particular task. In our case, the task is game program writing. The key behind a framework is the old maxim: don’t reinvent the wheel. The idea is to write code once and use it over and over again, many times in many different circumstances. Cool if you can do it. Now, you might be thinking that these framework things sound mighty like code libraries. Code libraries, such as Windows DLLs (Dynamic Link Libraries), also contain code written to perform a specific task, so why the fancy moniker of frameworks? Well, frameworks are the next logical step from code libraries. They are often described as active code libraries. Active is the operative word here. The framework contains a code library, but, instead of having to write extra code to use a library, the framework makes assumptions on your behalf. To do this, a framework often comes pre-configured. It assumes how you want to use it. You typically instantiate a class or two from the framework and, lo!, it works. It may not do anything particularly interesting, but it will work. In the case of GameWorks, you can instantiate a Game object and maybe some Actor objects. They won’t do much, but you will get a stage, scoreboard, and timer all neatly laid out, as shown in Figure 3.1.
Figure 3.1 Default framework game display. If you want something different to happen, you choose another configuration of the framework. Your main tasks as the programmer are to select the behavior of the framework and to add your own specific behavior. The framework then uses your selections to do the things you want done. Frequently, a well-written framework will contain the code needed to do a lot of your task. You just need to tell the framework which pieces of code you want to run. The framework is taking an active role. Another view that you can take is that frameworks turn the concept of code libraries inside out. You use a code library by writing code that calls the code library; you use a framework by writing code that the framework calls. Take a look at Figure 3.2, which illustrates this point. On the left of the diagram is the code library example. It shows a lengthy piece of code with a number of calls into the code library. On the right of the diagram is the framework example. Here, the user-written code is small fragments of code (overridden methods) that are called by the framework. Both examples potentially do the same job.
Figure 3.2 Coding a code library compared to coding a framework. Overall, frameworks take a lot of the burden of programming off the programmers shoulders. This process is often simpler and less error prone than writing all the code to directly call the code library. Of course, the flip side is that there are a lot of things happening on your behalf that you might not want. These are the things that you will override and, possibly, custom write. The good news, though, is that at the outset you can get a long way very quickly. If the framework is well designed and does most of the things that you need done, then the flip side is minimized, and you mainly see the gain. Frameworks are based on a lot of trust. The framework is doing a lot of work. Relinquishing that kind of activity is something programmers are reluctant to give up. Despite a lot of talk about reuse, programmers like to intimately know what’s going on under the covers. I’m reminded of a Russian programmer friend of mine who very much distrusted the TCP/IP sockets interface until he fully understood the underlying code that implemented it. When designing a framework, it is essential that the overall design be clear and simple, yet powerful enough to allow the framework to assume a lot of the burden from the programmer’s load.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE GAMEWORKS FRAMEWORK Okay, enough with the definitions. Let’s take a look at GameWorks. This is the framework for the single-user, non-networked game. By non-networked, I don’t mean that it is not used in a network. I just mean that the game is played by a single player and does not connect to other players’ machines. Having a completely non-networked Java game would not be using Java to its best potential. We want to be able to use the network to distribute our game, and we want to be able to play the game in a browser—and browsers are networked. I’m emphasizing the non-networked idea to contrast it with Chris’ GLE framework which is very networked. Chris shows you his framework later in the book. The decision to provide two frameworks is based in part on the fact that we had two frameworks, but also because providing two allows us to tune the framework to each case. For instance, the networked framework has a lot of extra classes associated with the networking. The single-user framework avoids the penalty of loading these classes across the network. This keeps the complexity of the single-user framework down and has performance gains when it loads. However, as I note in this chapter’s summary, we are working towards unifying these frameworks.
Note: I suppose this time is as good as any to give a word of warning about the consistency between the source code on the CD-ROM and the source code printed in this book. They are different. I can categorically state that we have made changes (improvements, I hope) to the source between completing the chapters of the book and putting the source on the CD-ROM. There are bug fixes and minor enhancements. View the book as elucidating the fundamentals of how the code works. The final word, as in all cases of software, is not the manual, it is the source. Use the source, Luke.
TIP: Other Games Frameworks There are several other publicly available Java-based game frameworks. The best known is Mark Tacchi’s Gamelet framework. This framework won a prize at the prestigious JavaCup International contest, see JavaSoft’s Web site at http:// java.sun.com for more details. Another good framework is CyberSite, which can be found at http://www2.tcc.net. CyberSite is an attempt to sell a games framework. They are targeting Web site developers who want to create entertainment to draw people to their sites.
FEATURES OF GAMEWORKS GameWorks (henceforth, I will occasionally refer to it as the framework) helps us write games that are played on a single screen, generally a browser window, by one player. This covers a lot of different types of games. The game could be a card game, a board game, or an action arcade-style game, to name a few. The framework needs to abstract out all the key elements of each of these games and provide default code for all of the key elements. GameWorks aids in the construction of 2D bitmap-based games. The games can be run as an applet or as a Java application. The framework sets out to support the following features: • • • •
An actor/stage paradigm to help develop games Arbitrary screen layouts Screen optimization (double-buffering, fixed actor management, dirty rectangles) Image support
• • • • • • • •
Input event management Clocking and timing Collision detection Drag-and-drop Backdrop management Additional random number support Score management Specific game class support: card and dice games
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Table 3.1 shows an alphabetically sorted list of the main classes that make up the framework. The table contains a description of what the class is for and lists the chapter that describes the class design in detail. Table 3.1The GameWorks classes.
Class Name
Chapter
Description
ActorManager
3
Manages a list of actors
Actor
3
Interactive elements of the game
BackdropManager
4
Manages the background to/to.html the stage
BBCollisionDetector
8
Bounding box collision detector
CardPack
12
Pack of playing cards
CardStack
12
Stack of playing cards on a playing surface
Card
12
A playing card
ClockLabel
7
Label showing elapsed time
ClockWatcher
7
Interface to objects watching a Clock object
Clock
7
An object generating regular clock pulses
CollisionDetector
8
Interface for all collision detectors
CountdownTimer
7
Label showing countdown time
DiceLabel
11
Label showing dice value
DiceObserver
11
Interface to objects watching a Dice object
Dice
11
A many-sided die
DirtyRectangleManager
8
Screen optimization class
DragDropManager
6
Manager to drag-and-drop actors
DropSite
6
Place to drop actors
EventInterface
5
Interface to objects watching for events
EventManager
5
Event distributor
GameApplet
3
Bridge between a game and an applet
GameEnv
3
Interface for games to bridge to outside world
GameFrame
3
Bridge between a game and a frame window
GameStats
9
High-score server game statistics
Game
3
Main game abstraction class
HighScoreStats
9
High-score server statistics
HighScoreRecorder
9
High-score server statistics record
HighScoreServerInputProcessor
9
High-score server subsidiary class
HighScoreServerThread
9
High-score server subsidiary class
HighScoreServer
9
High-score server class
ImageHandler
4
Interface to image handlers
ImageManager
4
Cached image manager
Movement
8
Interface to movement of actors
Mover
7
Automated drag-and-drop return to starting place
MultipleImageHandler
4
Handle image sequence composed of many, separate images
NewRandom
11
Assorted random number routines
NewtonMovement
8
Moves actor under Newton’s Laws of Motion
ScoreLabel
9
Displays a score
ScoreManager
9
Records scores
Score
9
High-score server score record
Server
9
Generic server class
ServerInputProcessor
9
Generic server subsidiary class
ServerThread
9
Generic server subsidiary class
SimpleDropSite
6
Drop site with minimal constraints
SingleImageHandler
4
Handle image sequence contains in a single image
StageManager
3
Place to display actors and optimize screen updates
Figure 3.3 shows a class diagram for GameWorks. The class diagram shows how the main classes are related to each other. There are two types of relationship between the classes: inheritance and has-a. Inheritance is, I hope, a concept you are familiar with; has-a means that a class contains a reference to another class. Inheritance is shown in the diagram as an arrowhead. The head of the arrow points to the super class. Has-a is shown by a line with a black dot at one end. The class with the black dot is the class contained inside another class. A number alongside the black dot indicates how many instances of the class are contained in the other class.
Figure 3.3 GameWorks class diagram. Just so you get the idea of how the diagram works, look at the classes Actor, ActorManager, Card, and Dice. Actor and ActorManager are linked by a line with a black dot, with the dot by the Actor class. The black dot has the letter n alongside it. This means that the ActorManager contains a list of n (0 or more) Actors. The Card and the Dice classes are linked to the Actor class by a line with an arrowhead. This means that both the Card and the Dice classes are derived from the Actor class. By inference, this means that the ActorManager could contain a list of Card and Dice objects.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
ACTORS ON A STAGE Earlier, we said that a framework needs to present a clear, consistent view to the user—something the user can trust. GameWorks provides this as a central abstraction on which all games can be based. GameWorks abstracts the games as actors playing on a stage. This means that we have a game object that has a stage object that has a list of actor objects. To understand the power of this concept, let’s look at how this is useful to each of the types of games we have mentioned as being targets for GameWorks. For card games, the game will be whatever card game we are playing (solitaire or bridge, for example), the stage will be the card table that the game is played on and the actors will be the cards themselves. Similarly for board games, the stage is the board, and the actors are the pieces. For an action game, the stage is the setting (space or the Wild West, for example), and the actors are the spacecraft, aliens, boulders, and people that comprise the game. So, we can represent each of our target games as actors on a stage. Where does that get us? Well, the framework can now take care of a lot of the housekeeping details for us. It can remember where the actors are placed on the stage, so that if the stage is redrawn, the actors will be correctly redrawn. It can also help manage screen performance when actors are moved around the stage. All the end-user has to do is derive a class from Actor. The framework handles the rest.
TIP: Other Actor-Based Frameworks This “actors on a stage” concept is very powerful and leads to a lot of very useful features. This is not a new idea; it is a common abstraction of games and simulations. Take a look at Theatrix the C++ games class library (C++ Games Programming, M&T books, ISBN 1-55851-449-X) and Mark Tacchi’s Gamelet framework for more examples of this concept in action.
COMMUNICATING THE DESIGN Before we get too engrossed in designing the framework, there are a few boring things we should get out of the way. In the book, I have used a couple of different ways to describe the classes that compose the framework. The two main techniques I use are the CRC card and the message interaction diagram. I have found these standard techniques to be most useful when doing designs with other people. They are an excellent way of communicating object-oriented ideas. I will be using these techniques to explain the key classes in the framework. One of the beauties of the techniques is that they are fairly self-explanatory, but, just so you know what they are, I have put explanations of these two techniques in sidebars. If you’re interested, have a read.
Java Perks: CRC Cards CRC cards are a useful way of designing classes. A typical CRC card is shown in Figure 3.4. In reality, CRC cards are made from standard 3×5 filing cards. Each CRC card documents one class. The primary goal of CRC cards is to help identify classes that are needed to make a framework. We will be using CRC cards throughout the discussion on the framework design. CRC cards identify the key responsibilities of each class. They also identify other classes that a class will use to carry out its responsibilities. These other classes are known as the collaborators. The main class will depend on its collaborators to supply information or operations. In case you haven’t already put it together, CRC stands for Class/Responsibilities/ Collaborators. A CRC card has four basic elements to it, the class’s name, responsibilities, collaborators, and type. As you can see from
Figure 3.4, the class name is placed on the top left, and the responsibilities are listed down the left in point form. The collaborators are listed on the right alongside the responsibility that they are used to achieve. A dividing line separates the responsibilities from the collaborators. The type is given in the top right. The type is either concrete (it can be constructed), abstract, or interface. CRC cards are useful from a number of perspectives. They are very useful for designing small is beautiful classes:—if the class’s responsibilities are too numerous to fit on the card, then something is wrong. Similarly, if you are having trouble fitting the collaborators on the page, then you probably have too many links between classes. Also, from our perspective, CRC cards are useful for summarizing a class at a glance. One neat thing that falls out of using CRC cards is if you keep them in a box somewhere, you can often reuse a lot of the patterns—the collaborations of a group of classes—in different circumstances. This is not exactly a high-tech solution, but a very practical one nevertheless.
Figure 3.4 A typical CRC card.
Java Perks: Message Interaction Diagrams The CRC card tells us a fair amount about the class, but the message interaction diagram shows the class in operation. It can be very useful when used in conjunction with the definition of a class. A typical message interaction diagram is shown in Figure 3.5. The concept is very simple: It shows the classes and the methods that interconnect them. The message interaction diagrams are useful for throwing away the chaff—they get rid of the housekeeping methods and just show the useful methods that drive the interaction of the classes. The arrows show the direction of the method call. The arrowhead points at the class that implements the method.
Figure 3.5 A typical message interaction diagram.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE CORE FRAMEWORK Now that we have seen the big picture, let’s concentrate on the core functionality of the framework. Figure 3.6 shows the message interaction diagram for the core classes. There are six classes and one interface in all. They are the Game class (which is the central class), the StageManager class, the Actor class, the ActorManager class (which contains the list of actors), and the external interface classes: GameEnv, GameApplet, and GameFrame. These classes are the core classes of the framework. The framework doesn’t need any other classes to work. Let’s look at each of these classes in detail.
Figure 3.6 The core functionality message interaction diagram.
Note: If you compare the version you are about to see with the final versions on the CD-ROM (or even those we saw last chapter to implement our first game), you will notice quite a difference. I’m going to be showing the core classes in their rawest form. They are as simple as they can get while still doing their job properly. I have stripped them of their performance optimizations and fancy features. In succeeding chapters, we will be adding flesh to these classes, as well as introducing other classes to enhance the framework further. So, treat these classes as the final versions undressed—showing you what they were born with.
THE STAGEMANAGER The stage is where all the action happens. You can think of it as the drawing board or the display on which the game is drawn. The StageManager is responsible for coordinating the drawing of the stage. It knows the actors that are on the stage and the backdrop against which the actors are moved. It is also responsible for all stage drawing optimizations. The CRC card for the StageManager is shown in Figure 3.7.
Figure 3.7 StageManager CRC card. Listing 3.1 shows an initial version of the StageManager class. It is a very basic implementation. There are no screen optimizations. When the StageManager object is created, it stores a reference to the Game object, sets itself a size, and stores itself as the stage for the Game object. When the stage is updated or painted, the StageManager queries the ActorManager for a list of actors. It then tells each actor to draw itself on the stage. Each actor supports a paint method to draw itself on the stage. Listing 3.1 The basic version of the StageManager class. class StageManager extends Canvas {
protected Game theGame; int width, height; public StageManager( Game aGame, int w, int h ) { theGame = aGame; width = w; height = h; theGame.setStage(this); } public Dimension size() { return new Dimension(width, height); } public void update (Graphics g) { paint(g); } public void paint( Graphics g) { g.drawImage( theGame.backdropImage, 0, 0, this); for (int i = 0; i < theGame.actorManager.numActors(); i++) { Actor actor = theGame.actorManager.actor(i); actor.paint(g); } } } StageManager is implemented as a Canvas class. The stage can be used as part of an AWT component layout. We can use AWT component layout classes to arrange the display of the game. This allows us to generate interesting game layouts. We can mix the stage with other features—for instance, we can add a scoreboard, a countdown clock, or maybe a video feed of our opponent as he desperately tries to outwit us. We will be adding more to this class in Chapters 6 and 8. THE ACTORS Actors are the elements of the game that we move or see moving around. They are chess pieces in a chess game, cards in a card game, and spaceships and bullets in an arcade game. The Actor class is responsible for maintaining the appearance and position of the actor. The CRC card for the Actor is shown in Figure 3.8.
Figure 3.8 Actor CRC card. Given the definition of actors that we have so far, you might assume that we could implement them as AWT components. However, this is not the case for a number of reasons: • Components do not overlap well • Components are not good at being moved around the screen We will not implement actors as AWT components. However, because components manage input on our behalf, we will have the headache of maintaining our own input. We’ll discuss this issue in Chapter 5. A stripped-down version of the Actor class is shown in Listing 3.2. The Actor class records the X and Y position of the actor on the stage. It also maintains a bounding box of the actor. This version of the Actor class does not show how the actor is drawn. The paint method does not yet do anything. Subclasses of the Actor class will implement the actual paint method.
The actor registers itself with the ActorManager using the registerActor method. Any actor registered with the ActorManager will be displayed. If you do not want to display the actor when it is initially created (perhaps you want to hide the actor initially), you can override the registerActor method. When we destroy an actor, we must remember to remove it from the ActorManager. The destroyActor method takes care of this for us. Listing 3.2 The basic Actor class. class Actor { protected Game theGame; protected double x; protected double y; public int width; public int height; Actor(Game aGame) { theGame = aGame; registerActor(); } public void destroyActor() { theGame.actorManager.removeActor(this); } protected void registerActor() { theGame.actorManager.addActor(this); } public Rectangle getBoundingBox() { bb.x = (int) x; bb.y = (int) y; bb.width = width; bb.height = height; return bb; } public int getX() { return (int) x; } public int getY() { return (int) y; } public void paint(Graphics g) { } } This is not the last we will see of the Actor class. We will be improving it in coming chapters. In Chapter 4, we will be adding image support so that we can see something of our actors. In Chapter 5, we’ll introduce delivering input to actors. Chapters 6 and 8 deal with moving actors.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE ACTORMANAGER The ActorManager has a very simple job: maintain a list of actors. As we have already seen, the StageManager uses the ActorManager to retrieve the list of actors. The game stores the ActorManager. Listing 3.3 shows the ActorManager class, and Figure 3.9 shows the CRC card.
Figure 3.9 ActorManager CRC card. Listing 3.3 The ActorManager class. class ActorManager { protected Game theGame; private Vector actors = new Vector(); public ActorManager(Game aGame) { theGame = aGame; } public ActorManager(Game aGame, int n) { theGame = aGame; actors = new Vector(n); } public void addActor(Actor anActor) { actors.addElement(anActor); } public void removeActor(Actor anActor) { actors.addElement(anActor); } public void removeAllActors() { actors.removeAllElements(); } public int numActors() { int i = actors.size(); return i; } public Actor actor(int i) { return (Actor) actors.elementAt(i); } }
Maintaining a list is such a simple activity, why have I chosen use a class to do this? Why not just incorporate the functionality into either the Game or StageManager classes? The reason is that, thinking ahead, we might want to shuffle around the actor management. For instance, we only have one stage in the current design of the framework, but suppose we wanted several stages with actors shared between them. A good example of this might be a radar scope, which shows the entire game world, and a main stage, which shows the current action in a small area of the game world. These could usefully be implemented as two stages sharing the same actor list; they also share the same ActorManager. Better that we separate out the concept now, rather than do heavy duty surgery later. THE GAME The Game class is what holds everything together. The CRC card for the Game class is shown in Figure 3.10. It creates and maintains a reference to both the StageManager and the ActorManager. Listing 3.4 shows a stripped-down version of the Game class. The Game is implemented as a Panel. The Game class creates the StageManager (a Canvas class) and lays it out on itself using the standard AWT layout management classes, as shown in the createStageManager method.
Figure 3.10 Game class CRC card. Listing 3.4 The Game class, stripped down. class Game extends Panel implements ClockWatcher { String name="Game"; GameEnv gameEnv=null; Canvas stage=null; StageManager stageManager=null; ActorManager actorManager=null; ImageManager imageManager=null; BackdropManager backdropManager=null; EventManager eventManager=null; ScoreManager scoreManager=null; CollisionDetector detector=null; Clock clock=null; Clock userClock=null; Game() { } void init() { createActorManager(); createStageManager(); createClock(); } public void start() { if (clock != null) { clock.start(); //tick(); } else { pendingStart = true; } } public void stop() { if (clock != null) { clock.stop(); } }
protected void createClock() { clock = new Clock(40); clock.addClockWatcher(this); if (pendingStart) { start(); } } protected void createImageManager() { imageManager = new ImageManager(this, imageLoaderWatcher); } protected void createBackdropManager() { backdropManager = new BackdropManager(this); } protected void createEventManager() { eventManager = new EventManager(); } protected void createActorManager() { actorManager = new ActorManager(this); } protected void createStageManager() { setLayout(new BorderLayout()); Panel p = new Panel(); p.setLayout(new FlowLayout(FlowLayout.CENTER,10,0)); p.add(new TextField(10)); add("North", p); add("Center", stageManager = new StageManager(this)); setStage(stageManager); resize(400, 400); stageManager.init(); } void setStage(Canvas aStage) { stage = aStage; } void setGameEnv(GameEnv aGameEnv) { gameEnv = aGameEnv; } Image getImage(String s) { return gameEnv.getImage(s); } public Image createImage(int w, int h) { return gameEnv.createImage(w, h); } public void play(String s) { gameEnv.play(s); } public DataInputStream openFile(String f) { return gameEnv.openFile(f); } public boolean handleEvent(Event anEvent) { return eventManager.handleEvent(anEvent); }
public void tick() { actorManager.tick(); stage.repaint(); } } I have left in a little more than what is strictly necessary for the support of the core functionality. Take a look at the list of class variables, and you will see a list of manager objects. This gives a flavor of the design of the Game class. It is a list of managers that the Game object maintains. You can think of the Game object as a master controller, delegating tasks to its subordinate managers. So far, we have encountered two managers: StageManager and ActorManager. As the book progresses, we will be expanding the Game class by adding more managers that it can delegate work to.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
EXTERNAL CONNECTIONS Until now, I have avoided too many details of how the game gets put on the screen. We have seen that the StageManager class is a Canvas and that the Game class is a Panel, but we have not said how this gets displayed. Are we talking applet or frame window? I decided that I didn’t like the “either/or” choice, so I implemented a system to allow both as potential candidates. The idea is to make it a one-liner to make the game playable as an applet or a frame window. This approach provides flexibility when you come to distribute the game. I imagine most people will opt for the applet choice—after all, applet distribution is a key advantage of Java over other languages. However, for those who want to distribute a standalone game, the choice is there. The key to this flexibility is to use an interface, called GameEnv, to access all things that are normally dependent on the choice of applet or frame. For instance, if you want to create an image and you are using an applet, you would use the applet’s createImage method. However, if you are using a frame, you would need to use the Toolkit class’s createImage method. The change is subtle, but essential. To eliminate the choice, the game will use the createImage method of the GameEnv interface. The interface will either call the applet or Toolkit’s createImage, depending on how the game is being run. Listing 3.5 shows the GameEnv interface, which provides services for images, sound, and file access. The applicability of these services depends on whether the game is being run as an applet or a frame. Listing 3.5 The GameEnv interface. interface GameEnv { Image getImage(String s); Image createImage(int w, int h); void play(String s); DataInputStream openFile(String f); } The Game object contains a reference to the current interface. It uses this to access the environment that the game is running in. Now, let’s look at an actual implementation of the GameEnv interface. Listing 3.6 shows GameApplet, the GameEnv implementation for applets. The GameApplet class provides an implementation for each of the GameEnv methods. It also provides hooks to interface the applet to the game. It implements the applet’s life-cycle methods—init, start, stop, and destroy—and uses them to control how the game is initialized. Listing 3.6 The GameApplet implementation. public class GameApplet extends Applet implements GameEnv { Game theGame; GameApplet(Game aGame) { theGame = aGame; add(theGame); resize(400, 400); theGame.setGameEnv((GameEnv) this); } public void init() {
theGame.init(); } public void start() { theGame.start(); } public void stop() { theGame.stop(); } public void destroy() { } public Image getImage(String s) { URL url = getCodeBase(); return super.getImage(url, s); } public void play(String s) { play(getCodeBase(), s); } public DataInputStream openFile(String name) { try { URL url = new URL(getCodeBase().toString()+name); InputStream s = url.openStream(); return new DataInputStream(new BufferedInputStream(s)); } catch (MalformedURLException e) { System.out.println("Bad file name "+name); } catch (IOException e) { System.out.println("Error reading file "+name); } return null; } } As an example of an alternative to the applet version, a demo frame version, GameFrame, is shown in Listing 3.7. This is an experimental version. Java does not yet allow you to produce sounds from anything other than an applet, so the frame version does not have sounds. However, this does show the principle that alternatives to the applet are possible. As the capabilities of the Java libraries are expanded (especially with the multimedia libraries), the frame-based version will become more viable and useful. Listing 3.7 Example GameFrame implementation. class GameFrame extends Frame implements GameEnv { Game theGame; GameFrame(Game aGame, String t, int w, int h) { super(t); theGame = aGame; setLayout( new FlowLayout() ); add(theGame); resize(w, h); show(); theGame.setGameEnv((GameEnv) this); theGame.init(); theGame.start(); } public Image getImage(String s) { return Toolkit.getDefaultToolkit().getImage(s); }
public void play(String s) { // frames don't allow us to play sounds yet } public DataInputStream openFile(String name) { return null; } public URL getCodeBase() { try { return new URL(""); } catch (MalformedURLException e) { System.out.println("Unabled to get code base for game panel"); return null; } } } ROUNDUP The six classes form the basis of the framework. We can now model games as objects that have a stage and some actors. The games can be delivered as either an applet or a frame. Still, the games can’t do much. There is a lot of work and a lot more of the framework to allow us to write decent games. We will be covering this in subsequent chapters.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
TECHNIQUES USED IN THE FRAMEWORK There are a number of techniques that occur over and over again in the framework. This section highlights the more important techniques. MANAGERS AND HANDLERS If you look way back in the chapter at the class diagram in Figure 3.3, you will notice a lot of classes with the word Manager in their name. The managers all share a common design. Each stores a reference to the Game object that it is working for, and each (generally speaking) has an associated method called createXManager (which is generally found in the Game class). Managers allow Game and other important classes to delegate responsibilities for certain tasks to “expert” classes whose only responsibility is to be good at doing that task. This allows us to fine-tune the manager classes and to provide more than one implementation of the class.
Use Of Factories The createXManager methods are called factory methods. They are responsible for creating an instance of the manager. Factory methods allow subclasses to change the manager or to not create the manager at all. This provides a lot of flexibility and is better than using parameters to control which manager gets created. For example, consider DragDropManager, which we’ll examine in detail in Chapter 6. It is responsible for managing dragging and dropping of actors. DragDropManager is created by the method createDragDropManager. This method creates an instance of the manager and stores it in the Game object. Games, such as arcade games, may not want the functionality to pick up an actor. Therefore, these games can override the createDragDropManager to do nothing, as shown here: void createDragDropManager { } Other games may wish to use a different drag-and-drop manager. In this case, the create method can be overridden to use the new drag-and-drop manager, as shown here: void createDragDropManager { dragDropManager = new MyNewDragDropManager(this); } If you think about a version that tried to do this using parameters, you will realize that the parameter method is a lot messier. You have to construct the class then provide parameters settings. If you need to change a setting, you will need to add more parameters. This can possibly upset existing parameters. The whole affair is very insidious and definitely the inferior solution. OBSERVERS Another common technique used in the framework is the observer, notifier, or model/view pattern (see the sidebar on patterns). The observer pattern is very useful for controlling access to shared information, which makes it ideal for managers.
Java Perks: Introducing Patterns Concepts such as the factory and observers are well known to OOP programmers. These concepts occur frequently in many different programs. Concepts that occur frequently in OOP programs are often known as patterns. Patterns developed as a way of documenting reusable design ideas and to provide guidance on good design—a way of writing down good ideas. There are plenty more patterns than just the factory and observer. Catalogs of patterns are available. Check out the Patterns Home Page at http://st-www.cs.uiuc.edu/users/patterns/patterns.html or the excellent book Design Patterns by Gamma et al. Published by Addison-Wesley, ISBN 0-201-63361-2.
For example, consider the ScoreManager discussed in Chapter 9. The ScoreManager records the current score of a player. The ScoreManager does not display the score—it just keeps track of the value of the score. Other classes such as the ScoreLabel are responsible for displaying the value of the score. However, the ScoreLabel needs to be told when the score changes. The observer pattern is used to do this. As implemented by the Java standard implementation, the observer pattern consists of two classes: Observable and Observer. The idea is that the Observable is managing some information. Observers register themselves with the Observable. When the Observable changes some aspect of the information, it notifies each of the Observers that something has changed. The Observers can then check the Observable for further information and update their own state accordingly. Figure 3.11 shows several Observers watching a single Observable.
Figure 3.11 Observer design pattern.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
In the Java standard implementation, the Observers register themselves with the Observable by calling the addObserver method. The Observable will maintain a list of all the Observers registered with it. When information held by the Observable changes, the Observable will first call the setChanged method and then call the notifyObservers method. This has the effect of calling the update method against each of the Observer objects, as shown in Figure 3.11. The Observers now know that the Observable has changed, and they are free to query the Observable for more information. This may seem like a lot of work. Especially in the case of the score manager. Why not just join together the ScoreManager and the ScoreLabel? The advantage of splitting up the scoring and the display of the score is that we can change how scoring is presented to the user with out making changes to the ScoreManager. We can change the way we display things— we can display more than one score at the same time, or we can instigate actions based on scores. None of these changes require changing the ScoreManager. They just require Observers to be added. For instance, we may want a score at the top of the screen and a score at the side of the screen. Changing the observer pattern case is simply a matter of adding another Observer; changing the non-observer case will mean having to work out a suitable way of adding another ScoreLabel and changing the scoreManager to update the new label. The observer pattern is used very heavily within the framework. However, it is not always implemented using the standard Java implementation of Observer and Observable. To use the standard implementation of Observer, you must derive a class for Observer. However, some classes are already derived from other classes. Since Java does not support multiple inheritance, it is occasionally necessary to implement our own version of the pattern.
SUMMARY GameWorks is a useful tool. It currently supports enough useful features that we can quickly implement a wide variety of games, from board games and card games to arcade games. This chapter introduced the idea of frameworks and shows the core design of the GameWorks framework in particular. The next 10 chapters pick up the ball and run with it. Of course, a piece of software like a framework is never finished; it is very much a work-in-progress. There are plenty more features that can be implemented. On the CD, you will find the latest version of the framework code. You are also invited to visit my Web page at http://www.the-wire.com/usr/neilb. I will be adding features over time. Features I am currently working on adding are: • Cell-based, side-scrolling games—You know, those Nintendo-style games. The ones where a happy little character wanders aimlessly through worlds and blows away aliens. Cell-based, side-scrolling is a technique that was designed for early memory-limited, slow-graphics machines. It enables a whole world to be created with perhaps only 250 unique tiles. The tiles are cleverly arranged in different ways to produce the appearance of a large work. So far my efforts are a little slow, but I’ll be sure to get it working soon. • Seamless evolution to a server-based framework—Obviously, designing multiplayer games and single-player games involves very different issues from a game-playing standpoint, but the framework should support an easy evolution if you want to migrate a single-player game to a multi-player version. Even the original Asteroids had a dual player “my turn/ your turn” version. The current framework does not make this easy. Chris and I are looking at a better migration of the two frameworks. • 3D Support—‘Nuff said.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 4 PAINTING ACTORS AND THE STAGE NEIL BARTLETT
I
n Chapter 3, we introduced the core concept of GameWorks—the idea of representing games as actors on a stage—and we put
together a preliminary version of the classes that implement it. The trouble is, we neglected to do any painting of either the actors or the stage. We put hooks in the code for the painting, but we skipped the implementation. In this chapter, we will fill in the gaps. We are going to look at techniques for painting the stage and the actors.
PAINTING ACTORS AND THE STAGE We have kept things simple so far. Actors implement a paint method. They can do whatever they like in the paint method, as long as they paint themselves on the stage. The stage is equally simple. It consists of a backdrop image on top of which the actors are drawn. In our design, the guiding light is that we want our framework to be as immediately useful as possible. We also want it to be flexible and easy to use. Painting actors and the stage involves two basic techniques: graphics primitives, such as lines and circles, and images. We will be covering both techniques in this chapter, but we will be emphasizing the use of imaging. The framework will, by default, be configured to accept images for the actors and for the backdrop of the stage.
GRAPHICS DRAWING Graphics drawing is the simplest technique, and the one requiring the least support from the framework. The actor simply overrides the paint method and uses the drawing operations provided by the Graphics class to draw itself on the stage. All drawing operations are performed using the standard Java coordinate system, which is shown in Figure 4.1. The origin of the coordinate system is the upper-left corner. The x coordinate increases across the screen; the y coordinate increases down the screen. Drawing operations include line, rectangle, and ellipse drawing; color manipulation; and text drawing.
Figure 4.1 The Java graphics coordinate system. For example, an actor that represents a paddle in a game of Pong, the original computer tennis game, could be implemented as shown in Listing 4.1. Notice that, as we saw last chapter, the x and y coordinates and the width and height dimensions are already defined within the Actor class.
Listing 4.1 An example implementation of a paddle in a game of Pong. Class PongPaddle extends Actor { Color color; PontPaddle(Game aGame, Color aColor) { super(aGame); color = aColor; } public void paint(Graphics g) { g.setPaintMode(); g.setColor(color); g.drawRect((int)x, (int)y, width, height); } }
IMAGE DRAWING The basis of image drawing is to place the image for the actor in a graphics file. The image is drawn in the actor’s paint method. Java supports two formats for images files: GIF and JPEG. In the games framework, we use both formats, but for actors, we will be using mainly GIF files. GIF files support concepts, such as transparency, that are useful for implementing more attractive-looking actors. We will talk more about this in a moment.
TIP: GIF Files The GIF (Graphic Interchange Format) format is a graphics file format first developed by CompuServe in 1987. It has undergone a number of revisions, the latest of which is 1989a. This format has the following features: • Supports 2 to 256 colors • Allows transparent pixels drawn in a transparent color • Provides compression GIF has other useful features, such as animation, but these features are not supported by Java. When using Java, it is best to stick with the GIF format. If you have an image in another format, I recommend that you convert it to GIF using an image converter, such as Graphics Workshop. However, if necessary, you can create support for new image formats. For more information, check out “Java Q&A” in the October 1996 issue of Dr. Dobb’s Journal. The code for this article is at http://www.digitalfocus.com/ddj/code.
USING IMAGES In principle, using images in Java is easy. All images are stored as objects of the Image class. You associate an Image object with an image file using the getImage method, which is supplied by both the Applet class and the Toolkit class. You then draw the image using the drawImage method, which is supplied by the Graphics class. The coordinate system for images is the same as the coordinate system for graphics primitives. Using Java in this way will work; however, it does not provide optimal solutions for games. In particular, Java uses an asynchronous, cached, demand-loading mechanism for loading images that causes unpredictable results the first time you draw images. Later in this chapter, we’ll look at what “asynchronous, demand-loading” means and how to fix the problems it causes for game programming. For now, though, we’ll concentrate on the image support we need.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
NON-RECTANGULAR IMAGES All Java images are rectangular, however, a lot of game images, actors in particular, are not rectangular. In fact, most actors in games are most decidedly not rectangular. Who ever heard of a monster from the planet Thraag being rectangular (for that matter, who ever heard of a monster from the planet Thraag)? Our first task then is to simulate non-rectangular images. The way to do this is to use transparent pixels. Pixels are the individual dots that make up an image. A pixel of an image corresponds to a dot on the computer screen. A transparent pixel will let whatever pixel is already on the screen show through. For instance, if we have a background image drawn on the screen and we place an image with a transparent pixel on top of the background, a single pixel from the background will show through the transparent pixel of the image. If we arrange the image so that all the pixels from the edge of the actor image to the edge of the Java image are transparent, we can, in effect, simulate nonrectangular images. This is shown in Figure 4.2.
Figure 4.2 How transparent pixels are used.
Java Perks: How Image Transparency Works Images in Java can be considered to be an array of pixels. Each pixel corresponds to a dot on the screen. In Java, each pixel has four separate attributes. Three of the attributes (R, G, and B) represent the colors red, green, and blue. Each of the attributes for each of the pixels is assigned a value between 0 and 255. A pixel that has R as 255 and G and B as 0 appears red. The fourth attribute, called the Alpha value, controls the blending of the original pixel with the pixel that it is drawn on top of it. If the Alpha value is 0, then the top pixel is transparent; if the value is 255, then it is opaque. A value in between 0 and 255 is a blend of the two pixels.
GAMEWORKS IMAGE SUPPORT The following items are the main image support requirements for GameWorks: • Background Images—These typically static images are used to display the background on which the action is taking place. The background image might be an image that completely covers the background, or it might be an image that is used to tile the background. For example, a marble texture image might be used to cover the background of a card game to give the effect of the card game being played on a marble surface. The tiling allows us to use a smaller (hence, fasterloading) image file. • Static Image Actors—Many actors, for example playing cards in a card game, use a single image that is static across the lifetime of the game. • Animated Actors—Animated actors are actors that change the image that represents them. For example, a fish swimming across the stage will look more realistic if its tail waggles. To simulate this, we use a number of different images of the
fish, each one with the tail in a different position. We then cycle through the images to create the impression of the tail moving. • Combination Actors—These actors change between being static and animated, or use several static images at different stages of the game. For example, we might want playing-card actors to support being turned over, so they must be able to display both the back and the front of the card. These four items represent our primary uses for images. In this section, we’ll concentrate on providing support for actors. Background image support is the responsibility of the BackdropManager, which we’ll discuss toward the end of the chapter. Broadly stated, the requirements boil down to representing an actor as either a sequence of images or as a single image. The sequence of images can be represented in one of two ways: • Multiple Images—Each image in the sequence is represented by a separate Image object. • Celled Image—A single image contains all the images. The single image is divided into a rectangular grid of cells, each cell containing a different image, as shown in Figure 4.3. Typically, each cell is the same size.
Figure 4.3 How cells are arranged in an image. Celled image files are an efficient way of storing an animated image. Generally, a sequence of actor images are sufficiently similar that compressed image formats, such as GIF files, will store the individual images more compactly as a single, celled image file than as multiple image files. For instance, the set of images that I use for playing cards occupies 50 K as multiple GIF files, but only 34 K as a single GIF file. This may not sound like a huge savings, but on a slow Internet connection, this will shave about 15 seconds off of a game’s load time.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
IMAGE HANDLERS To support imaging for actors, we could design a mechanism that uses a different subclass of the Actor class for each of the types of imaging support we need. For example, we could have SingleImageActor for actors using a single image and MultipleImageActor for actors that use a sequence of images. However, this is a very limiting idea. For instance, suppose we design a board game and decide at the outset to implement the pieces on the board as an animated sequence of images that act out action sequences when they are moved—à la Battle Chess. When we first implement them, the action sequences are stored in multiple image files, so we derive the board piece from the MultipleImageActor class. Later, an artist friend gives us a superbly crafted action sequence that is in cell format. To use the new sequence, we must either convert the sequence to multiple images or re-inherit the board piece from a celled actor class. Both are potentially messy changes. A solution to this problem is to use what I call image handlers. Image handlers are classes that implement the static, multiple, or celled images on behalf of the Actor class. Also, if we happen to come up with alternative strategies, the image handler can implement those, too. All image handlers implement the ImageHandler interface, shown in Listing 4.2. Actors use an image handler to manage the images for them. The image handler hides the details of the image management from the actor. Image handlers are then free to implement a variety of management schemes. The actor calls the image handler to paint the image. The ImageHandler interface allows the actor to determine the number of images and then to select an image to display from that number. Listing 4.2 The ImageHandler interface. interface ImageHandler { int setImageNumber(int i); int incrementImageNumber(int i, int j); int getNumberOfImages(); int getWidth(); int getHeight(); void paint(Actor a, Graphics g); } The relevant code from the Actor class that supports the image handler is shown in Listing 4.3. All code not relevant to the image handler support has been stripped out. The image handler is constructed by the createImageHandler factory method. By default, this will construct a SingleImageHandler. The actor maintains a reference to the image handler. To select another image handler, an actor can override the createImageHandler method. I’ll describe the available image handlers later in this section. The actor uses the image handler to set the image(s), to paint the current image and to select which image is displayed. The setImage methods provide a selection of services to configure the image handler. The paint method uses the image handler to draw the currently selected image. The image to be displayed is selected by calls to the image handler. For instance, setCell will automatically cycle through the available images. It will move forward one image in the sequence each time it is called. The swimming fish actor I mentioned earlier would simply call the setCell method each time it wanted to move its tail. The setCell call could be tied to a clock that would cause the tail to waggle automatically. We discuss clocks in Chapter 7. We discuss sprites, which make heavy use of cell animation, in Chapter 8. Listing 4.3 How the Actor class supports the ImageHandler interface. class Actor { protected Game theGame;
protected ImageHandler images; public int image; public int nImages; Actor(Game aGame) { theGame = aGame; createImageHandler(); } protected void createImageHandler() { images = new SingleImageHandler(theGame, this); image = images.setImageNumber(0); } protected void setCell() { image = images.incrementImageNumber(image, 1); } public void paint(Graphics g) { if (visible) images.paint(this, g); } protected void setImage(String s) { SingleImageHandler sih = (SingleImageHandler) images; sih.setImage(s); nImages = sih.getNumberOfImages(); height = sih.getHeight(); width = sih.getWidth(); } protected void setImage(String s, int c, int n) { SingleImageHandler sih = (SingleImageHandler) images; sih.setImage(s, c, n); nImages = sih.getNumberOfImages(); height = sih.getHeight(); width = sih.getWidth(); } protected void setImage(String s, int w, int h, int c, int n) { SingleImageHandler sih = (SingleImageHandler) images; sih.setImage(s, w, h, c, n); nImages = sih.getNumberOfImages(); height = sih.getHeight(); width = sih.getWidth(); } } GameWorks defines two image handlers: SingleImageHandler and MultipleImageHandler. In effect, image handlers are plug-andplay objects. The actor selects the image handler that matches the images that the actor wants to display. If the actor wants to use a different type of image display, the actor only needs to construct a different image handler.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
SINGLEIMAGEHANDLER SingleImageHandler is responsible for implementing both the single image and the celled image. The single image is treated as a image with just one cell. The code for the SingleImageHandler class is shown in Listing 4.4. It implements each of the methods of the ImageHandler interface. Listing 4.4 ImageHandler for single- and multi-celled images. class SingleImageHandler implements ImageHandler { Game theGame; Actor actor; int width, height; int cellWidth, cellHeight; protected Image image; protected int numCells; public int cellCols; SingleImageHandler(Game aGame, Actor anActor) { theGame = aGame; actor = anActor; } protected void loadImageInfo(Image anImage) { do height=anImage.getHeight(theGame.stage); while ( height == -1); do width=anImage.getWidth(theGame.stage); while (width == -1); } public void setImage(String aName) { Image img=theGame.imageManager.getImage(aName, true); loadImageInfo(img); setImage(img); } public void setImage(Image anImage) { loadImageInfo(anImage); setImage(anImage, width, height, 1, 1); } public void setImage(String aName, int aw, int ah, int aCellCols, int aNumCells) { Image img=theGame.imageManager.getImage(aName, true); loadImageInfo(img); setImage(img, aw, ah, aCellCols, aNumCells); } public void setImage(Image anImage, int aw, int ah, int aCellCols, int aNumCells) { image = anImage;
cellCols = aCellCols; numCells = aNumCells; cellWidth = aw; cellHeight = ah; } public void setImage(String aName, int aCols, int aNumCells) { Image img=theGame.imageManager.getImage(aName, true); loadImageInfo(img); setImage(img, width/aCols, height/(int)Math.ceil((double)aNumCells/(double) aCols), aCellCols, aNumCells); } public void setImage(Image anImage, int aCols, int aNumCells) { loadImageInfo(anImage); setImage(anImage, width/aCols, height/(int)Math.ceil((double)aNumCells/(double) aCols), aCellCols, aNumCells); } public int setImageNumber(int i) { if (i < 0) return numCells-1; if (i >= numCells) return 0; return i; } public int return } public int public int public int
incrementImageNumber(int i, int c) { setImageNumber(i+c); getNumberOfImages() { return numCells; getWidth() { return cellWidth; } getHeight() { return cellHeight; }
}
public void paint(Actor anActor, Graphics g) { if (image == null) return; int x= -(anActor.image%cellCols)*cellWidth; int y= (int)-Math.floor(anActor.image/cellCols)*cellHeight; Graphics g2 = g.create((int) anActor.x, (int) anActor.y, cellWidth, cellHeight); g2.drawImage(image, x, y, theGame); g2.dispose(); } } The image is stored in a single Image object. A fair amount of code is devoted to calculating the cell of the image to be displayed. The height and width of the cell are recorded in the cellWidth and cellHeight variables. All cells are assumed to be of equal size. The numCells determines how many images are stored in the single file. The cellCols variable determines the number of columns in the grid. See Figure 4.4 for more details.
Figure 4.4 Arrangement of cells in a single image. The SingleImageHandler uses a manager called the ImageManager to help perform generic image-management tasks, such as doing a getImage. The ImageManager, which we will see more of later in this chapter, provides image services and performance enhancements to all classes in GameWorks.
Java Perks: Clipping To A Smaller Graphics Rectangle Drawing a cell raises the problem of how to draw only a portion of an image. Java supports the idea of a clipping rectangle—a rectangle that restricts drawing to only the area inside the rectangle—with the Graphics.clipRect method. Unfortunately, this method does not set the clipping rectangle to the rectangle that you supply. Instead, it creates a new rectangle from the intersection of the new clipping rectangle with the existing clipping rectangle. Therefore, if you are setting several successive rectangles and they do not overlap, the clipRect method will not correctly set the clipping rectangle for you, as shown in Figure 4.5. To solve this problem, use the following idiom: Rectangle r; public void paint( Graphics g ) { Graphics gRect = g.create(r.x, r.y, r.width, r.height); gRect.drawImage(anImage, -r.x, -r.y, anObserver); gRect.dispose(); } This idiom creates a new graphics context, gRect, from the graphics context, g. The new graphics context is the same as the old, except that the clipping rectangle is set to the given rectangle. Oh, and there’s one other difference. The original of the graphics context is translated to the point (r.x, r.y). Therefore, we have to fix the offset by subtracting the point (r.x, r. y) when we draw any images.
Figure 4.5 Incorrect clipping rectangle setting.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
MULTIPLEIMAGEHANDLER MultipleImageHandler, shown in Listing 4.5, is similar to SingleImageHandler. The main difference is that it stores the images in an array of images rather than in a single image. This approach simplifies the calculation of the portion of the image to display. MultipleImageHandler just indexes the array. Listing 4.5 The MultipleImageHandler class. class MultipleImageHandler implements ImageHandler { Game theGame; Actor actor; int width, height; protected Image images[]; protected int numImages; MultipleImageHandler(Game aGame, Actor anActor) { theGame = aGame; actor = anActor; } void setImages(String n[]) { images = new Image[n.length]; for (int i=0; i < n.length; ++i) { images[i] = theGame.imageManager.getImage(n[i], true); loadImageInfo(images[i]); } numImages = n.length; } void setImages(Image aimages[]) { images = new Image[aimages.length]; for (int i=0; i < aimages.length; ++i) { images[i] = aimages[i]; loadImageInfo(images[i]); } numImages = aimages.length; } protected void loadImageInfo(Image anImage) { do height=anImage.getHeight(theGame); while (height==-1); do width=anImage.getWidth(theGame); while (width == -1); } public int setImageNumber(int i) { if (i < 0) return numImages-1; if (i >= numImages) return 0; return i; }
public int incrementImageNumber(int i, int c) { return setImageNumber(i+c); } public int getNumberOfImages() { return numImages; } public int getWidth() { return width; } public int getHeight() { return height; } public void paint(Actor anActor, Graphics g) { Image image = images[anActor.image]; if (image == null) return; g.drawImage(image, (int)anActor.x,(int)anActor.y, theGame); } } IMAGE SHARING A useful performance enhancement is to share images at the class level. This will stop the images from being loaded, and Image objects from being created, for each object of the class. For instance, imagine a Card class. Every card in a pack has the same backing pattern. It makes sense to only load in the backing pattern image, and create an Image object for it, once. To do this, the Card actor class simply uses a static ImageHandler object to store the reference to the current image handler. It only constructs the image handler for the first card. All other cards share the same image handler. Some example code that implements this sharing concept is shown in Listing 4.6. Listing 4.6 An actor that shares images across all objects of the class. class SharedImageActor extends Actor { static ImageHandler masterImageHandler = null; protected void createImageHandler() { if (masterImageHandler == null) { images = new SingleImageHandler(theGame, this); image = images.setImageNumber(0); } else { images = masterImageHandler; image = images.setImageNumber(0); } } } Notice that the image handlers do not store the current image number. This information is stored in the Actor class. I took this approach specifically to support the shared-image concept. If the current image was stored in the image handler, then all actors of the class would have to display the same image at the same time. This would make a rather odd-looking card game.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
LOADING IMAGES When I first mentioned using images, I stated that Java uses an asynchronous, cached, demand-loading mechanism to support images. Simple, huh? Well, we are going to spend the next few pages examining what all those words mean. The first thing we will look at is image loading. As far as you, the programmer, are concerned, images are objects of the Image class. Images are created and loaded using either createImage or getImage method calls. Image-loading is the process of reading in a file or some other image source, such as a in-memory image, and storing it in an Image object. Images are not loaded until they are needed. They are loaded on-demand. Therefore, a call to getImage will return immediately. It will not even check if the source of the image, a file say, exists. The getImage just registers where the image will come from. The reason for doing this is to prevent Java programs from blocking or stopping while large images are loaded over a potentially slow network. The actual image loading is only performed when the image is needed. A typical example of this is when we want to draw the image. We can draw an image using the drawImage method from the Graphics class. When the Graphics class tries to draw the image, it first checks to see if the image is loaded, if it isn’t then it starts to load the image from its source. Obviously, Java has now created a problem for itself. It has delayed the loading of the image until it needs it, but when it needs the image, Java has to wait for the image to be loaded until it can draw it. (At least, the first time it wants to draw it.) Java solves this problem in a rather ingenious way. When Java needs an image, it uses a separate thread to load the image. The loading thread allows the rest of the program or applet to get on with its life without having to hang around for the image to be completely loaded. An object called an image observer is used to watch the thread load the image. Once the image is loaded, the image observer can perform any pending operations for the newly loaded image. For instance, when we draw an image and it is not loaded, the call to drawImage will set up an image observer to watch the loading, tell the loader thread to start loading the image, and then return immediately. The image observer will watch the image as it loads. When the loading is complete, the image observer will draw the image on the screen. Of course, this might be several seconds or even several minutes after the original drawImage was requested, but Java has achieved its aim: Images are loaded across the network without blocking the Java program. Image observers are objects that implement the ImageObserver interface. which contains one method, the imageUpdate method. The imageUpdate method is called as an image is loading. It uses status bits to describe how the image loading is progressing. All objects of the AWT Component class implement the ImageObserver interface. The standard behavior for components is to do a redraw when image loading completes. DIGGING DEEPER So, how does the loading actually work? To understand this, we need to meet two new classes, core classes that act at the very heart of the imaging system: ImageProducer and ImageConsumer. Given the names, I bet you can guess what each of these classes does. Yes, the ImageProducer produces an image, while the ImageConsumer consumes the image. You can imagine that the producer feeds the consumer with the image. Loading an image consists of a producer feeding the pixels of the image from, say, a file to a consumer, which stores the pixels in the image.
The image-loading process proceeds as follows. The producer stores the image source location. A consumer registers itself with the producer and asks it to get the representation of the image. The producer goes to the source and transfers the information to the consumer in chunks. This whole process is watched by an image observer. Each time a chunk goes between the producer and the consumer the imageUpdate method is called. Take a look at the diagram shown in Figure 4.6. It shows an image being loaded. The ImageProducer is sending the ImageConsumer the pixels of the image from a file, circle.gif. The whole process is watched by an ImageObserver.
Figure 4.6 ImageProducer-ImageConsumer watched by ImageObserver.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Circle.gif is a file containing the image of a circle in GIF format. The ImageProducer is reading the file in GIF format but is converting it into an array of pixels. The ImageConsumer expects the image to be delivered in this array of pixels format. It stores the pixels inside the Image object in an internal representation format. We don’t know—and we don’t need to know—how the internal image format works. It is hidden from us. If we want to get our hands on the pixels of the image, we have to use a consumer, called PixelGrabber, to get the array of pixels from an image producer (see the sidebar Examining Pixels for more information). Each pixel is stored in the array as an int. Each int stores the color and transparency information for one pixel. The pixels are arranged in the int array as horizontal lines of the image joined together. In other words, the first pixel of the first line is at the very start of the array. The rest of the pixels for the first line follow the first pixel one by one. After the last pixel of the first line comes the first pixel for the second line followed by the rest of the pixels from the second line, and so on with the rest of the lines of the image. Take a look at Figure 4.7 to see how the pixels are laid out in the array.
Figure 4.7 Arrangement of pixels as an array of ints.
Java Perks: Examining Pixels The only way to examine the individual pixels of an image is to use the PixelGrabber. The PixelGrabber is an image consumer that turns an image into an array of pixels. For example, to find the color of an individual pixel of an image, you construct a PixelGrabber and call its grabPixels method. This will (eventually) return an array of pixels which you can index to find the pixel and its color. This technique is slow. The PixelGrabber has to break apart the entire image to find the one pixel you want to look at. For this reason, examining individual pixels of an image is not often used in Java games programming. This is in contrast to traditional games programming environments, such as DOS, which allow fast indexing of individual pixels. These environments exploit the fast pixel access and use it in techniques such as color-based collision detection in which a collision between two actor images is detected based on the color of the pixels of each of the actors.
So the image-loading process is not so difficult to understand. The producer sends the image to the consumer. Once it is ready, the observer performs any pending image operations. The image is asynchronously loaded on-demand. Earlier, I said that the images are cached. Caching means that the images are stored in a more accessible format, ready to be reused. Maybe I should not have mentioned this. You should try to imagine that the image is never actually stored on your system. Each time the image is loaded, the producer-consumer thing happens. An image observer sees it happen, watches it complete, and draws the image. And then the image is thrown away. Just discarded. Next time we draw the same image, we go through the same process: producer-consumer, observer, paint. This sounds extreme, doesn’t it? It certainly grates on our finely tuned sense of performance. It does an awful lot of work each time we want to draw an image. Why not save the image and redraw it each time from an internal store? Well, in truth this is what does happen, but the point is you can’t count on it. The reason for using this process is that images are potentially large bulky things. They consume a lot of memory space. The runtime implementation retains the right to throw the image representation away in order to regain space, if space is tight. To retain this right, the producer-consumer thing might be needed at any time.
For this reason, you should consider that the image is always loaded on-demand by the producer-consumer each and every time the drawImage is needed. In practice, of course, most times the image will be in memory ready and waiting for fast performance, but you should not write code that assumes this. READY FOR SOME MORE? We have talked about loading, but there is more. Think about this for a moment. If we can never guarantee that the image is in memory, how do we know how big the image is? How do we know its height or its width? Surely it must be loaded for us to know this information. This is true. If you look at the methods that get the image size, getWidth and getHeight, you will see that they too take an ImageObserver. If they return –1, then the information is not ready and the imageUpdate is called when the information is ready. As you can see, this asynchronous image-loading is very insidious. It touches all aspects of image handling. You need to always have your wits about you when you code images. The information you need might still be a producer-consumer away.
THE IMAGEMANAGER Well, I think we have had more than enough theory. We could go on about this producer-consumer stuff into the wee hours of the night. But I think it’s about time we introduced the ImageManager. The ImageManager is owned and created by the Game class. It is responsible for providing imaging support services. The ImageManager provides basic services, including preloading images, image tiling, and image effects. Unlike a lot of the other GameWorks’ managers, though, the ImageManager is more of a collection of useful image services, a kind of code library—rather than a manager, such as StageManager, that controls a resource. Let’s look at a selection of services provided by the ImageManager.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
PRELOADING IMAGES The essence of any game involving movement is that the game flows smoothly without interruptions. Fast-paced, arcade-style games demand that all the game actors are rapidly created and moved around the screen. One potential damper on this movement is the image-loading mechanism we just discussed. Once a game is in play, it is disruptive if an image is loaded—either the action is entirely missed, or only a portion of the action is shown. This will confuse the game player and detract from the game play. Therefore, we need to subvert the image-loading mechanism to ensure that all the images for a game are fully loaded before game play commences.
Java Perks: Scaling Images It is very easy to scale images. Simply use the drawImage (image, x, y, width, height, observer) version of the Graphics class drawImage method. If the image dimensions do not match in either width or height, the image will be scaled to match the width and height. Be warned, though, scaling uses the producer-consumer system to create the scaled image. Therefore, it is best to prescale the images before the game starts. To do this, use the following technique: Image scaledImage; MediaTracker tracker=new MediaTracker(theGame); scaledImage = theGame.createImage(width, height); scaledImage.getGraphics.drawImage(image, 0, 0, width, height, theGame); tracker.addImage(scaledImage, 0); tracker.waitForAll();
The ImageManager automatically does this for us. When the ImageManager is created, it automatically starts loading all the images that it requires. It does this by calling the preloadImages method, shown in Listing 4.7, from its constructor. This method uses Java’s MediaTracker to ensure that all images are loaded before proceeding. The MediaTracker class allows us to add images to a tracking list and then wait for all the images in the list to load before continuing. Listing 4.7 The ImageManager preloadImages method. void preloadImages(String file) { String line; MediaTracker tracker=new MediaTracker(theGame); try { DataInputStream f = theGame.openFile(file); while ((line = f.readLine()) != null) { Image image = getImage("images/"+line); tracker.addImage(image, 0); } tracker.waitForAll(); if (tracker.isErrorAny()) System.out.println("Error preloading image files"); } catch (Exception e){ System.out.println("Error loading image file " + e.getMessage()); }
} One problem is that we don’t know which images to load for a given game. We might store images from several games in a single directory, but we don’t want to load more images than necessary. The solution is to place the names of all the images that we want to load for a game in a file. Before the game starts, we ensure that all images listed in this file are fully loaded. Different games will each have their own associated file. The format of the file is very simple; it lists the file names, one file name per line. The preloadImages method accepts the name of the file to load.
CREATING A TILED IMAGE To draw backgrounds, it is often useful to take a small image and tile it. The ImageManager has a method—the createTileImage method shown in Listing 4.8—that does this. This method employs a very useful Java API method called createImage, which creates an image of a given size without an associated image file. You can then stuff whatever you want into the image. In this case, we stuff in as many copies of the tile image as we can fit into the newly created image. Another useful method, getGraphics, allows us to directly draw in the new image. Listing 4.8 A method to create a tiled image from a smaller image. Image createTiledImage(Image anImage, int w, int h) { Image image; image= theGame.createImage(w, h); for (int i= 0; i < w; i += anImage.getWidth(null)) { for (int j= 0; j< h; j += anImage.getHeight(null)) { image.getGraphics().drawImage(anImage, i,j, theGame); } } return image; }
SPECIAL EFFECTS One of the many advantages of using images for animation sequences is that we can use graphics tools to develop the effects for us. We can then snapshot the results and store them as a sequence of images. The end result is faster game loading. Typically, it is faster to load in the images than it is to calculate the effects at runtime. For instance, in Chapter 2, we used a sequence of images to rotate a spaceship. The sequence of images was generated by a graphics package rather than at runtime using Java. However, sometimes, it is useful to perform the effects at runtime. The ImageManager is the repository of runtime effects. We are going to look at one of the effects it can produce: actor fades. But first, some theory. IMAGE FILTERS If you want to do any manipulations on the pixels of an image, you will need to use an image filter. An image filter sits between an image producer and an image consumer, as shown in Figure 4.8.
Figure 4.8 How the ImageFilter process works. When the producer sends image data to the consumer, the filter intercepts the image data and modifies it before the consumer sees it. To use a filter, you use the following generic piece of code: ImageProducer src = anImage.getSource(); ImageFilter filter = new MyImageFilter(); ImageProducer producer = new FilterImageSource( src, filter);
Image newImage = createImage(producer); This code takes an image and turns it into an image producer. It creates a filter of the class MyImageFilter. The producer and the filter are then tied together to create another producer. This second producer produces the output from the filter. A new image is then created using the second producer. The image acts as the consumer for the producer. The net effect is that we take the source image, pass it through the filter, and create a new image from the result. Java supplies two standard filters: RGBFilterImage, which is the base class to use to implement RGB color filters, and CropImageFilter, which trims the dimensions of an image.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
FADING The scene is the Engine Room of the Starship Enterprise: The Dilithium crystals rapidly fade in and out—pulsating, ready to explode. Or, maybe the scene is a haunted-house: Ghosts materialize in front of you, then fade off as they move through walls. Whichever scene you choose, you will need to implement a fader. The fader is a simple manipulation of the Alpha values of the pixels of the image. The Alpha values, as I mentioned earlier in the chapter, are the control values that control how an image is merged with its background. Lowering the Alpha value of the pixels of an image makes the image appear to fade away; raising the Alpha values makes the image fade-up. Now that we know about image filters, changing the Alpha values becomes a simple matter of creating an image filter to change the Alpha values. Listing 4.9 shows AlphaFilter, an image filter that sets the Alpha values of an image to a given value. AlphaFilter is a descendant of RGBImageFilter, which provides access to the RGB and Alpha values of an image. Listing 4.9 A filter that creates a fading image. class AlphaFilter extends RGBImageFilter { int alpha; public AlphaFilter(int a) { alpha = a; canFilterIndexColorModel = true; } public int filterRGB(int x, int y, int rgb) { return ((rgb & 0x00ffffff) | (alpha java Test Derived Class aMethod called via Derived Class Derived Class aMethod called via Interface Derived Class aMethod called via Base Class
So, the good news is, you can share a name and, hence, promote consistency.
We have looked at events at a generic level, and we have considered how these event are distributed to the actors. Now, let’s look at how we can work with the events. We will be looking at how the actors will decode the events that they receive. We will also be adding some extra services to the default event handler.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
KEYBOARD EVENTS A keyboard event is generated when a key is pressed, and another one is generated when the key is released. The two event handlers for the keyboard are keyDown and keyUp. In addition to the physical movement of the keys, the keyDown is also generated when the keyboard auto-repeat mechanism kicks in. The keyDown is generated without any extra keyUp events between the keyDown events. Therefore, code that wishes to detect repeated keys should not look for keyUp events. If you are writing code to do keyboard work, and you want your Jackie Chan character to kick whenever the K key is pressed, for example, you would generally override the keyDown event handler as the basis for the keyboard detection mechanism. DECODING KEY VALUES The keyboard events keyDown and keyUp get the parameters of the event object and the key that was pressed. The key value itself is provided as a parameter to the keyDown and the keyUp events. It can also be found buried inside of the event object. The event object has a public variable called key, which is the key value. The key itself is given as an int, not a char, value. The key value is potentially a full Unicode value. Unicode means that the key value can be used to decode keys on large language keyboards, such as Chinese and Japanese Kanji keyboards. These keyboards can generate several thousand different key values. Because there are so many possible values for the key, the key value must be processed to decode what the key actually is. Listing 5.3 shows a simple actor, KeyTestActor, which will print the string value of a key to standard output. Listing 5.3 KeyTestActor.java: Decoding keyboard input. import java.awt.*; public class KeyTestActor extends Actor { String isActionKey(int key) { switch(key) { case Event.HOME: return "HOME"; case Event.END: return "END"; case Event.PGUP: return "PGUP"; case Event.PGDN: return "PGDN"; case Event.UP: return "UP"; case Event.DOWN: return "DOWN"; case Event.LEFT: return "LEFT"; case Event.RIGHT: return "RIGHT"; case Event.F1: return "F1"; case Event.F2: return "F2"; case Event.F3: return "F3"; case Event.F4: return "F4"; case Event.F5: return "F5"; case Event.F6: return "F6"; case Event.F7: return "F7"; case Event.F8: return "F8"; case Event.F9: return "F9"; case Event.F10: return "F10";
case Event.F11: return "F11"; case Event.F12: return "F12"; } return null; } String haveModifier(Event evt) { if (evt.shiftDown()) return "shift+"; if (evt.controlDown()) return "control+"; if (evt.metaDown()) return "meta+"; return null; } public boolean keyDown(Event evt, int key) { String str = "Key Down "; String s; if ((s= haveModifier(evt)) != null) str+= s; if (key == 0) str+="Key value of 0"; else if ((s=isActionKey(key)) != null) str+=s; else if (key >=32 && key =0; --i) { draggedActor = (Actor) picklist.elementAt(i); if (draggedActor.canDrag()) { draggedActor.startDrag(); originalx = draggedActor.getX(); originaly = draggedActor.getY(); dx = x - originalx; dy = y - originaly; dragging = true; theGame.stage.repaint(); return true; } } return false; }
Not satisfied that the actor was actually under the mouse, the drag-and-drop manager asks the actor if it can be dragged using the canDrag method. Note the default canDrag will return true. This may not seem to make sense if we are trying to write a generic framework for both board games (where, by default, you can drag actors) and arcade-style games (where, by default, you cannot drag actors). We appear to be unfairly biasing the framework for board games. However, this is not the case. For arcade games, we will not have a drag-and-drop manager installed. Therefore, the canDrag can return true because it is not called when the drag-and-drop manager is not installed. If the actor can be dragged, we call the actor’s startDrag operation. This is typically a no-op for most actors. It is just a place holder for those actors that want to do special effects during drags. For instance, a chess piece might run an animated sequence of fighting moves as it is dragged. The startDrag method can be overidden by Actor-derived classes to do these kinds of effects. The rest of the startDrag method is fairly straightforward. The dragging boolean variable is used to record that we are doing dragging. Some simple positional variables are recorded for helping with the move and for resetting the drag if it is canceled. Finally, the scene is repainted. THE MOVEMENT BASICS The movement stuff is very simple. We just take each mouseDrag event, set the new position of the actor, and repaint the scene: protected boolean dragTo(int x, int y) { if (dragging) { draggedActor.moveTo(x-dx, y-dy); theGame.stage.repaint(); return true; } return false; } A very slight subtlety comes from the position we move the actor to. We don’t move it to precisely the position given by the mouse. The actor’s moveTo method takes a point as a parameter and positions the anchor point of the actor at this point. The default anchor point, as implemented by the Actor base class, is the top-left of the actor. Using the default anchor point if we pick the actor at any place other than its anchor point, we will experience a jump when the actor is moved, as shown in Figure 6.4.
Figure 6.4 Jumping actor. Of course, a more desirable effect is to be able to pick the actor anywhere on its body and have the movement retain the position of the pick relative to the anchor point. Therefore, to eliminate this jump effect, we maintain the offsets from the anchor point of the actor to the position where the user picked the actor. These offsets are recorded in the variables dx and dy. Then, before we move the actor, we correct for the offset. This gives a nice smooth pick and move with no jumps.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
INTRODUCING DROP SITES Fine, we can move our actors around the screen, but where can we put them? We could code up each actor with a lot of knowledge about the layout of the screen. We could place knowledge of the board/game area layout in the actor and let it figure out if it could be dropped on the place we are about to drop it. As you can probably guess, with the number of “coulds” in the last couple of sentences, this is not the technique I prefer. A much better way is to use the concept of a drop site. The drag-and-drop manager can contain a list of potential drop sites. If the user tries to drop the actor on a drop site, then the dragand-drop manager asks the drop site if this is okay. If the user tries to drop the actor anywhere but at a drop site, the drag-and-drop manager will automatically stop the dragging operation and return the actor to the original starting place. One of the beauties of this arrangement is that a lot of the work is performed generically inside of the drag-and-drop manager. The actor does not specifically know anything about what is happening except that it is being moved around. This type of arrangement removes a lot of the linking between the actor and the drop site. The actor is relieved of the burden of managing the dragging and dropping. The drop site is equally clean. When the user attempts to drop an actor on the drop site, the drag-and-drop manager will ask the drop site if it will accept this actor. If it won’t, then the actor is again automatically returned to its starting spot. The drop site is not burdened with knowing where to return the actor. The drop sites are maintained as a Vector inside the drag-and-drop manager. There are two methods, addDropSite and removeDropSite, to manage the Vector: Vector dropSites = new Vector(); public void addDropSite(DropSite aDropSite) { dropSites.addElement(aDropSite); } public void removeDropSite(DropSite aDropSite) { dropSites.removeElement(aDropSite); } The drop sites themselves are just interfaces that must be implemented by objects that want to be drop sites. The interface requires two methods, containsPoint and acceptDrop. The containsPoint method determines if the mouse is in the drop site; the acceptDrop method determines if a drop of a given actor is okay: interface DropSite { boolean containsPoint(int x, int y); boolean acceptDrop(Actor a, int x, int y); } In this implementation of the drag-and-drop manager, drop sites are only checked at the drop phase. However, they could also be
checked at the drag phase to implement effects, such as highlighting, when an actor is over a drop site. DROPPING THE ACTOR The dragging is stopped by the stopDrag method, shown in Listing 6.4. Listing 6.4 Stopping the drag. protected boolean stopDrag(int x, int y) { boolean ret = false; if (dragging) { dragging = false; for (int i = dropSites.size()-1; i >= 0; --i) { DropSite ds = (DropSite) dropSites.elementAt(i); if (ds.containsPoint(x, y) && ds.acceptDrop(draggedActor, x-dx, y-dy)) { theGame.stage.repaint(); return true; } } draggedActor.stopDrag(); draggedActor.moveTo(x-dx, y-dy); theGame.stage.repaint(); } return false; } Each of the drop sites is examined to see if it contains the drop point. The order of examination is the reverse of the order of registration. In this way, later-registered drop sites that overlap existing drop sites will get preference. The checking is done by testing the current cursor point against the drop site. In addition to containing the drop point, the drop site must also accept the actor. If the drop site accepts the actor, the drop site will call the actor’s stopDrag method and position the actor accordingly. The drag-and-drop manager will force a repaint of the scene. If there is no drop site to accept the actor, the dragging is stopped and the actor is return to its original position. IMPLEMENTING THE DROPSITE INTERFACE The drop site is designed as an interface called DropSite. The interface concept is useful here because it allows us to use a number of different types of objects as drop sites. For instance, an actor might be a drop site, or an abstract object, such as an area, might be a drop site. Let’s look at a simple implementation of a DropSite interface. We’ll call it SimpleDropSite. SimpleDropSite creates a drop site that accepts any actor dropped on it. A typical use for SimpleDropSite is to act as a drop site for the entire stage. Once the SimpleDropSite is registered with the DragDropManager, any actor can be dropped anywhere on the stage. An example of this use might be in a Mahjong game. Listing 6.5 shows the SimpleDropSite code: Listing 6.5 A simple DropSite. class SimpleDropSite implements DropSite { Game theGame; Rectangle bbox; SimpleDropSite(Game aGame, int x, int y, int width, int height) { theGame = aGame; bbox = new Rectangle(x, y, width, height);
theGame.dragDropManager.addDropSite(this); } public boolean containsPoint(int x, int y) { return bbox.inside(x, y); } public boolean acceptDrop(Actor anActor, int ax, int ay) { anActor.moveTo(ax, ay); anActor.stopDrag(); return true; } } SimpleDropSite does the bare minimum to support a drop site. It sets up a bounding box that covers the area of the drop site and then registers itself with the drag-and-drop manager. The containsPoint method just checks the drop point against the drop site’s bounding box; acceptDrop moves the actor to the last known mouse position and terminates the drag.
Fancy Returns If the drop fails, the drag-and-drop manager does not have to blandly jump the actor to where it started from. It can do an automated slide back or some other form of fancy return to the original position. This creates a pleasing effect, especially for card games. It is a good feedback mechanism, also. We will see an example of a fancy return in the next chapter, which covers clocks.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
SMOOTH MOVEMENT The code we have written so far is fine except that it does not produce very smooth movement. In fact, the movement is so jerky, you might even say that it does’t work. Smooth movement can be achieved by implementing two concepts: • Fast redraw • Redrawing out of eye sight The first one is fairly obvious. The faster we can draw the actors, the faster the actor can be made to move, and, up to a point, the smoother the movement. We need to reach 25 to 30 redraws per second to fool the human eye into seeing smooth motion. The concept of redrawing out of eye sight is not so intuitive. Nearly all movement of an actor will require the actor being drawn on top of something. That something maybe the background, another actor, or other graphical artifact that we are drawing. As the actor moves, we need to repair the area where the actor just was. If we don’t, we get so-called “mouse droppings” left on the screen. The mouse droppings are missing areas of the object the actor was on. The problem with repairing the screen is that it leads to flicker. The eye sees the momentary redraw of the background, then it sees the actor being drawn back on top. You might think that you can get rid of this flicker by just redrawing the scene faster. However, this is generally not the case. The eye still picks up the flickering. The best way is to not let the eye see the repairs at all. DOUBLE-BUFFERING Double-buffering is a technique that allows you to complete scene repairs away from the visible screen. The technique is to use a hidden graphics area to do all the screen updates. The scene is redrawn into the hidden area, and then the hidden area is either rapidly drawn or switched to the visible screen. The effect is very much like a movie. The switch is just like a new frame of a movie. Compare this to the non-double-buffered scenario, which is like an animator quickly rubbing out bits of the old drawing and drawing the new bits in place. The concept of double-buffering is shown in Figure 6.5.
Figure 6.5 Updating the screen using double-buffering. To incorporate double-buffering into our framework, we only need to update the StageManager. Take a look at Listing 6.6. It shows the StageManager with double-buffering incorporated. Listing 6.6 StageManager with double-buffering. class StageManager extends Canvas {
protected Game theGame; private Image offscreenImage; private Graphics offscreenGC; public StageManager( Game aGame ) { theGame = aGame; offscreenImage = theGame.gameAdapter.createImage( size().width, size().height); offscreenGC = offscreenImage.getGraphics(); } Dimension size() { return theGame.stage.size(); } public void backdropChanged() { } public void paint( Graphics g) { offscreenGC.drawImage( theGame.backdropManager.getBackdrop(), 0, 0, theGame.stage); for (int i = 0; i = 0; --j) { Actor aj = (Actor) actors.elementAt(j); if (ai.zorder < aj.zorder) actors.setElementAt(aj, j+1); else break; } if (i != j+1) actors.setElementAt(ai, j+1);
} } Similarly, when we do a drag-and-drop pick to find out which actor the user wants to drag, we always try to find the highest Z-order actors. We start with the higher Z-order actors until we find an actor that matches the pick. In this way, we will always get the actor that was on top of another actor. It would appear very odd to the user if he picked a card, say, from a scattered deck of cards and got the card below the one he picked. Another concern when dragging is that the Z-order of the dragged actor should be higher than any other actor in the game. If this is not the case, the actor will not be drawn on top. Enter the concept of maximum Z-order. When we drag an actor, we temporarily boost its Z-order to the maximum value. Then, as we move it around the screen, it is drawn on top of all the other actors. When dragging is complete, we calculate a more realistic Z-order for the actor—typically one more than the highest Z-order of all the actors that it overlaps.
PUTTING IT ALL TOGETHER Now that we have seen a lot of the theory and covered how the framework part works, let’s see it from the other side of the coin. We’ll write a very simple card game in which the actors use the framework for the drag-and-drop we’ve covered. The game consists of three stacks of cards: a source stack and two target stacks—one target stack for red cards, the other target stack for black cards. The idea is to move the cards from the source stack and place them on the appropriate target stack as quickly as possible. The complete source code for the game is shown in Listing 6.9. Figure 6.8 shows the game in play.
Figure 6.8 Our simple card game.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Listing 6.9 Simple card game. class Card extends Actor { CardStack cardStack; Card(Game aGame, String aName, CardStack s) { this(aGame, aName, s.bbox.x, s.bbox.y); setCardStack(s); cardStack.addCard(this); } Card(Game aGame, String aName, int ax, int ay) { super(aGame); setImage("images/"+aName+".gif"); x = ax; y = ay; theGame.actorManager.addActor(this); } public void startDrag() { if (cardStack != null) { cardStack.removeCard(this); cardStack = null; } setFixed(false); zorder = MAXZORDER; theGame.actorManager.sortByZOrder(); } public void stopDrag() { zorder = 0; Vector v = theGame.actorManager.actorsOverlapped((int)x, (int) y, width, height); for (int i=0; i < v.size(); ++i) { Actor a = (Actor) v.elementAt(i); if (a != this && a.zorder > zorder) zorder = a.zorder +1; } setFixed(true); theGame.actorManager.sortByZOrder(); } public void setCardStack(CardStack s) { cardStack = s; } protected void createMovementManager() { } } class CardStack implements DropSite {
Game theGame; public Rectangle bbox; Stack cards = new Stack(); CardStack(Game aGame, int x, int y, int width, int height) { theGame = aGame; bbox = new Rectangle(x, y, width, height); theGame.dragDropManager.addDropSite(this); } public void addCard(Card aCard) { int zorder = 0; try { Card card = (Card) cards.peek(); card.draggable = false; card.visible = false; zorder = card.zorder + 1; } catch (EmptyStackException e) {} cards.push(aCard); aCard.moveTo(bbox.x, bbox.y); aCard.setCardStack(this); aCard.zorder = zorder; } public void removeCard(Card aCard) { if (cards.peek() == aCard) { cards.pop(); aCard.zorder = Card.MAXZORDER; } try { Card card = (Card) cards.peek(); card.draggable = true; card.visible = true; } catch (EmptyStackException e) {} } public boolean containsPoint(int x, int y) { return bbox.inside(x, y); } public boolean acceptDrop(Actor anActor, int ax, int ay) { // make sure the actor is a card Card aCard; try { aCard = (Card) anActor; } catch (ClassCastException e) { return false; } // don't allow drop if card already on the stack if (cards.search(aCard) != -1) { return false; } addCard(aCard); aCard.stopDrag(); return true; } } class CardGame extends Game { String cards[] = {"a","2","3","4","5","6","7","8",
"9","t","j","q","k"}; String suits[] = {"c", "h", "d", "s"}; public void init() { super.init(); backdropManager.setTiled("images/n002.gif"); // weak attempt at shuffling the pack int c[] = NewMath.randomCompleteSequence(13); int s[] = NewMath.randomCompleteSequence(4); CardStack srcStack = new CardStack(this, 10, 10, 73, 97); for (int i = 0; i < cards.length; ++i) { for (int j = 0; j < suits.length; ++j) { new Card(this, cards[c[i]]+suits[s[j]], srcStack); } } CardStack redStack = new CardStack(this, 10, 120, 73, 97); new Card(this, "ad", redStack); CardStack blkStack = new CardStack(this, 100, 120, 73, 97); new Card(this, "as", blkStack); } public void createActorManager() { actorManager = new ActorManager(this, 52); } public void createClock() { // don't need a clock for this app } } The three user classes we have written are Card, CardStack, and CardGame. The Card is the fundamental play card actor. The card can be dragged between the stacks. When the drag is started, the card removes itself from the stack (if it is in one) and boosts its Z-order. When the card is dropped, the card will recalculate a decent Zorder for itself, based on the Z-orders of the cards it is sitting on. The CardStack is a generic card stack. The current implementation stacks the cards directly one on top of the other. A more useful card stack would be able to stack the cards in a variety of fashions: partially showing cards, vertical and horizontal orientations, etc. The card stack implements the drop site interface. This allows cards to be dragged on to the drop site. The CardGame class is a specific class for this game. It is a derived class of the generic Game class. We have developed the beginnings of a simple card game framework—an extension to the game framework that will allow us to rapidly write card games. In Chapter 12, Steve will be developing these humble beginnings into a more useful framework.
SUMMARY Starting with static actors, we have introduced the concept of movement into the framework. First off, we looked at drag-and-drop. We introduced the drag-and-drop manager and drop sites. Using them, we can now move actors around the stage under user direction. Unfortunately, even humble ol’ drag-and-drop brought drawing performance into the spotlight. We tackled the performance issue and improved it using double-buffering, visibility, and fixed actor management. We then looked at Z-order to work out which actors are moving across the front of the stage, and finally wrote a simple card game showing all the ideas we have developed in this chapter. As a bonus, we witnessed the birth of the card game framework that will be developed in Chapter 12.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 7 CLOCKS NEIL BARTLETT
A
h, how time flies when you are having fun. But what’s that ticking? Tick, Tick, Tick. No, it’s not a bomb; it is the heartbeat of
the framework—the Clock class—ticking away. A clock is a very useful addition to a game, as we will soon see. The clock can be used to help us implement a number of effects from a straightforward elapsed time display through to animating and pausing a game.
INTRODUCING THE CLOCK The basic timing utility in the framework is the Clock class. The Clock class, which is a very simple class, uses a thread to deliver a regular heartbeat. The heartbeat ticks at a frequency determined by the clock. We want to use the clock object for a variety of purposes. To prevent us from hard-coding the clock for one purpose, we implement clock watchers. These are objects that are registered with the clock. Every time the clock ticks, it notifies each of its registered clock watchers by calling the clock watcher’s tick method. The message interaction diagram for this activity is shown in Figure 7.1.
Figure 7.1 Clock message interaction diagram. The beauty of this approach is that we can use one clock to deliver timing ticks to any number of objects. Each object implements the tick method and registers itself with the clock at runtime. Then, every time the clock ticks, the clock watcher is told about it.
IMPLEMENTING THE CLOCK CLASS The complete Clock class is shown in Listing 7.1. The clock is implemented as a thread. When the clock is started, the thread is created. The thread sleeps for a period of time determined by the variable sleepTime. Whenever the clock wakes, it calls the tick method of each of the clock watchers. Calling each of the tick methods will take a finite amount of time. If we don’t try to correct for this time, the timing of the clock ticks will be dependent on the amount of work done in the tick methods. Ideally, we want the clock to be ticking at a regular beat. To ensure this, we only sleep for sleepTime less the amount of time spent in the tick methods. Unless, the total time spent in the tick methods is greater than sleepTime, this correction will ensure a regular ticking of the clock. Listing 7.1 The Clock and ClockWatcher classes. interface ClockWatcher { void tick( Clock c ); }
class Clock implements Runnable { Thread ticker=null; int sleepTime; public long currentTickTime=0; public long lastTickTime=0; public long startTickTime=0; public long tickCount=0; Vector cws = new Vector(); Clock(int t) { sleepTime = t; } void addClockWatcher( ClockWatcher c) { cws.addElement(c); } void removeClockWatcher( ClockWatcher c) { cws.removeElement(c); } public void start() { if (ticker == null) { ticker = new Thread( this ); } ticker.start(); startTickTime = lastTickTime = currentTickTime = System.currentTimeMillis(); } public void stop() { if (ticker != null) { ticker.stop(); } ticker = null; } public void run() { while (ticker != null) { currentTickTime = System.currentTimeMillis(); tickCount++; for (int i = 0; i < cws.size(); ++i) { ClockWatcher c= (ClockWatcher) cws.elementAt(i); c.tick( this ); } lastTickTime = currentTickTime; long timeLeft = sleepTime-System.currentTimeMillis() +currentTickTime; if (timeLeft > 0) { try { Thread.sleep( timeLeft ); } catch( InterruptedException e) {} } } ticker = null; } long timeSinceLastTick() { return currentTickTime - lastTickTime; }
long getTickCount() { return tickCount; } } The clock provides a public method, setSleepTime, to set the value of the sleepTime variable. This method is declared as synchronized so that access to changing the sleepTime time variable is strictly sequential.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
HOW MANY CLOCKS? Using the Clock and ClockWatcher concept, we can add as many clock watchers as we want to a single clock. The clock will notify each watcher on each tick of the clock. So it makes sense to use a single, central clock for our Game class, then register each watcher with the central clock. Good idea, right? Not really. We can do this, but we might hit a performance wall. The problem is one of granularity. Suppose we are using the clock to run some movie speed animation. To do this, we need the clock to tick at 30 clicks per second or faster. In other words, we use a delay of approximately 50 milliseconds between each clock tick. Now, suppose we also want to update a fancy clock and play a tick-tock sound once every second. We could use one clock to do this. We could add watchers for both the animation and for the fancy clock, but we would be carrying the overhead of ensuring that the ticking for the fancy clock did not eat up too much time from the movie engine. For instance, when the clock ticks, our animation might stick while the fancy clock is updated. A better approach is to use two clocks—that is, two separate threads—and let the Java thread-management mechanism take care of the problem for us. The thread-management system incorporates true time-slicing mechanisms to interleave the processing. This leads to a smoother, more even interleaving of the animation and the fancy clock.
DEFAULT GAME CLOCK An important feature of a framework is to do a lot of useful things by default, so that the user of the framework does not have to specifically program stuff to happen. In other words, the framework assumes things on behalf of the user. If the framework is well designed, the assumptions will be good, and the end-user will be happy. The ultimate example of this is the proverbial one-line power station: One line of code runs an entire nuclear power station. To make good on the framework promise, the Game class instantiates a default clock object. By default, the Game class will create a clock that ticks at 30 ticks per second. The clock is created by the clock factory method, createClock. This factory method creates a single default clock. The clock is started when the game is started. Listing 7.2 is an excerpt from the Game class showing all the stuff appropriate to the default clock. Listing 7.2 Default clock support in the Game class. class Game extends Panel implements ClockWatcher { Clock clock=null; boolean pendingStart= false; void init() { createClock(); } public void start() { if (clock != null) { clock.start(); } else { pendingStart = true; } }
public void stop() { if (clock != null) { clock.stop(); } } protected void createClock() { clock = new Clock(40); clock.addClockWatcher(this); if (pendingStart) { start(); } } public void tick() { actorManager.tick(); stage.repaint(); } }
TIP: Too Many clock Variables Spoil The Soup Be very careful not to use the variable name clock for one of the secondary clocks in your Game-derived class. The problem is that the clock variable in the derived class will obscure the clock variable stored in the Game class. This will lead to some obscure timing bugs.
CHANGING THE CLOCK SPEED A very useful function is to change the speed of the clock. This technique can be used, for example, to speed up the rate of game play to make the game more difficult. By using a very large sleep time, such as Long.MAX_VALUE, we can effectively stop the clock. This is useful as one method of implementing a pause function. We can register a key with the event manager as the pause key then just call the setSleepTime on the master game clock to pause the game.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
MORE ON CLOCK WATCHERS The clock itself is very simple. The interesting stuff comes from what we do on each heartbeat. Our clock watchers can be very useful, unlike their real-world counterparts who do little but sit and idly wile away time. Using clock watchers, we can provide some very useful, built-in features for our framework. A TIME ELAPSED LABEL A useful feature for many games is to display the time elapsed. The inclusion of a visible clock often provides an added intensity to the game. The game player feels the pressure to beat the clock. For example, some people I know can quote their best time at Windows Solitaire. It’s a matter of pride. We’ll supply a user visible clock to our framework. The simplest approach is to use a label to display the time. This is the purpose of ClockLabel. A simplified version is shown in Listing 7.3. Listing 7.3 A clock label. class ClockLabel extends Label implements ClockWatcher { String prefix;
Clock clock;
ClockLabel(Clock c, String p) { super(p+"----", Label.LEFT); clock = c; clock.addClockWatcher(this); prefix = p; setTime(); } public void tick() { setTime(); } void setTime() { long t = (clock.currentTickTime - clock.startTickTime)/1000; Long l = new Long(t); setText(prefix+l.toString()+"
");
} } The ClockLabel is derived from the standard AWT Label class. It provides a label that displays the number of seconds since the clock it is watching was started. Figure 7.2 shows the ClockLabel in action.
Figure 7.2 The ClockLabel keeps track of the seconds elapsed. A COUNTDOWN TIMER You’ve never felt real pressure until the villain in a game leaves you in a room with a ticking bomb, and you have to find your way out before it explodes. Watching the seconds elapse until you’re blown to bits can certainly take its toll. To keep your players on the edge of their seats, you might find it helpful to implement a countdown timer. The CountdownTimer, shown in Listing 7.4, will call its finished method when the timer has counted down an appropriate number of milliseconds. Listing 7.4 The Countdown Timer. class CountdownTimer implements ClockWatcher { Game theGame; long startTime; long expireTime; Clock clock; CountdownTimer(Game aGame, Clock aClock, int aTime) { theGame = aGame; expireTime = aTime; clock = aClock; startTime = aClock.currentTickTime; aClock.addClockWatcher(this); } public void tick() { if ((clock.currentTickTime - startTime) > expireTime) finished(); } public void finished() { } } By subclassing CountdownTimer, we can achieve all manner of elapsed time effects. One idea that comes to mind is to introduce actors at random intervals. To do this, you would need to create a countdown timer to countdown a random number of milliseconds. When the timer finishes, it calls the finished method, which then creates an actor. The timer can be restarted by creating another CountdownTimer object. In fact, a typical game might include a number of countdown timers attached to a single clock. Each timer might be counting down to a particular event: introduction of another actor, explosion of a bomb, increase in level difficulty, or, even, the end of the game. You can use a simple variant of the ClockLabel to track the countdown timer and feedback a display of the current countdown time to the user. FANCY DRAG-AND-DROP RETURNS In the last chapter, I promised to show you how to write a fancy return animator for drag-and-drops. And I never go back on my promises. The animator I’m going to show you will return an actor back to its original location if it is not dropped on a valid drop site. This animator, shown in Listing 7.5, moves the actor linearly from the position it was dropped back to its original location. The
animator class is called Mover. Listing 7.5 Fancy drag-and-drop mover. class Mover implements ClockWatcher { Game theGame; Actor actor; int x, y; int x1, y1; int dx, dy; int MOVETIME=300; int NUMPOINTS=10; Clock clock; int count; Mover(Game aGame, Actor anActor, int ax, int ay, int ax1, int ay1) { theGame = aGame; actor = anActor; x = ax; y = ay; dx = (x-ax1)/NUMPOINTS; dy = (y-ay1)/NUMPOINTS; x1 = x-NUMPOINTS*dx; y1 = y-NUMPOINTS*dy; count = NUMPOINTS; clock = new Clock(this, MOVETIME/NUMPOINTS); clock.start(); } public void tick() { x1 = (Math.abs(x-x1) r.x+r.width) actor.x = r.x; if (actor.y > r.y+r.height) actor.y = r.y; return actor.x == x && actor.y == y; } } All boundary classes conform to the BoundaryHandler interface, shown here: interface BoundaryHandler { boolean check(Actor a); } The actor creates the boundary classes in its constructor by calling the factory method createBoundaryHandler. By default, a WrapRectBoundary object is created with the boundary set to the size of the stage. There are plenty more boundary classes that we
can create. For example, GameWorks also provides BounceRectBoundary, which bounces the actor off the rectangular boundary.
COLLISION DETECTION With all these actors moving around, some of them are bound to bump into each other. The essence of most action games is to blow up the baddies. This generally involves a projectile from some modern weapon of destruction making contact with the body of an alien nasty. Collision detection is how we are going to detect when two (or more) actors bump into each other. Collision detection is not as straightforward as it might sound. The problem is one of performance (just like virtually every other aspect of game programming, for that matter). First of all, to make sure that all collisions are detected, each actor must check for collisions with every other actor. For 10 actors, that works out to 90 comparisons (each actor must check itself against 9 other actors). This is not too bad. A very manageable number. Now how do we test for an actor-to-actor collision? There are many techniques. Let’s look at two: pixel comparison and boundingbox comparison. These two techniques are illustrated in Figure 8.1.
Figure 8.1 Detecting collisions. Pixel comparison works by comparing each pixel of one actor with each pixel of the other actor. If any pixels overlap then the actors have hit each other. Putting aside how we would test if two individual pixels overlapped, the pixel comparison technique is very costly. For two actors each 20 pixels by 20 pixels big, the total number of pixel tests would be 160000 (each actor has 400 pixels, and each pixel of each actor must be compared). And remember, this is just for one comparison of actors. For our 10-actor scenario that works out to 14,400,000 comparisons. If you consider that an if statement takes, say, 1 microsecond to execute, then just for ifs alone, this technique would cost 14 seconds of time. This is hardly in keeping with a redraw rate of 25 times a second or one redraw every 40 milliseconds. Bounding-box comparison is much more efficient. What we’re testing for here is when the boxes (or rectangles) that make up the individual actors touch. For each actor test, this technique performs four checks. Therefore, our 10-actor case only requires that 360 tests are performed. Now, that’s more like it. (For more information, see the AWT Rectangle class’s intersects method.)
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
IMPLEMENTING BOUNDING-BOX COLLISION DETECTION Because we are using handlers for everything, we will once again be using handlers to detect collisions. We will see why in a moment. The collision handler interface is: interface CollisionDetector { void detectCollisions(); } The collision detection is initiated by the ActorManager. After the ActorManager has told each of the actors to move, it will call the collision detector to determine if a collision has occurred. When a collision occurs, the collision detector calls the hit methods of the two actors that collided. The actors can then do whatever they like inside their hit method. For instance, if a missile hits a bad guy, the bad guy and the missile will be destroyed, an explosion will be created, and the score will be incremented accordingly. Equally, if the actors should not collide (a spaceship colliding with its own missiles, for instance), then the hit method will do nothing.
Java Perks: Improving The Bounding-Box Detection The bounding-box detection technique will cause false readings because not all actors are actually rectangular. If the actors are close to rectangular, the player will not notice, especially in the heat of action. However, if the actors do not fit snugly inside their bounding boxes, the player will notice. In these situations, we can further refine the bounding-box technique. These refinements kick in once bounding-box overlap has been found. One refinement is to add pixel comparison (this time there is good reason to suspect that the actors overlap, so the extra work is worth it). Another refinement involves adding extra bounding boxes to divide up the actor into smaller chunks.
The code for the bounding-box collision detector is shown in Listing 8.4. It contains two loops: the outer loop, controlled by the i variable, loops through all of the actors; the inner loop, controlled by the j variable, compares each actor with every other actor. The intersects method from the AWT Rectangle class is used to do the comparison. Listing 8.4 A bounding-box helps detect collisions. class BBCollisionDetector implements CollisionDetector { Game theGame; ActorManager actorManager; BBCollisionDetector(Game aGame, ActorManager anActorManager) { theGame = aGame; actorManager = anActorManager; } public void detectCollisions() { int size = actorManager.numActors(); for (int i = 0; i < size; ++i) { Actor a1 = (Actor) actorManager.actor(i); for (int j = 0; j < size; ++j) { Actor a2 = (Actor) actorManager.actor(j); if (a1 != a2 &&
a1.getBoundingBox().intersects(a2.getBoundingBox())) { a1.hit(a2); } } } } } Collision detectors are created in the Game class using the createCollisionDetector method. We can implement other forms of collision detection, as well. A very popular form is the circular bounding-box. In this technique, a circle is used to represent a bounding-box. If the centers of two bounding circles are within two circle radii of each other, then the circles overlap. The test is easily performed by determining the x and y distance between the two centers and then using Pythagoras’ Theorem to determine if they are less than two radii. This technique is very useful for ball-based games, such as Pool.
ACTORMANAGER UPGRADES We have mentioned the humble ActorManager several times in this chapter: its tick method and its support for the collision detection. Let’s take a look at the ActorManager itself, shown in Listing 8.5. Most of it is a straightforward implementation. However, you will notice two Vectors—newActors and deadActors—have been added since we last saw the code in Chapter 3. When an actor is added or removed from the list, the ActorManager does not change the master list immediately. The master list is only changed at the beginning of the tick method. This is to allow collision hits to remove actors, while still allowing the removed actors to affect other collision hits during the current collision detection. For instance, when a missile collides with two bad guys, you don’t want the bullet removed from the master list before it has had a chance to hit both of the bad guys. Listing 8.5 The ActorManager with support for collision detection. class ActorManager protected Game private Vector private Vector private Vector
{ theGame; actors = new Vector(); newActors = new Vector(); deadActors = new Vector();
public ActorManager(Game aGame) { theGame = aGame; } public void addActor(Actor anActor) { newActors.addElement(anActor); } public void removeActor(Actor anActor) { deadActors.addElement(anActor); } public void removeAllActors() { newActors.removeAllElements(); deadActors.removeAllElements(); actors.removeAllElements(); } public int numActors() { return actors.size(); } public Actor actor(int i) { return (Actor)actors.elementAt(i);} public void tick() { for (int i = 0; i < newActors.size(); i++) actors.addElement(newActors.elementAt(i)); newActors.removeAllElements(); for (int i = 0; i < deadActors.size(); i++) actors.removeElement(deadActors.elementAt(i)); deadActors.removeAllElements(); for (int i = 0; i < actors.size(); i++) {
Actor a = (Actor) actors.elementAt(i); a.tick(); } if (theGame.detector != null) theGame.detector.detectCollisions(); } }
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
MOVEMENT PERFORMANCE REVISITED All this movement has caused a real headache. The game is moving very slowly. We need to look at movement performance again. Is there any more we can squeeze out of the graphics sponge? I hope so. What exactly is the problem? Well, plain and simple, we are doing too much drawing. We are now clocking screen redraws 25 times a second. Let’s re-examine the drawing process to see if there any improvements we can make. You might recall from Chapter 6 that we are using double-buffering to smooth out our drawing to the screen. Double-buffering, in case you forgot, hides the messy graphics updates from the users eye, giving the illusion of smooth movement. Double-buffering first copies each actor into a hidden buffer. Then, finally, copies the entire hidden buffer to the screen. This last step is hurting us. In a typical circumstance, we are probably drawing at least 10 times more than we need to. Why? Take a look at Figure 8.2. It gives you an idea of the amount of the hidden buffer that needs copying to the screen compared to the actual amount that is copied to the screen. As you can see, the area that needs updating is much smaller than the area which we are actually updating. We need to figure out a way to reduce the amount of updating we are doing.
Figure 8.2 There is too much drawing to the screen. TALK DIRTY TO ME I have laid out the problem. I’m not going to leave you high and dry without a solution, am I? Of course not. Our solution comes in the form of dirty rectangles. A dirty rectangle is an area of the screen that needs to be updated. For instance, if an actor moves from one position to another, both the area where the actor was and the area where the actor is now must be redrawn. The bounding box where the actor was, and the bounding box where the actor is now are both dirty rectangles. Figure 8.3 shows the dirty rectangles for Figure 8.2.
Figure 8.3 Hanging out the dirty rectangles.
The upshot is this: If we record the dirty rectangles that change on each clock tick, we know the exact areas of the screen that need to be refreshed. We can draw each of the actors into the hidden buffer, recording the dirty rectangles as we go. Once the hidden buffer is complete, we draw the hidden buffer to the screen. For each dirty rectangle, we clip the drawing to the dirty rectangle and draw the hidden buffer. Hold on. How is this a speed up? We are now drawing the hidden buffer to the screen a whole lot more times: once for each dirty rectangle. Well true, but the crucial point is how many pixels get drawn on the screen. Only those pixels inside the dirty rectangles are being drawn. Modern graphics cards and graphics engines are very good at clipping away stuff that is not drawn to the screen. Clipping is the process of restricting drawing to an area inside a rectangle known as the clipping rectangle. We can draw huge amounts of graphics, but as long as the clipping rectangle is small, the performance will be good. So drawing the hidden buffer a number of times is not really a problem. Of course, if we draw it too many times, performance will begin to suffer. There are two more optimizations we ought to consider: eliminating overlapping rectangles and merging adjacent rectangles. The elimination of overlapping rectangles is easy to understand. Take a look at Figure 8.4. Notice that there are two dirty rectangles here: one for the erase of the original actor (we want to replace this with the background) and one for the drawing of the actor in its new location. The point is that these two positions most likely overlap. We are drawing the area of the overlap twice. And this means real pixels are being drawn twice: They are not clipped away.
Figure 8.4 Overlapped erase and draw areas. The merging of adjacent rectangles is more subtle. Drawing a rectangle carries some overhead on the part of the graphics card—the more drawing, the more overhead. Where possible, we should minimize the number of draws, but we must be careful not to do this at the expense of drawing more pixels. Sound like a contradiction? It is. We must keep the number of rectangle draws low and the number of pixels drawn low. The technique we will use is to merge together dirty rectangles that are close to each other into a single, larger dirty rectangle. The definition of “close” is a matter of discretion. Overlapping rectangles classify as close, so this technique will work for that situation, as well.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
IMPLEMENTING DIRTY RECTANGLES Dirty rectangle management is essentially a screen optimization. We don’t want to go burdening our actors with problems of screen management, so we should not place code for this stuff in the actors. It makes good sense to centralize the work into a single class that is independent of the actors: the DirtyRectangleManager. The DirtyRectangleManager will retain a list of rectangles that have changed on the screen. Each time an actor paints to the screen, the StageManager will get the actor’s bounding box and add that to the DirtyRectangleManager. When it comes time to draw the hidden buffer to the screen, the DirtyRectangleManager will do the drawing. The complete code for the DirtyRectangleManager is shown in Listing 8.6. Listing 8.6 The DirtyRectangleManager comes to clean up. public class DirtyRectangleManager { private Vector rectangles; final int OVERSIZE = 64; public DirtyRectangleManager() { rectangles = new Vector(); } public DirtyRectangleManager(int i) { rectangles=new Vector(i);} public int size() { return rectangles.size();} public void reset() { rectangles.removeAllElements();} final private boolean merge(Rectangle r1, Rectangle r2) { boolean result; r1.width += OVERSIZE; r1.height += OVERSIZE; r2.width += OVERSIZE; r2.height += OVERSIZE; result = r1.intersects(r2); r1.width -= OVERSIZE; r1.height -= OVERSIZE; r2.width -= OVERSIZE; r2.height -= OVERSIZE; return result; } public void addRectangle(Rectangle ar) { Rectangle r=new Rectangle(ar.x, ar.y, ar.width, ar.height); for (int i = 0; i < rectangles.size(); ++i) { Rectangle r1 = (Rectangle) rectangles.elementAt(i); if (merge(r, r1)) { r = r.union(r1); rectangles.removeElementAt(i); } } rectangles.addElement(r); } public void drawImage(Graphics ag, Image anImage,
ImageObserver anObserver) { for (int i = 0; i < rectangles.size (); ++i) { Rectangle r = (Rectangle) rectangles.elementAt(i); Graphics g = ag.create(r.x, r.y, r.width, r.height); g.drawImage(anImage, -r.x, -r.y, anObserver); g.dispose(); //DBG ag.drawRect(r.x+1, r.y+1, r.width-2, r.height-2); } } }
TIP: Checking Your Work Notice the commented line: //DBG ag.drawRect(r.x+1, r.y+1, r.width-2, r.height-2); It has been deliberately left in the code. If the line is uncommented to be /*DBG*/ ag.drawRect(r.x+1, r.y+1, r.width-2, r.height-2); the inner workings of the DirtyRectangleManager will be drawn on screen. This is useful for checking that the DirtyRectangleManager is working correctly.
USING DIRTY RECTANGLES My description in the last section of how the DirtyRectangleManager works was a gross simplification. I implied that the dirty rectangle management can be handled by one dirty rectangle manager. Well it can, but it is better to use two. Let me explain. First, firmly fix in your mind that the aim is to draw only those areas of the screen that have changed. For each actor in a paint of the stage, we want to: • Erase the actor from where it was located • Draw the actor in its new location We are using dirty rectangles to do this, so let’s rephrase this in terms of dirty rectangles. For a repaint of the stage, we want to draw: • The dirty rectangles for an actor’s old location • The dirty rectangles for an actor’s new location Imagine we are using a single dirty rectangle manager to do this. We start off with a blank stage with no actors on it. We are starting from ground zero. In the first paint of the stage, we will place one actor on the stage. Successive paints will draw the actor in different positions as it moves around the stage. Let’s look at each paint in detail: • First Paint—The dirty rectangle manager contains no rectangles. We draw the actor to the hidden buffer and record the dirty rectangle. We then draw the hidden buffer, clipping to the dirty rectangle. This works fine. It draws the actor on the stage. • Second Paint—The dirty rectangle manager contains the rectangle from the previous position. We draw the actor to the hidden buffer and record the current dirty rectangle. The dirty rectangle manager now contains the rectangle from the previous position and the rectangle from the current position. We then draw the hidden buffer, clipping to the dirty rectangles. This works fine. It repairs the stage where the actor was last paint and draws the actor where it is this paint. • Third Paint—This is where things go wrong. The dirty rectangle manager contains the rectangle from the two previous positions. We draw the actor to the hidden buffer and record the current dirty rectangle. The dirty rectangle manager now contains the rectangle from the two previous positions and the rectangle from the current position. We then draw the
hidden buffer, clipping to the dirty rectangles. This repairs the stage where the actor was last paint and draws the actor where it is this paint. However, it also does extra work redrawing the stage where the actor was two paints ago. If we carry on like this, I think you can see that the dirty rectangle manager will become cluttered with redundant dirty rectangles. In other words, we are painting the stage more than is necessary. The question is how do we get rid of the redundant rectangles? We could work out a scheme of tagging the rectangles and deleting them as they become redundant, but this would mean a lot of work searching through the dirty rectangle manager for redundant rectangles. Fortunately, there is a better way: use two dirty rectangle managers.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
The idea is simple. We use one dirty rectangle manager, we’ll call it DR-A, to do the drawing. It records the dirty rectangles for the current paint. We use a second dirty rectangle manager, DR-B, to record the dirty rectangles from the previous paint. At the start of a paint, we clean out DR-A so that it contains no dirty rectangles. DR-B contains the dirty rectangles from the previous paint. When we draw the current rectangles, we record them in DR-A. Just prior to drawing the hidden buffer, we copy the dirty rectangles from DR-B to DR-A, so DR-A contains both the dirty rectangles from the last paint and the dirty rectangles from this paint. We paint the hidden buffer using DR-A. Prior to the next paint, we clean out DR-A and make sure that DR-B contains the rectangles from this current paint (ready to become the redraw rectangles for the next paint). DR-A is now clean and DR-B has the old dirty rectangles ready to be loaded into DR-A. Notice this was the same state we came into the paint with, so we now have a repeatable technique which does not accumulate redundant dirty rectangles. We will add one more twist. To help us smooth out some of the fine details, we will use a swapping technique to transfer the dirty rectangles from DR-A to DR-B. Let’s look at the first three paints again: • First Paint—DR-A and DR-B contain no rectangles. We draw the actor to the hidden buffer and record the dirty rectangle in both DR-A and DR-B. We then draw the hidden buffer, clipping to the dirty rectangle in DR-A. This works fine. It draws the actor on the stage. • Second Paint—We swap DR-A and DR-B and clean out DR-B. DR-A now contains the dirty rectangle from the previous position. DR-B contains nothing. We draw the actor to the hidden buffer and record the current dirty rectangle in DR-A and DR-B. DR-A now contains the rectangle from the previous position and the rectangle from the current position; DR-B contains only the rectangle from the current position. We then draw the hidden buffer, clipping to the dirty rectangles in DR-A. This works fine. It repairs the stage where the actor was last paint and draws the actor where it is this paint. • Third Paint—We swap DR-A and DR-B and clean out DR-B. DR-A now contains the dirty rectangle from the previous position. DR-B contains nothing. We draw the actor to the hidden buffer and record the current dirty rectangle in DR-A and DR-B. DR-A now contains the rectangle from the previous position and the rectangle from the current position; DR-B contains only the rectangle from the current position. We then draw the hidden buffer, clipping to the dirty rectangles in DR-A. This works fine. It repairs the stage where the actor was last paint and draws the actor where it is this paint. I hope that you noticed that the second and third repaints were identical. By swapping DR-A and DR-B and cleaning out DR-B, we can repeat the repaints ad infinitum and we won’t accumulate redundant dirty rectangles. Figure 8.5 shows the technique. The StageManager class implements this swapping technique as shown in Listing 8.7.
Figure 8.5 A dirty rectangle manager tag team. Listing 8.7 Upgrading the StageManager to support dirty rectangles. class StageManager extends Canvas{ protected DirtyRectangleManager dirtyRects; protected DirtyRectangleManager drawRects; public Rectangle bgRect; public StageManager( Game aGame ) { dirtyRects = new DirtyRectangleManager(50);
drawRects
= new DirtyRectangleManager(dirtyRects.size());
} public void init() { int w = size().width; int h = size().height; bgRect = new Rectangle(0, 0, w, h); dirtyRects.addRectangle(bgRect); } public void backdropChanged() { bgRect = new Rectangle(0, 0, size().width, size().height); dirtyRects.addRectangle(bgRect); } protected void updateFixedActors() { int numActors = theGame.actorManager.numActors(); if (redoFixed) { dirtyRects.addRectangle(bgRect); theGame.backdropManager.paint(fixedActorsGC); for (int i = 0; i < numActors; ++i) { Actor actor = theGame.actorManager.actor(i); if (actor.visible && actor.fixed) actor.paint(fixedActorsGC); } redoFixed = false; } dirtyRects.drawImage(offscreenGC, fixedActorsImage, this); } protected void swapDirtyRectangleManagers() { DirtyRectangleManager tmp; tmp = drawRects; drawRects = dirtyRects; dirtyRects = tmp; dirtyRects.reset(); } public void paint(Graphics g) { int numActors = theGame.actorManager.numActors(); updateFixedActors(); swapDirtyRectangleManagers(); for (int i = 0; i < numActors; ++i) { Actor actor = theGame.actorManager.actor(i); if (actor.visible && !actor.fixed) { Rectangle r = actor.getBoundingBox(); dirtyRects.addRectangle(r); drawRects.addRectangle(r); actor.paint(offscreenGC); } } drawRects.drawImage(g, offscreenImage, this); } }
SUMMARY A transformation has occurred. Our actors are now sprites, complete with the ability to move autonomously around the stage and to collide with each other. Actors use handlers to provide a flexible approach to implementing algorithms for movement, boundary checking, and collision detection. Of course, all of these additions made for a slow-running game. To correct for this problem, we implemented dirty rectangle management to get better performance.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 9 HIGH SCORING NEIL BARTLETT
A
h, the element of competition. The thrill of seeing your name up in lights. The proud moment when your name is placed
highest on the list of all high scorers. There can be only one; the excitement is trying to be that person. Of course, the thing that makes all this possible is scoring. Without scoring, we can’t add that competitive edge to our game. The game framework is designed to support scoring. It can’t help you with designing an exciting scoring system for your game, but it can help you display the score and record the score on a high score table. This chapter will examine the framework’s scoring support. In addition, we’ll look at how to design a high score server. A server that sits out there somewhere on the Net, waiting, ready to record high scores or provide an eager player with the top scores of a favorite game.
A SCORE MANAGER The first thing we have to do is to track the current score. We’ll create a score manager for doing this stuff. The score manager is designed on the by-now-familiar model/view concept. You give your scores to the score manager (the model), and it tells all the interested parties (the views) that the score has changed. The views then get the current score from the model and do their thing with it. Compare this concept with the Clock and the ClockWatcher classes from Chapter 7, and I’m sure you will see more than a passing resemblance. Of course, the views do not have to be simply things that display the score. They might be listeners that make the game more difficult when a certain score is reached, or they might be score multipliers that respond when a particular combination of events occurs, such as the bonus system in a game of pinball. The design of the score manager should be familiar. We have the usual suspects at play. There is the ScoreManager class, which does the scoring work, and the Observer interface, which is told when the score has changed. Things that are interested in scores will implement the Observer interface. A souped-up message interaction diagram is shown in Figure 9.1. It shows a label and a network connection that are both interested in knowing the current score.
Figure 9.1 The ScoreManager message interaction diagram. IMPLEMENTING THE SCORE MANAGER
I have used the Java-supplied Observer and Observable classes to implement the score manager system. We have rather neglected these very useful classes in the framework. I decided to use the Java classes for the score manager class, ScoreManager. ScoreManager is not derived from anything else, so deriving it from Observable won’t cause us any problems. The neat thing about Observable is that it handles all the registration stuff for you. We don’t need to include a vector or provide add and remove methods. This makes the ScoreManager a rather short piece of code, as you can see by looking at Listing 9.1. Listing 9.1 The ScoreManager. class ScoreManager extends Observable { int total; ScoreManager(Game aGame) { theGame = aGame; reset(); } public void set(int score) { total = score; setChanged(); notifyObservers(); } public void add(int score) { total += score; setChanged(); notifyObservers(); } public void reset() { set(0); } public int getScore() { return total;
}
} As you would expect, the ScoreManager is instantiated in the Game class, using the factory mechanism that we use for other managers. The factory method is called createScoreManager. It simply instantiates a ScoreManager object. Games that don’t want score management can override the createScoreManager method to do nothing.
A SIMPLE SCORE LABEL Because we are using the Observable class for the ScoreManager, we only need to implement the Observer interface on the classes that are interested in the scores. Listing 9.2 shows the ScoreLabel class, which is used to display the score. Listing 9.2 The ScoreLabel class. class ScoreLabel extends Label implements Observer { String prefix; ScoreLabel(ScoreManager s, String p) { super(p+"----", Label.LEFT); prefix = p; s.addObserver(this); } public void update(Observable o, Object arg) { ScoreManager sm = (ScoreManager) o; setText(prefix+ sm.getScore()+" "); } }
This is a very simple piece of code. Games that want to display the score create a ScoreLabel class and place it somewhere on the game panel. Because displaying scores is a virtual necessity, I decided to make the default Game class do this for all games.
A WEB HIGH SCORE SERVER Tracking a player’s score is fine. The element of competition, though, comes when that score is ranked among the high scores of others. Of course, you always run into the rare few who do it “because it was there,” but ultimately, most people want to know how they fared against their fellow humans or, increasingly, their silicon opponents. Recording a player’s score is a very common need in virtually every game. Even for games that don’t produce an actual score (headto-head board games such as chess and Abalone), it is useful to record who beat whom. It is often possible—by applying ranking rules—to determine a score for each player by calculation. Most traditional computer games record the high scores on your computer’s hard drive. The high score code is generally fairly simple in concept: read in the high score table from disk, check if the player’s score is higher than any of the existing scores, and, if it is, write a new file out to disk. Obviously, with a Web game, this is more difficult. Though the game code may have been read off of a Web or a game server on a single machine in the network, the game code is actually running on any number of machines in any number of locations across the world. If we store a high score table on a player’s local hard disk drive, it will only be the high score table for players on that machine. Hardly a useful high score table. No, we need to implement a high score server that will allow players from around the globe to record their high scores in one central location.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
WHAT WILL THE SERVER DO? Obviously, the high score server will store and rank high scores. But wait, there’s more. Here is a list of other very useful characteristics that I have come up with: • The server should be independent of any game. • Adding a score to the high score table should be secure. • The server should support useful user reports. Let’s define each of these points more clearly. INDEPENDENT OF A GAME The high score server has a very well-defined job. The job is applicable to a wide range of games. The amount of useful information stored about a game will probably not be very much: a top-10 high score table for a game will only use a few kilobytes of disk space storage. It makes sense, then, that a single server be used to store the high scores of a number of games. The vision is that there will be a small number of high score servers on the Web. These would each support a large number of games. SECURE SCORES Just as being able to place a score on a high score table is a great incentive to play and do better at a game, being able to trust the high score table is equally important. There is not much fun in slogging away for a couple of hours at your favorite game to beat the high score, only to have some hacker bozo place “Garth was here 100 billion billion points” on the high score table ahead of you. This kind of Internet graffiti defeats the whole point of playing. We must stop Garth the Bozo from getting his 15 minutes of fame on the cheap. The high score server will only accept legitimate scores. (My apologies if your name is Garth, but if it’s “Garth the Bozo,” boy, do you need to change your name!) USEFUL USER REPORTS Although we want adding high scores to be trustworthy, we want players and other interested parties to be able to query the high scores of their favorite games. Getting this type of information should not be privileged. Any player should be able to get the complete high score table for any game that the server supports.
DESIGNING THE HIGH SCORE SERVER Now that we have a reasonable idea of what our high score server is all about, let’s peel another layer off the onion and look at a more detailed design. SERVER CONFIGURATION The high score server will be a client/server socket design. Commands will be sent from the client to the server using sockets. We
will use a multithreaded design for the server; the server will be able to process the commands from a number of clients at once. The threaded design is shown in Figure 9.2. Each of the square boxes with timer symbols in the corner is a thread.
Figure 9.2 The server threaded design. The server will have one thread that accepts a connection from a client. When this thread receives a connection, it will start a secondary thread which will process the commands from the client. The original thread goes back to waiting for more clients—potentially creating further secondary threads while the original secondary thread is still running. The secondary threads will take the commands from their client and process them. There will be yet another thread that manages the high score table itself. The secondary threads will talk to the high score thread to update the high score table. The high score thread will synchronize access to the high score table so that two threads can’t access it at the same time. The high score thread will also refresh the stored high score table to some storage—a file on the server machine, for example—when the high score table has changed. COMMANDS Take a look at Table 9.1. It shows the list of commands the server will support. Admittedly there are not many, but they will let us run the high score server and do all we want. Clients can add and list games, and they can add and list scores in a game. For each command, the client-side protocol works through a chain of events: connect to the server, send the command, wait for the acknowledgment, and disconnect from the server. The server, meanwhile, will receive the command, process it, send back the result, and finish the connection. Table 9.1Table of commands.
Action
Command
Parameters
Add game
addgame
game, encryption key
List all games
listgames
Add score
addscore
game, encrypt(player, score, email)
List a high score table
listhighscores
game
The commands will potentially take parameters. These also are shown in Table 9.1. The tilde character is used to separate the commands and the various parameters, which allows the parameter to contain normal punctuation characters (spaces, commas, hyphens, etc.) without upsetting the command and parameter recognition code in the server. For example, a typical addgame command might be: addgame~Neil's Great Java Quake Clone~7Jer8h-49/02 The server can receive this and correctly determine the name of the game (Neil’s Great Java Quake Clone) and the encryption key (7Jer8h-49/02). The brackets in the encrypt(player, score, e-mail)
parameter of the addscore command show that the parameter is a single encrypted string which, when decrypted, reveals the player’s name, the score, and the player’s email address. The email address is a gratuitous addition. If you are being beaten on a network game high score table, you can simply challenge a guy above you by sending the guy email. Just the thing for a pre-game Ali-Frazier-style war of words. We will be talking more about encryption in a moment.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
THE HIGH SCORE DATABASE The high score database will record the information for a number of games. It will record the name and encryption key of each game, as well as a list of ranked scores. Each score will have the player’s name, score, and email address. I have opted not to use a full database. I figure that, for the moment, a full Java database is not worthwhile. It would add to the overall cost of the system without providing any appreciable benefit. The database for the high score server will be a plain ol’ ASCII file. The format for the file will be: <encryption key> <score1> <e-mail1> <score2> <e-mail2> … <encryption key> <score1> <e-mail1> <score2> <e-mail2> … This format has the advantage of being easy to read. The main overhead will be in coding the reading and saving the file. HIGH SCORE TABLE SECURITY Security is what is going to stop Garth the Bozo from hacking our high score table and putting up false high scores. How are we going to do this? Well, this would not be a modern book on a Web topic without mentioning the “E” word—more formally know as encryption—would it? Take a look at Figure 9.3, which sums up how things will work. Both the game and the high score table will each know a hidden key value. The hidden key will be generated by the game programmer and transferred to the high score server when the game is registered. The high score server provides the addgame command to register a game and its encryption key online. For the truly paranoid, because game addition will not be that frequent a thing, the registration could be done via the guy who operates the server.
Figure 9.3 Encrypting the high score. When the game wants to add a high score to the system, it encrypts the player’s name, score, and email address (with appropriate separators) into a single string. It then sends the game name and this string as part of the addscore command. The high score server can decrypt the string on the basis of the hidden encryption key which is stored with the game in the high score server.
We won’t go into the actual encryption method here. Encryption algorithms are not something I want to write down. They are covered by all manner of International Law. Unfortunately, I have a very poor understanding of this law. Here I am, in Canada, writing a book for a U.S. company who has world-wide distribution. What encryption technology can I write about? After all, my bible for encryption algorithms (Applied Cryptography by Bruce Schneier from John Wiley & Sons) contains some rather worrisome legalese on the inside front cover and appears to be published only in the U.S. and Canada. So, to relieve my publisher from hiring a high-priced lawyer, I have not given any code for the encryption. I have only written dummy encryption methods, which act as placeholders for the real encryption algorithm. Obviously, this takes some of the wind out of my sails, but it keeps me out of jail! I will point you, however, to Systemics Ltd. (http://www.systemics.com/) who has implemented, in Java, all the very best encryption algorithms. The algorithms are provided for free and come with export restriction notices. The package, called Cryptix, is a superb piece of work. Well worth a look.
How Secure Is This? Of course, this is still not the complete solution to trustworthy high-scores. We have a secure high score server, but we have given away the crown jewels with the client, the game. The game contains the high score server key, and it is vulnerable to attack. If someone can reverse engineer the game Java byte stream, then they can crack the high score server. So ultimately, this is only one step better than having a high score with no security. We have only succeeded in upping the skill level required to hack the high score server. This is only a problem for non-server based games. For server based games, the high score is maintained by a trustworthy central agent—the server.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
IMPLEMENTING THE HIGH SCORE SERVER Now that we have covered the design, let’s implement the high score server. We are going to do a detailed and rather complete look at the high score server. Before moving on, you should be sure that you understand the concepts of port and socket from the Java documentation. For more information, you could always check out our previous book, The Java Progamming EXplorer, also from Coriolis. THE GENERIC SERVER PART The high score server is based on a simple server package I have put together, so we’ll start with that. The server is a threaded server. It runs in a thread that accepts incoming requests. It comes in three parts: server, server thread, and input processor. Listing 9.3 shows each of the three parts. They are embodied in three classes: Server, ServerThread, and ServerInputProcessor. The Server class lays the groundwork for the socket. It creates a server socket on an appropriate port, then issues the accept call to wait for clients to connect to the port. Whenever a client knocks on the door of the port, an IOException is thrown. The Server class creates a new ServerThread object, starts it, and then goes back to waiting for more clients to connect. The ServerThread object turns the client socket connection into an input stream and an output stream. It then creates an input processing object to handle the input, and then passes the input from the client one line at a time to the input processor, until the input processor says to stop processing. The ServerInputProcessor is the input handler. It is very simple. It takes the input from the client, echoes it to standard output, and stops the input processing. Listing 9.3 Generic server code. public class Server { int port= 4321; void setPort(String[] args) { for (int i=0; i < args.length; ++i) if (args[i].equals("-p")) try port = Integer.parseInt(args[++i]); catch (ArrayIndexOutOfBoundsException e){} catch (NumberFormatException e) {} } ServerThread createServerThread(Socket clientSocket) { return new ServerThread(clientSocket); } void start(String[] args) { ServerSocket serverSocket = null; boolean listening = true; setPort(args);
try serverSocket = new ServerSocket(port); catch (IOException e) { System.err.println(e.getMessage()); System.exit(1); } while (listening) { Socket clientSocket = null; try clientSocket = serverSocket.accept(); catch (IOException e) { System.err.println(e.getMessage()); continue; } createServerThread(clientSocket).start(); } try serverSocket.close(); catch (IOException e) {System.err.println(e.getMessage());} } public static void main(String[] args) { (new Server()).start(args); } } class ServerThread extends Thread { Socket socket = null; ServerThread(Socket socket) { super("ServerThread"); this.socket = socket; } ServerInputProcessor createInputProcessor() { return new ServerInputProcessor(); } public void run() { try { DataInputStream is = new DataInputStream( new BufferedInputStream(socket.getInputStream())); PrintStream os = new PrintStream( new BufferedOutputStream(socket.getOutputStream(), 1024), false); ServerInputProcessor ip = createInputProcessor(); String inputLine; while ((inputLine = is.readLine()) != null) { os.println( ip.processInput(inputLine); os.flush(); if (ip.endProcessing()) break; } os.close(); is.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } class ServerInputProcessor { String processInput(String anInput) { System.out.println(anInput); return anInput; }
boolean endProcessing() { return true; } } The server classes are all concrete: You can create them. They don’t do much, though. To use them effectively, you need to implement a server using them. The high score server is shown in Listing 9.4. The thing to notice is that there is no manipulation of sockets in the high score server code (providing you turn a blind eye to the references forced on you by Java). All the socket work has been done in the generic server classes. The high score server can restrict itself to the task at hand: reading streams of data. The first two classes, HighScoreServer and HighScoreServerThread, are little more than instantiation classes that ensure the HighScoreInputProcessor class is created. HighScoreInputProcessor class does all the real work. It takes the input stream, chops it up into commands using the StringTokenizer class, and calls the appropriate methods in the high score table manipulation code.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Listing 9.4 The high score server. public class HighScoreServer extends Server { HighScoreServer(String[] args) { hs = new HighScoreRecorder(args[0]); start(args); } ServerThread createServerThread(Socket clientSocket) { return new HighScoreServerThread(clientSocket); } public static void main(String[] args) { new HighScoreServer(args); } } class HighScoreServerThread extends ServerThread { HighScoreServerThread(Socket s) { super(s); } ServerInputProcessor createInputProcessor() { return new HighScoreServerInputProcessor(); } } class HighScoreServerInputProcessor extends ServerInputProcessor { String processInput(String anInput) { try { StringTokenizer s= new StringTokenizer( anInput, "~") ; String cmd = s.nextToken(); if (cmd.equalsIgnoreCase("addgame")) { String game = s.nextToken(); String key = s.nextToken(); return HighScoreRecorder.inst().addGame(game, key); } else if (cmd.equalsIgnoreCase("listgames")) { return HighScoreRecorder.inst().listGames(); } else if (cmd.equalsIgnoreCase("addscore")) { String game = s.nextToken(); String code = s.nextToken(); return HighScoreRecorder.inst().addScore(game,code); } else if (cmd.equalsIgnoreCase("listscores")) { String game = s.nextToken(); return HighScoreRecorder.inst().listScores(game); } } catch (NoSuchElementException e) {} catch (NullPointerException e) {} return "error"; } } HIGH SCORE PROCESSING
Consider what we have achieved so far. The client side wants to save the high score. The client side has taken its high score, converted it into a command and a string, and squirted it across the network to the server. The server has taken the string and chopped it up into the original command. If you think about it, all this work has been done just to do the next step: call a method to record the score if it is a high score. If this had not been a server program, we could have made one call, and we would have been done. What is the call that the client would have made directly? Well, that is provided by the addScore method of the HighScoreRecorder class, shown in Listing 9.5. The HighScoreRecorder class runs the thread that synchronizes access to the high score data and periodically saves it to disk. It provides the four methods—addGame, listGames, addScore, and listscores—that correspond to the commands that the server supports. Listing 9.5 The HighScoreRecorder class. class HighScoreRecorder implements Runnable { static HighScoreRecorder singleton=null; HighScoreStats highScores; boolean refresh=false; String filename; Thread ticker=null; HighScoreRecorder(String aFilename) { filename = aFilename; singleton = this; highScores = new HighScoreStats(filename); } static HighScoreRecorder inst() { return singleton; } synchronized String addGame(String game, String key) { refresh = highScores.addGame(game, key); return ""+refresh; } synchronized String listGames() { return highScores.listGames(); } synchronized String addScore(String aGame, String aCode) { return ""+highScores.addScore(aGame, aCode); } synchronized String listScores(String aGame) { return highScores.listScores(aGame); } public void refresh() { if (refresh) highScores.output(filename); } public void start() { if (ticker == null) ticker.start(); } public void stop() { if (ticker != null) ticker = null; }
ticker = new Thread( this );
ticker.stop();
public void run() { while (ticker != null) { try Thread.sleep( 10000 ); catch( InterruptedException e) {} refresh(); } ticker = null; } } The HighScoreRecord does not do the work itself. It calls upon three other separate classes to manage the data on its behalf: HighScoreStats, GameStats, and Score. HighScoreStats manages the high scores for all games, GameStats manages the scores for a single game, and Score manages a single score. These three classes are shown in Listing 9.6. To read and save the high score data to the high score file, the three management classes are designed to support serialization. Serialization is the process of taking an object and converting it into a streamed format and vice versa. Basically, this just means saving all the data in the object as a single string, or taking a single string and creating an object out of it. If you look at the constructors for each of the classes, you will see that they each read in some part of a file. When taken as a whole, HighScoreStats will read the number of games from the file then create a GameStats object for each game. Each GameStats object reads the number of scores for its game and creates a Score object for each score. The Score object reads in a single score. This whole process in shown in Figure 9.4.
Figure 9.4 Serialized high score. The reverse process is writing the data out to the file. Each of the management classes supports a method called output that will take the data in the class and write it to the file. The GameStats class, in Listing 9.6, decodes the encryption of the addscore command with the decrypt method. I have given a nonencrypted implementation of this. It just returns the string. Note that the encrypted string uses the vertical bar (|) character, rather than the tilde character, to separate the parts of the string. In this way, the non-encrypted version does not give false parameter breaks when it is parsed by HighScoreInputProcessor.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Listing 9.6 The high score management classes. class HighScoreStats { Vector gameStats = new Vector(); HighScoreStats(String file) { try { DataInputStream f = new DataInputStream( new FileInputStream(file)); int numGames = Integer.parseInt(f.readLine()); for (int i=0; i < numGames; ++i) gameStats.addElement(new GameStats(f)); } catch (NumberFormatException e) { } catch (FileNotFoundException e) { } catch (IOException e) {} } boolean addGame(String game, String key) { gameStats.addElement( new GameStats(game, key)); return true; } String listGames() { String s = new String(); for (int i=0; i < gameStats.size(); ++i) { GameStats gs = (GameStats) gameStats.elementAt(i); s += "~"+gs.getName(); } return s; } long addScore(String aGame, String aCodedString) { for (int i=0; i < gameStats.size(); ++i) { GameStats gs = (GameStats) gameStats.elementAt(i); if (gs.getName().equals(aGame)) return gs.addScore(aCodedString); } return 0; } String listScores(String aGame) { for (int i=0; i < gameStats.size(); ++i) { GameStats gs = (GameStats) gameStats.elementAt(i); if (gs.getName().equals(aGame)) return gs.listScores(); } return ""; } void output(String file) { try { DataOutputStream f = new DataOutputStream(
new FileOutputStream(file)); f.writeBytes(gameStats.size()+"\n"); for (int i=0; i < gameStats.size(); ++i) { GameStats gs = (GameStats) gameStats.elementAt(i); gs.output(f); } f.flush(); } catch (NumberFormatException e) { } catch (FileNotFoundException e) { } catch (IOException e) {} } } class GameStats { String name, key; Vector scores = new Vector(); GameStats(DataInputStream d) { try { StringTokenizer s=new StringTokenizer(d.readLine(), "~"); name = s.nextToken(); key = s.nextToken(); int numScores = Integer.parseInt(s.nextToken()); for (int i=0; i < numScores; ++i) scores.addElement(new Score(d)); } catch (NumberFormatException e) {} catch (NullPointerException e) {} catch (IOException e) {} } GameStats(String aName, String aKey) { name = aName; key = aKey; } String getName() { return name; } String decrypt(String aCode, String aKey) { return aCode; } long addScore(String aCodedString) { String scoreString = decrypt(aCodedString, key); if (scoreString.equals("")) { return -1;} StringTokenizer s = new StringTokenizer(scoreString, "^"); String aName = s.nextToken(); if (!aName.equals(name)) {return -1; } String aPlayer = s.nextToken(); String aScore = s.nextToken(); String aEmail = s.nextToken(); return addScore(aPlayer, aScore, aEmail); } long addScore(String ap, String as, String ae) { long score = Long.parseLong(as); for (int i=0; i < scores.size(); ++i) { Score s = (Score) scores.elementAt(i); if (score > s.getScore()) { scores.insertElementAt(new Score(ap, as, ae), i); return i+1; } } scores.addElement(new Score(ap, as, ae)); return scores.size(); }
String listScores() { String hs= scores.size()+"\n"; for (int i=0; i < scores.size(); ++i) { Score s = (Score) scores.elementAt(i); hs += s.getScoreLine(); } return hs; } void output(DataOutputStream ds) { try { ds.writeBytes(name+"~"+key+"~"+scores.size()+"\n"); for (int i=0; i < scores.size(); ++i) { Score s = (Score) scores.elementAt(i); s.output(ds); } } catch (IOException e) {} } } class Score { String name, email; long score; Score(DataInputStream d) { try { StringTokenizer s=new StringTokenizer(d.readLine(), "~"); name = s.nextToken(); score = Long.parseLong(s.nextToken()); email = s.nextToken(); } catch (NumberFormatException e) {} catch (NullPointerException e) {} catch (IOException e) {} } Score(String aName, String aScore, String aEmail) { name = aName; score = Long.parseLong(aScore); email = aEmail; } String getScoreLine() { return (name+"~"+score+"~"+email+"\n");} long getScore() { return score; } void output(DataOutputStream ds) { try ds.writeBytes(name+"~"+score+"~"+email+"\n"); catch (IOException e) {} } }
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
IMPLEMENTING THE CLIENT For the client code, I’m only going to show how adding a score will work. The client code is relatively simple. It just packages up the score in an addScore format and sends it over to the server. I have added the client code, shown in Listing 9.7, to the ScoreManager. Listing 9.7 The ScoreManager client. String encrypt(String s, String key) { return s; } int recordScore(InetAddress server, int port) { Socket s = null; PrintStream os = null; DataInputStream is = null; int position=0; try { s = new Socket(server, port); os = new PrintStream(s.getOutputStream()); is = new DataInputStream(s.getInputStream()); String m = encrypt(theGame.name+"^"+ theGame.playerName+"|"+ total+"|"+"^", theGame.highScoreServerKey); os.println("addScore~"+theGame.name+"~"+m); os.flush(); try position = Integer.parseInt(is.readLine()); catch (NumberFormatException e) {} os.close(); is.close(); s.close(); } catch (UnknownHostException e) { System.err.println(e.getMessage()); } catch (IOException e) { System.err.println(e.getMessage()); } return position; }
OTHER USEFUL FEATURES No piece of software is ever truly complete, but the high score server still has a number of useful features we might want to add. WEB USER INTERFACE Some of the features, such as getting a listing of a high score and getting a list of games, are not just tied directly to playing the game. It should also be possible to access this information from a Web page.
USER-DEFINABLE HIGH SCORE CALCULATION We briefly touched on this topic when we were talking about what the high score server should do. The idea is that a game’s programmer can upload a class to the server to aid with the high score calculation. The class would be registered against the game so only the given game would get the new calculation. This would allow chess ranking systems and other high scoring systems to be implemented without having to code them up-front into the high score server. To accommodate this feature, the high score calculations would have to be accessed via an interface. The user-defined class would have to conform to that interface. The server code would instantiate the class named in the game database when it needed to calculate a high score ranking. AUTOMATIC EMAIL Players that have done well on the high scores are probably very interested in your game. If they get bumped down the high score list, then they might appreciate being told so that they can have a chance to redress the balance. The high score table records the email addresses of the players, so it will be easy to send an automated email message whenever a user is bumped down a notch or two.
SUMMARY Let’s face it. Winning is everything—at least when it comes to games. Your players deserve to know where they rank in relation to other players. Keeping score in your games should be a priority. Hopefully, this chapter has helped you to see how easy it is to implement a scoring system, both locally and on the Web.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 10 THE HILLS ARE ALIVE: SOUNDS IN JAVA GAMES STEVE SIMKIN
A
game without sound effects is, well, silent. Most games can be enhanced by the judicious use of sounds. Luckily, Java has a
few methods to help you add all the crashes, squawks, and kabooms that you want to your creations. In this chapter, we’ll explore these methods, but let’s get some unhappy business out of the way first. It is my sad duty to inform you that, as of this writing, audio support in Java is extremely limited. There are two constraints on playing sounds in Java that have direct implications for your work as a game developer. First, the methods for loading sound files are offered only by instances of the Applet and AppletContext classes. Not only does this prevent you from playing sounds in Java applications (which may not particularly bother you), but, as we’ll see later in the chapter, it also complicates the design of audio methods for your game applets. The second constraint on playing sounds in Java is that the only audio file format currently supported by the JDK is u-law encoded, 8-bit mono, 8,000 kHz AU format—a format that originated at Sun Microsystems and NeXT Software. Javasoft has announced its intention to release a comprehensive media API later this year. This API will include support for MIDI and other audio formats. To get the latest announcement on Java’s media API plans, check out the Java Media Framework API Overview (http://java.sun.com/ products/apiOverview.html). Until the folks at Javasoft give us more options, we’ll just have to work with AU files.
TIP: What About My Great WAV Files? If you have files in WAV or any other audio format, you can convert them to AU format with a really nifty tool called GoldWave, written by Chris S. Craig. Last time I checked, GoldWave came in a beta version for Windows 95 and a fully functional release for Windows 3.1. If you go for the 3.1 release, make sure you get version 3.03, not 3.02. GoldWave converts audio files of different formats and has lots of neat editing features. You can download GoldWave from http:// web.cs.mun.ca/~chris3/goldwave.
So much for what you can’t do. Now, let’s see what you can do. With all the limitations, Java gives you just enough audio functionality to let you put sounds where you need ‘em in your Internet games.
PLAYING SOUNDS IN JAVA There are two steps to playing sounds in Java. The first step is to load AU files into memory in the form of AudioClip objects. AudioClip (in the java.applet package—they had to put it somewhere) is the interface that describes the essential methods for playing audio files. The second step in playing sounds in Java is to use one of AudioClip’s methods to play the loaded AudioClip object. Sound1.java, shown in Listing 10.1, demonstrates these two steps. It describes an applet with a single button. When a user presses the button, the applet plays a file called gong.au. Listing 10.1 Sound1.java.
import java.applet.*; import java.awt.*; class Sound1 extends Applet { Button play; AudioClip ac; public void init() { play = new Button("Play"); add(play); ac = getAudioClip(getCodeBase(), "gong.au"); } public boolean action(Event event, Object arg) { if (event.target == play) { ac.play(); return true; } return false; } } At its simplest, that’s all there is to it. The following line, from the init method, handles the first step, loading the AU file into an AudioClip object: ac = getAudioClip(getCodeBase(), "gong.au"); In its most common usage, getAudioClip accepts two arguments: a base URL and the location of the sound file relative to the base URL. Normally, you’ll use the getCodeBase method to supply the base URL. The getCodeBase method returns the URL from which your .class files were loaded. There is a second method for returning a URL, getDocumentBase. This method returns the URL from which the HTML page was loaded. It is used to allow site administrators to pass audio files as arguments to applets. Because you will probably want control over which sounds your game plays, you should use getCodeBase. When the user triggers an action event by pressing the Play button, the following AudioClip method plays the sound described by gong.au: ac.play(); LOOPING, LOOPING,… AudioClip has two more methods, whose use is demonstrated in Sound2.java. This program, shown in Listing 10.2, enhances Sound1.java by adding sound loop functionality. Listing 10.2 Sound2.java. import java.applet.*; import java.awt.*; class Sound2 extends Applet { boolean looping = false; Button play; Button loop; Button stop; AudioClip ac1, ac2; public void init() { play = new Button("Play"); add(play); loop = new Button("Loop"); add(loop);
stop = new Button("Stop"); stop.disable(); add(stop); ac1 = getAudioClip(getCodeBase(), "gong.au"); ac2 = getAudioClip(getCodeBase(), "drip.au"); } public void start() { if (looping) ac2.loop(); } public void stop() { if (looping) ac2.stop(); } public boolean action(Event if (event.target == play) ac1.play(); return true; } if (event.target == loop) ac2.loop(); loop.disable(); stop.enable(); looping = true; return true; } if (event.target == stop) ac2.stop(); loop.enable(); stop.disable(); looping = false; return true; } return false; }
event, Object arg) { {
{
{
}
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Together, the loop and stop methods control repeated playing of the audio clip. At first glance, it seems simple enough. But why, then, does adding loop functionality double the size of the program? Because of a quirk in Java’s implementation of the loop method. The thread in your application that calls the AudioClip play method doesn’t actually play the sound. Instead, it passes a “please play this sound” request to a JVM system thread called audio. Once the audio system thread receives the request, playing the sound is no longer under the jurisdiction of your applet—it belongs to the system. Consequently, once a sound clip starts looping, it continues to loop—until explicitly stopped—for as long as the browser is running, even if the user leaves the Web page containing the applet that initiated the loop! Therefore, good etiquette dictates that you stop the loop whenever your applet is suspended. The place to put the code that does this is in the applet’s stop method. You may remember that the Applet class has methods called start and stop, which are called by the browser whenever it decides to suspend or resume the applet, usually when it is scrolled in or out of view. You can override these methods to implement whatever behavior you feel should be suspended and resumed along with the applet. In Sound2.java, I use a boolean variable called looping to keep track of whether the audio clip is looping.
Java Perks: Beware Of The NoisyBear Surprisingly, looping AudioClips are front and center in discussions of Java security. The Hostile Applets home page (http://www.math.gatech.edu/~mladue/HostileApplets.html) features an applet called NoisyBear, which simply displays a picture of a bear and launches an extremely annoying, never-ending audio clip of a beating drum. There is no way to stop the drum beat short of closing your browser. NoisyBear is not a traditional security threat. It doesn’t erase files or broadcast your credit card number over the Internet. But it does monopolize your system’s resources in an unwelcome way. At its most extreme, an applet of this sort can take total control of your CPU, leaving you with no choice but to reboot and lose your unsaved work. This sort of hostile takeover of system resources is known as a denial of service attack. It prevents people from using their own computers. Using what you’ve just learned about playing sounds in Java, you will quickly realize that the NoisyBear applet requires only a few lines of code. NoisyBear’s behavior is more mischievous than malicious. But it does exemplify the kind of rudeness that turns off many people (especially system administrators) to Java. Remember that surfers could unintentionally link to a Web site featuring NoisyBear. The bear would invade their computer entirely uninvited. The moral: When designing your Net applications, please be considerate of users who may not want you to invade their systems.
PERFORMANCE CONSIDERATIONS: MULTITHREADING SOUNDS Obviously, class files and images must be fully downloaded before play can begin on your game—not necessarily so with audio files. If you’re planning to equip your game with a rich set of sound effects, Java provides two techniques that allow play to proceed smoothly with no interference from audio files in transit. First, you can move your getAudioClip calls to low priority threads. These calls will then run in the background while your main thread is busy initiating the game. You may object that doing this raises the possibility that the game will want to play a sound before the sound’s AudioClip object has actually been created. Your objection conveniently brings me to the second technique for allowing play to proceed while audio files load in the background: an AudioClipRegistry class. AudioClipRegistry offers both loading and playing services. It starts a new thread for each load request it receives and registers each AudioClip object as it is loaded. When it receives a play request, it checks the register for the requested file. If the file is already loaded, AudioClipRegistry calls the AudioClip object’s play method. If, on the other hand, the file has not finished loading, AudioClipRegistry graciously returns without doing anything. Listing 10.3 shows the code for the AudioClipRegistry class and its companion, AudioClipLoader. Listing 10.3 Sound3.java.
class AudioClipRegistry extends Hashtable { Applet applet; public AudioClipRegistry(Applet applet) { this.applet = applet; } public void loadAudioClip(URL url, String filename) { new AudioClipLoader(applet, this, url, filename); } public void registerAudioClip(String filename, AudioClip ac) { put(filename, ac); } public void playAudioClip(String filename){ AudioClip ac = (AudioClip) get(filename); if (ac != null) ac.play(); } } class AudioClipLoader extends Thread { Applet applet; AudioClipRegistry acr; URL url; String filename; public AudioClipLoader(Applet applet, AudioClipRegistry acr, URL url, String filename) { this.applet = applet; this.acr = acr; this.url = url; this.filename = filename; setPriority(MIN_PRIORITY); start(); } public void run() { AudioClip ac = applet.getAudioClip(url, filename); acr.registerAudioClip(filename, ac); } } Let’s take a close look at these two classes. The AudioClipRegistry class extends Hashtable. Hashtable allows objects of any class to be stored and retrieved by association with a String key. In our case, the objective is to store AudioClip objects as they are loaded, using their audio file names as the associated keys. You may be wondering why the constructor for AudioClipRegistry accepts an applet as an argument. If you remember, in the introduction to this chapter, I said that getAudioClip is an Applet instance method. This means that if I want my applet to delegate responsibility for loading an AudioClip to another class, it has to hand that class a reference to itself to use when calling getAudioClip. On the assumption that AudioClipRegistry will serve only one applet, I pass the reference in the constructor.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
AudioClipRegistry doesn’t call getAudioClip itself. Instead, it makes use of yet another class, AudioClipLoader. The reason for this is to allow each AudioClip to be loaded in a separate thread. Therefore, AudioClipRegistry’s loadAudioClip method merely acts as a middleman, creating a new AudioClipLoader thread for each audio file to be loaded, and handing off all its arguments to the new thread’s constructor method. The AudioClipLoader class extends Thread. It adds its own set of instance variables whose values are needed by its run method. The AudioClipLoader constructor method accepts these values as arguments. After assigning them to the instance variables, it also takes the opportunity to use Thread’s setPriority method to assign itself a low priority. Like AudioClipRegistry, the AudioClipLoader constructor accepts one argument whose purpose may not be immediately apparent. The AudioClipRegistry passes a reference to itself to AudioClipLoader, which the latter uses in its run method. After it finishes loading the AudioClip, AudioClipLoader uses the reference to call the AudioClipRegistry’s registerAudioClip method. This method accepts a String and an AudioClip as arguments, and simply passes them to Hashtable’s put method. So far, we’ve launched a thread for each audio file we need to load. And we’ve seen that as each file is successfully loaded, its thread registers the resulting AudioClip object with the AudioClipRegistry. But how can our applet actually play the sound? By calling AudioClipRegistry’s playAudioClip method. The playAudioClip method accepts the audio file name as an argument. It uses this name as the key to call Hashtable’s get method. If the requested AudioClip has already been loaded and registered, the get method extracts it, and the sound can be played. If the AudioClip has not yet been loaded and registered, the get method returns a null, and playAudioClip shrugs its shoulders and ends quietly. The final result of these levels of indirection is that sounds get loaded in the background without delaying your game. They are available for playing as soon as they’re loaded, but they won’t hang the game when they’re not. Extending AudioClipRegistry to accommodate looping is a simple matter, so is tagging selected sounds as necessary to play. Rather than step you through the implementation of these extensions, I’ll leave them as exercises for you. Listing 10.4 shows the code for the Sound3 class. It is an adaptation of Sound1.java, with the appropriate substitutions to make use of AudioClipRegistry’s services. Listing 10.4 The Sound3 class. class Sound3 extends Applet { boolean looping = false; Button play; Button loop; Button stop; String gongFilename = "gong.au"; AudioClipRegistry acr; public void init() { play = new Button("Play"); add(play); acr = new AudioClipRegistry(this); acr.loadAudioClip(getCodeBase(), gongFilename); } public boolean action(Event event, Object arg) { if (event.target == play) {
acr.playAudioClip(gongFilename); return true; } return false; } } This listing shows the changes needed to take advantage of AudioClipRegistry. First, assign your audio file names to String variables. Second, add an AudioClipRegistry variable, and call its constructor in your applet’s init method. Then, replace calls to getAudioClip with calls to loadAudioClip. Finally, replace calls to the AudioClip play method with calls to playAudioClip. It’s a bit of work, but it may make playing your game a more pleasant experience. In the competitive, impatient world of the Web, you never know what may help you keep that precious market share.
SUMMARY We’ve learned what little there is to know about playing sounds in Java. Until Sun makes its more extended media API available, we’ll have to make do with what we’ve got. The hashing and multithreading techniques presented here should help you squeeze the most out of Java’s limited audio capabilities.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 11 Seven Come Eleven STEVE SIMKIN
I
’d like to say that I only play games of skill. I wish I could tell you that only a match of pure wit and stealth warms my gamer’s
blood. But the truth is that I sometimes sneak out for a lottery ticket or throw a few dollars into the hockey pool at work. One of my fondest memories was cleaning out my boss in my second-ever poker game. I savor the precious instant before the dice come to rest and the second when I’m about to turn over my cards. Those moments are ripe with potential and are delicious in their own right. If the potential is realized, that’s a bonus. You can help your players feel the thrill of losing control by imbuing your games with random events. In this chapter, we’ll look at Java’s random functionality and learn how you can incorporate it into dice and card objects.
RANDOM EVENTS The heart of this chapter is Java’s Random class, from the java.util package. Random is the beast that allows you to roll dice, shuffle cards, or do anything else that must have the appearance of randomness. To make use of Random’s services, you must instantiate a variable of type Random. This variable will happily supply you with a series of numeric values of whatever type you require. Because this chapter focuses on dice and playing cards, we’ll be sticking to integer values. The following snippet shows how to create a Random variable and ask it for an integer value: Random rand = new Random(); int i = rand.nextInt(); Paste this code into a program and display the results. Adding a few more calls to nextInt should be enough to convince you of Random’s randomness and simplicity. But as a game programmer intending to roll a few dice, you’ll see an immediate problem. Your dice have only six (or some other fairly low finite number) sides, and the numbers returned by nextInt are distributed over the full range of values for int. In order to emulate an old-fashioned, six-sided die, we need a few refinements. First, by using the Java modulus operator (%) to return the remainder left after dividing the result of nextInt by 6, we can restrict the range of results to -5 through 5. Then, by applying the Math.abs method to return absolute values only, we can further restrict the range of results to 0 through 5. All that’s left is to add one, and we’re rolling, so to speak. The new, improved code, whose value will always fall within 1 through 6, looks like this: int i = Math.abs(rand.nextInt() % 6) + 1; Armed with this information, we’re ready to build a class that provides a set of useful services centered around random events.
A DICE CLASS The game framework that Neil developed in the early chapters of this book includes a class called Dice. As you might expect, Dice includes a method for simulating a series of random tosses of a standard six-sided die. It also includes functionality for informing
observers of the results of each toss and for triggering a repaint of the playing area. You can find the complete source for Dice.java on the CD-ROM that accompanies this book, but we’ll examine just the methods that implement these three main areas of functionality. ROLLING THE DICE The core method for generating the values returned by dice tosses is generateNewNumber, shown in Listing 11.1. Listing 11.1 The generateNewNumber method. protected void generateNewNumber() { currentFrame = Math.abs(rand.nextInt()) % 6; } The body of generateNewNumber looks very similar to the line of code I showed you a couple of paragraphs ago, except that it doesn’t add 1 before returning. Why not? Won’t this code give us values within the range of 0 to 5, instead of 1 to 6? Yes, it will, which is exactly what we want. You’ll see why as we develop the code that displays the result of the dice toss in our user interface. It’s time to discuss a feature of the Random class that we’ve ignored until now. Have you wondered how Random decides where within its series to start? The answer is that, by default, it bases its starting point on the current time, represented as the number of milliseconds which have passed since midnight of January 1, 1970. By the way, the starting point for a random series is known as its seed value. At any rate, this value seems random enough to keep players from peeking or cheating. But there is a problem, nevertheless. What if my game has two dice, and their Random objects get initialized in the same millisecond. They would then go on to return the same values on every single roll. Bummer. Luckily, Java provides a solution. It allows you to choose your Random object’s seed value when you create it. Simply pass the long value of your choice to Random’s constructor. Our next hurdle is approaching fast. How do you ensure the uniqueness of the values you choose? You could, of course, enumerate your Random objects as you create them, but that demands overhead, and, more importantly, it’s one more thing for your addled developer’s brain to keep track of. Once again, you’re in luck. Java has already done it for you! The Object class provides a method, hashCode, that, for any object, returns an int value based on the object’s identity. The Dice class uses this method to seed its Random object with a unique value. The following lines of code, from Dice’s constructor method, show how it’s done: Random rand; rand = new Random(hashCode() * System.currentTimeMillis()); In order to guarantee uniqueness within each game session, it would have been sufficient to seed the random sequence with the value returned by hashCode. However, this value is based on the object’s address, and the JVM is fairly consistent in its memory assignment behavior. Together, these facts mean that sequences based on hashCode alone stand a good chance of repeating from session to session. Assuming that your users are reasonably intelligent, they probably won’t take long to discover that your dice always roll a 3 followed by a 6 followed by. . . The solution is to combine the values of hashCode and the current time (as returned by System.currentTimeMillis) to generate a seed value that is unique both within the gaming session and across sessions.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
INFORMING OBSERVERS The Dice class doesn’t roll dice merely for its own amusement. It assumes that someone out there may be interested in the values it tosses. It accommodates these interested parties by allowing them to register and then informing them every time it changes values. Any object wishing to register as an observer of the Dice object has to do a couple of things. First, it must implement an interface called DiceObserver. DiceObserver mandates a single void method, diceThrown. The diceThrown method accepts the Dice object as an argument. It is the place to put the observer object’s behavior upon being informed that the dice have been tossed. The second thing that the observer object must do is to register itself with the Dice object. It does this by calling the Dice object’s addDiceObserver method. This method accepts a DiceObserver as an argument. It simply adds each incoming DiceObserver to a Vector called observers. Listing 11.2 shows the code for addDiceObserver. It also shows addDiceObserver’s opposite number, removeDiceObserver, which is used to deregister observers. Listing 11.2 The addDiceObserver and removeDiceObserver methods. public void addDiceObserver(DiceObserver d) { observers.addElement(d); } public void removeDiceObserver(DiceObserver d) { observers.removeElement(d); } The Dice object is responsible for informing its observers after each roll of the dice. It does so in its notifyObservers method, which steps through observers, calling each element’s diceThrown method. Listing 11.3 shows the notifyObservers method. Listing 11.3 The notifyObservers method. void notifyObservers() { for (int i=0; i < observers.size(); ++i) { DiceObserver o = (DiceObserver) observers.elementAt(i); o.diceThrown(this); } } SHOWING THE TOSS As an actor, the Dice object itself has no direct access to the user interface. It has no business knowing anything about the graphics of whatever game it happens to be acting in. On the other hand, it must be able to describe its own graphical representation to the Game in which it’s participating. Equally important, it needs to be able to inform the Game that an event demanding a redraw of the playing area—such as a roll of the dice—has occurred.
What I Look Like Dice fulfills the first of these requirements—describing how it looks to the Game in which it’s acting—during its own construction.
Listing 11.4 shows the Dice class’s constructor method. Most of the method consists of the Dice object describing what it looks like, and we’ll analyze that part now. We’ll discuss the remainder of the constructor in a few paragraphs, when we show how the object’s main areas of functionality come together. Listing 11.4 The Dice constructor. Dice(Game aGame, int ax, int ay) { super(aGame); setImage("images/die.gif", 3, 6); moveTo(ax, ay); rand = new Random(hashCode()*System.currentTimeMillis() throwDice(); }
);
The Dice constructor accepts three arguments: the Game object and a pair of ints indicating the location of the dice image within the applet. It passes the Game reference up to the constructor of its superclass, Actor. Remember that Actor’s constructor is responsible for registering Actors with stage (the Panel in the Game applet where the action takes place), so we don’t have to worry about that particular administrative detail. Whenever stage gets drawn, our Dice object will get drawn. The Dice constructor then calls Actor’s setImage method to assign the images/die.gif file as the object’s image. In addition to the file name, we pass a pair of ints to setImage, indicating the total number of images in the GIF file, as well as their division into columns and rows. In this case, the values 3 and 6 mean that die.gif contains a total of six images, broken into three-column rows. The last step in describing the dice image consists of placing it by calling Actor’s moveTo method, passing it the coordinates that were originally passed to the constructor. We’ve already seen the next line of the constructor. It is used here to seed the Dice object’s sequence of random values. We need to hold off on the constructor’s final line for just a little longer.
I Need To Be Seen So far, we’ve learned how the Dice object describes its graphical representation to the Game. Now, we need to take at look at how Dice says to Game, “Draw me, please.” We’ll start by revisiting the generateNewNumber method. When we first discussed this method, we ignored a couple of details which we’re now ready to examine. The first detail is the fact that the result of our random value generation is assigned to a variable called currentFrame. The code for Dice.java contains no other reference to currentFrame, but if you look in the implementation of paint in Actor.java, you’ll see that currentFrame indicates which image to select from a multi-image GIF file. The fact that currentFrame represents a displacement within a collection—rather than the actual value of the dice—explains the second mystery of generateNewNumber: why we don’t add one to the result of our number generation. Figure 11.1 shows the entire die image file.
Figure 11.1 The multiframe die image. Now we know that the Actor class’s paint method is responsible for selecting the correct image and drawing it in the right place. We also know that drawing stage will automatically cause the dice image to be refreshed. So, the way to say, “Draw me please” is to call stage’s repaint method. We do this with a direct call as follows: theGame.stage.repaint();
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
PUTTING IT ALL TOGETHER Dice’s three areas of functionality—rolling the dice, informing observers, and triggering a screen refresh—come together in the last line of its constructor method, the call to throwDice. Listing 11.5 shows the code for throwDice as it dutifully fulfills Dice’s responsibilities. Listing 11.5 The throwDice method. public synchronized void throwDice() { generateNewNumber(); notifyObservers(); theGame.stage.repaint(); } Each line of throwDice carries out one of Dice’s tasks. The total effect is that rolls of the dice, and all their desired consequences, are brought together in one simple package.
THE DICE CLASS IN ACTION By now you are surely convinced that the Dice class is everything you need for implementing the great cyber crap-shoot (I agonized over the hyphenation of that one!). How can you use Dice in your next game? Figure 11.2 shows a round of TestDiceGameApplet, the simplest imaginable dice game. All it does is role a die and display the results. I’ll let you decide how to win or lose.
Figure 11.2 The Dice class doing its stuff. This thrilling game is implemented in DiceGame.java. Listing 11.6 is a listing of the entire game. It doesn’t do much, but it does make clear how to integrate a Dice object into your game. Listing 11.6 Integrating a Dice object into your game. import Game; import java.awt.*;
class DiceGame extends Game { DebugIndicator di; public void init() { super.init(); backdropManager.setTiled("images/felt.gif"); Dice d = new Dice(this, 60, 50); d.addDiceObserver(di); } protected void createStageManager() { master.setLayout(new BorderLayout()); master.add("North", di = new DebugIndicator("Hello")); stageManager = new StageManager(this); master.add("Center", stageManager); setStage(stageManager); master.resize(200, 200); // note hard-coded size stageManager.init(); } public void createActorManager() { actorManager = new ActorManager(this, 52); actorManager.setUseCaching(false); } public // } public // }
void createCollisionDetector() { don't need a collision detector for this app void createClock() { don't need a clock for this app
} After laying down a velvety green crap table backdrop, the init method creates a Dice variable, specifying where to place the dice image. That’s all there is to it. Dice takes care of the rest.
DICEOBSERVERS Well, that’s not quite all there is to it. We’ve talked about how DiceObservers register with the Dice object and how Dice informs them after each roll. But we haven’t seen a DiceObserver in action. Let’s implement one and see what it can do. Listing 11.7 shows the code for DebugIndicator, a simple DiceObserver. Listing 11.7 A simple DiceObserver. import java.awt.Label; class DebugIndicator extends Label implements DiceObserver { DebugIndicator(String t) { super(t); } public void diceThrown(Dice d) { setText(""+d.diceValue()); } }
DebugIndicator is just a Label that implements the DiceObserver interface. Its diceThrown method simply sets the Label’s text to the value returned by Dice’s diceValue method. Any implementation of DiceObserver will almost certainly call diceValue and then do something with the result. If you take a look back at the code for DiceGame.java (Listing 11.6), you’ll see that the DebugIndicator, di, is created and placed with the following lines of code: DebugIndicator di; public void init() { d.addDiceObserver(di); } protected void createStageManager() { master.setLayout(new BorderLayout()); master.add("North", di = new DebugIndicator("Hello")); } Thanks to the infrastructure we laid in Dice.java and its underlying classes, the coding in DiceGame.java is trivial. Score one for the game framework.
MAKING IT RUN In order to bring our dice game to life, we have to do one more thing: create an applet that will actually run the thing. Listing 11.8 shows the code for DiceGameApplet.java, which really shows how simple life becomes when you have a good framework. Listing 11.8 DiceGameApplet.java. import GameApplet; import java.awt.*; public class DiceGameApplet extends GameApplet { public DiceGameApplet() { super(new DiceGame()); } }
EXTENDING THE DICE CLASS For old-fashioned dice in a single-player game, the Dice class we’ve developed works well enough. If, however, your game uses seven-sided dice, or you want to supply your own die-face images, it is unusable. As a first step toward generalizing Dice to participate in a game framework, we need to enable it to handle unusual dice. On the other hand, we want to keep it easy for developers who do want to use our plain old dice as a default. We can accomplish both goals by adding a couple of instance variables to Dice.java and replacing its constructor with a pair of new ones. Listing 11.9 shows the relevant parts of the new, improved Dice.java. Listing 11.9 New instance variables and constructors in Dice.java. class Dice extends Actor implements EventInterface { /** * Number of sides on die. */ int sides; /** * Current value of die. */ int currentValue;
Dice(Game aGame, int ax, int ay, String aimageFile, int asides, int aFrameCols) { super(aGame); sides = asides; setImage(aimageFile, aFrameCols, asides); // Ensure that we can create more than one die // and throw them simultaneously as gen true randomness. rand = new Random(hashCode()*System.currentTimeMillis() ); throwDice(); moveTo(ax, ay); } /** * Default constructor for old-fashioned six-sided dice */ Dice(Game aGame, int ax, int ay) { this(aGame, ax, ay, "images/die.gif", 6, 3); }
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
In addition to the three familiar arguments, the Dice constructor method accepts three new ones. The first, aimageFile, is the name of the file containing the die-face images. Like die.gif, it must be divisible into frames of equal size, one for each die-face. The second new argument, asides, indicates the number of sides per die. The final argument, aFrameCols, represents the number of columns in each row of the image file. The new constructor replaces the old constructor’s hard-coded values with the values of these arguments, using them to make the same function calls as the old constructor. In addition, it sets a new instance variable, sides, to the value of asides. The generateNewNumber method will make use of this variable when it generates the die’s next value. I hear a voice out there moaning, “But I’ve already written Parcheesi-to-the-death using the old Dice class! Do I have to rewrite my code to conform to your interface?” Not to fear, gentle reader, I would never treat you so harshly. If you look at the second constructor method in Listing 11.9, you’ll see that it has the same interface as the old constructor. It supplies all the old hard-coded values as defaults and passes them on to the new constructor. Similarly, the mouseDoubleClick event handler method remains untouched as the class’s default behavior. Any derived class wishing to neutralize the double click can simply override the method. So your Parcheesi code will keep working, and its developer doesn’t even need to know that the class has changed. This is the beauty of dynamic linking, but it does depend on responsible behavior on the part of the developers of the lower-level classes. When you change and re-release your software, make every effort to preserve all public interfaces. Nobody likes surprises at runtime. While we’re decoupling the Dice class from a particular image representation, let’s more clearly distinguish between the methods that handle the dice rolling functionality and those that communicate with graphical functions. Listing 11.10 shows all the methods that were changed, with the relevant lines highlighted. Listing 11.10 Cleaned-up Dice.java methods. protected void generateNewNumber() { currentValue = (Math.abs(rand.nextInt()) % sides) + 1; } /** * Tell everybody who's interested that the dice value * has changed. The ImageManager is always an unregistered * DiceObserver, so set currentFrame to a displacement one less * than currentValue. */ void notifyObservers() { currentFrame = currentValue - 1; for (int i=0; i < observers.size(); ++i) { DiceObserver o = (DiceObserver) observers.elementAt(i); o.diceThrown(this); } } public int diceValue() { return currentValue; } The Dice class is now ready to play a part in a multiplayer Internet dice game. We’ll set it aside for now. Chris will pick up the theme in Chapter 15, when he introduces you to the basics of network gaming.
BEFORE WE SHUFFLE OFF While we’re still in this random world, we should explore the other gaming activity with randomness at its heart—shuffling cards. Card shuffling presents a slightly more complex problem than dice rolling. Rolling a die merely requires that we generate any value within the die’s range. A shuffled deck of cards must contain an exhaustive, random series of the range values. That means that each card must appear exactly once in the deck. From time to time, there are vigorous debates in the rec.games.programming newsgroup regarding the ultimate card-shuffling algorithm. I’ve chosen one primarily for aesthetic reasons. It simply appeals to me. I have not run benchmarks comparing its speed to that of other algorithms. But on my PC, this algorithm shuffles a deck of cards in 170 milliseconds. Whatever speed I might save with a cleverer algorithm would be trivial compared to the time it takes to load the images of 52 playing cards. You can find the implementation of this algorithm in the game framework code in a file called NewRandom.java. Its idea is simple. First, create an array of ints whose size equals the number of values in the range you want to shuffle. Then, populate each member of the array with its own position in the array. Thus, na[0] == 0, na[1] == 1, etc. Finally, traverse the array, swapping the value in each position with the value in another, randomly chosen position. This guarantees that each value in the range will appear exactly once, and that every value will be randomly placed at least once. Listing 11.11 shows the Java implementation of this algorithm. Listing 11.11 The Set method. public static final int[] Set(int n) { int na[] = new int[n]; Random rand = new Random(); System.out.println("NewRandom:Set n = " + n); for (int i = 0; i < n; ++i) { na[i] = i; } for (int i = 0; i < n; ++i) { swap(na, i, Math.abs(rand.nextInt()) % n); } for (int i = 0; i < n; ++i) { System.out.print(na[i]); } System.out.println(""); return na; } private static final void swap(int[] na, int a, int b) { int temp; System.out.println("Swapping " + a + " and " + b); temp = na[a]; na[a] = na[b]; na[b] = temp; } To use this method to shuffle cards, just use the following code: int cardValues[] = NewRandom.Set(52); You must then associate the values in the array returned by Set with actual playing card values. We’ll learn how to do that in Chapter 12. As you can see, Set calls a private method called swap. The swap method simply, umm, swaps a pair of int values. Figure 11.3 shows the effect of applying the Set method to a pack of cards.
Figure 11.3 Applying the Set method to a pack of cards.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
ONE MORE THING While we’re peeking into NewRandom.java, I’ll show you one more trick it knows how to do. The nextDoubleBetween method returns a random double between any pair of doubles. Neil uses this method in an Asteroids game he wrote. By applying the result to his asteroids’ trajectory, Neil introduces a wandering effect into their otherwise straight path through the sky. Listing 11.12 shows nextDoubleBetween with its single line of code. Listing 11.12 The nextDoubleBetween method. public static final double nextDoubleBetween(double l, double u) { return ((u - l) * Math.random() + l); } Notice that in place of the Random class and its methods, nextDoubleBetween uses the Math class’s random method. It can do this precisely because it needs a random double. Math.random returns a random double between 0.0 and 1.0. With a little arithmetic, nextDoubleBetween places this value in the proportional spot within the range indicated by the arguments passed to it.
SUMMARY This might be the place for some existential thought on random events, but I’ll table the philosophy for another time. We’ve learned how to use the methods of Java’s Random class, as well as the Math.random method. We’ve also seen how to embed these methods in classes that simulate the common elements of chance in games: rolling dice and shuffling cards.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 12 Going It Alone: Java Solitaire STEVE SIMKIN
F
ess up. What was the first thing you did after installing Windows? And how many hours did you do it for? If you’re like the rest
of us, you went straight for the Solitaire. Personally, Windows Solitaire was my introduction to the concepts of dragging, dropping, window resizing, and many other attributes of the contemporary Graphical User Interface. Of course, I kept playing the game long after its educational value had faded. But only outside of work hours, of course. In this chapter, we’ll learn how to use the game framework to develop a Solitaire game. We’ll take the basic card classes that Neil introduced and extend them to provide all the required behaviors. We’ll review the steps I followed in creating the game, defining each of the component classes and combining them to create a complete (albeit no-frills) Solitaire game.
CARD STACKS From the perspective of the game framework, Solitaire can be broken down into a set of 13 objects from 4 different subclasses of the CardStack class. Of course, there are Cards in the game as well, but their behavior is determined entirely by their source and destination CardStacks. We can’t go very far trying to describe the behavior of individual cards without specifying where they came from and where they’re trying to go, so our first task will be to define each type of CardStack that we’ll need to play Solitaire. ALTERNATECOLORDESCENDINGCARDSTACK This unwieldy name communicates the most prominent feature of the card stacks that stretch across the bottom of the Solitaire playing area—as DropZones they accept cards of alternating color and descending value. In addition, they must implement the following behaviors: • When the stack is empty, it accepts Kings. • When the stack is not empty, but its top card is not face up (for instance, after its face-up cards were successfully dragged and dropped to another card stack), its top card can be turned over by double-clicking. • The stack displays its cards with a vertical offset. • The stack can be initialized with varying numbers of cards. • When the stack is initialized, the top card is face up, while all the others are face down. • Each card that is dropped onto an AlternateColorDescendingCardStack is kept face up. • Either the top card in the stack or the set of all face-up cards is available for dragging. To select the top card, the user clicks on a point contained by the top card. To select the set of all face-up cards, the user clicks on a point contained by the bottommost face-up card (if the user clicks on a point where the two cards overlap, the click is assigned to the top card). To refer to the top card and the bottom face-up card, AlternateColorDescendingCardStack has a pair of variables, currentCard and baseCard. These variables are passed to drag-and-drop operations. In addition, currentCard is used to validate cards that the user tries to drop onto the stack. Listing 12.1 shows the methods from AlternateColorDescendingCardStack.java that implement the requirements we’ve defined so far.
Listing 12.1 The AlternateColorDescendingCardStack class. import java.util.*; class AlternateColorDescendingCardStack extends CardStack { SolitaireCard baseCard; // the bottom face-up card SolitaireCard currentCard; // the top card AlternateColorDescendingCardStack(Game aGame, int x, int y, int width, int height) { super(aGame, x, y, width, height); setOffsets(0, 15); // cards stack vertically } /** * actorOkay returns true if 1) cardStack is empty and card is a * king; or 2) card is of opposite color and value one less than * currentCard. */ protected boolean actorOkay(Actor anActor) { if (!cards.empty() && currentCard == null) // if top card face return false; // down, return false if (super.actorOkay(anActor)) { boolean ok; SolitaireCard card = (SolitaireCard) anActor; // king-on-empty test if (cards.empty() && card.getValue() == card.KING) { baseCard = card; ok = true; } // alternate-color-descending test else ok = ((card.isBlackCard() != currentCard.isBlackCard()) && (currentCard.getValue() - card.getValue() == 1)); if (ok) currentCard = card; return ok; } return false; } /** * allow card to drop. Set baseCard of card's old cardStack to null. * Set currentCard of card's to stack to this card. */ public boolean acceptDrop(Actor anActor, int ax, int ay) { if (super.acceptDrop(anActor, ax, ay)) { currentCard = (SolitaireCard) anActor; return true; } return false; } /** * placeCards places n cards on the stack, turns the last one * face up, and sets currentCard and baseCard to its value. */ void placeCards(Stack cards, int n) { SolitaireCard card = null; for (int i = 0; i < n; i++) { card = (SolitaireCard) cards.pop(); addCard(card); }
turnOver(card); } /** * turn card over and set currentCard and baseCard to its value. */ void turnOver(SolitaireCard card) { card.turnOver(); currentCard = card; baseCard = card; } } The actorOkay method implements the first three requirements in our list, the conditions under which AlternateColorDescendingCardStack—acting as a DropSite—will accept a dropped card. The constructor method handles requirement four by calling the CardStack class’s setOffsets method. The placeCards method takes care of requirements five and six. It accepts a Stack of cards and an int specifying the number of cards to be placed. It removes that number of cards from the Stack, adding them to its own stack. It then turns over the last card and sets currentCard and baseCard to that card’s value. It does all this by passing the Card to a turnOver method. I put this functionality in a separate method because I’ll need it again later, when I implement turning the top card face-up by double-clicking on it.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
We don’t have to do anything special to implement requirement seven, keeping dropped cards face up. It is taken care of by CardStack’s default acceptDrop behavior. In fact, the only reason to give AlternateColorDescendingCardStack its own acceptDrop method is to allow it to set currentCard to the value of the dropped card. Once again, I’m setting values I will need later when I implement the drag-and-drop behavior. Figure 12.1 shows a pair of AlternateColorDescendingCardStacks, one in an initialized state and the other after it has accepted dropped cards.
Figure 12.1 A pair of AlternateColorDescendingCardstacks. SAMESUITASCENDINGCARDSTACK This name describes the place every Solitaire card devoutly wishes to end its life: on one of the four stacks—one for each suit—in the upper-right corner of the playing area. Because SameSuitAscendingCardStacks can only be destinations—not sources—for dragged cards, their requirements are relatively simple: • • • •
The stack accepts cards if the dropped card is of the same suit and of the next higher value as the current top card. The stack accepts cards if it is empty and the dropped card is an ace (of any suit). The accepted dropped cards remain face up, with no horizontal or vertical displacement. The cards are not available for dragging.
Listing 12.2 shows the methods from SameSuitAscendingCardStack.java that implement the required behaviors. Listing 12.2 The SameSuitAscendingCardStack class. class SameSuitAscendingCardStack extends CardStack { SolitaireCard currentCard;
// test against this when trying to drop
SameSuitAscendingCardStack(Game aGame, int x, int y, int width, int height) { super(aGame, x, y, width, height); } protected boolean actorOkay(Actor anActor) { boolean ret = false; if (super.actorOkay(anActor)) { SolitaireCard card = (SolitaireCard) anActor; if (card.isCurrentCard()) { if (cards.empty()) ret = (card.getValue() == card.ACE);
else ret = ((card.getSuit() == currentCard.getSuit()) && (card.getValue() - currentCard.getValue() == 1)); } } return ret; } /** * allow card to drop. Set currentCard to this card. */ public boolean acceptDrop(Actor anActor, int ax, int ay) { if (super.acceptDrop(anActor, ax, ay)) { SolitaireCard sCard = (SolitaireCard) anActor; currentCard = sCard; return true; } return false; } } Once again, the validity check on dropped cards is performed in actorOkay. Like AlternateColorDescendingCardStack, SameSuitAscendingCardStack uses a variable called currentCard to compare with the value of the dropped card. It also calls a couple of methods belonging to the Card class: getSuit and getValue. The variable card.ACE, used in the comparison, is one of a set of public static final ints defined in Card. The rest of the requirements for this class are taken care of by the game framework’s default behavior, so we can move on to the next card stack. STOCKPILECARDSTACK The stockpile is the stack that keeps you in the game. When all seems lost, you turn over just three more cards from the stockpile in the upper-left corner, in the hope that you will break your latest logjam. This card stack exhibits extremely simple behaviors. It accepts a group of cards from a deck. They are placed face down, with no horizontal or vertical displacement. They are unavailable for dragging and will not accept dropped cards. What a boring life! The StockpileCardStack does have one interesting behavior, however, which we’ll discuss after a look at its source code. That source code, from StockpileCardStack.java, is shown in Listing 12.3. Listing 12.3 The StockpileCardStack class. import java.awt.*; import java.util.*; class StockpileCardStack extends CardStack implements EventInterface { StockpileCardStack(Game aGame, int x, int y, int width, int height) { super(aGame, x, y, width, height); notifyForEvents(); } protected boolean actorOkay(Actor anActor) { return false; // stockpile never accepts a drop } /** * placeCards places n cards on the stack, leaving them all face down. */ void placeCards(Stack cards, int n) { SolitaireCard card = null;
for (int i = 0; i < n; i++) { card = (SolitaireCard) cards.pop(); addCard(card); card.visible = false; if (card.facingUp) card.turnOver(); } card.visible = true; } Stack removeCards(int n) { Stack removedCards = new Stack(); for (int i = 0; i < n; i++) removedCards.push(cards.pop()); if (!cards.empty()) ((SolitaireCard) cards.peek()).visible = true; return removedCards; } protected void notifyForEvents() { theGame.eventManager.addNotification(this); } public boolean mouseDown(Event anEvent, int x, int y) { if (containsPoint(x, y) && cards.empty()) ((SolitaireGame) theGame).refreshStockpile(); return false; } } After studying the previous two card stacks so closely, we can make short work of most of StockpileCardStack’s code. The actorOkay method simply returns false, preventing dragged cards from ever being dropped on a stockpile. The only way to add cards to the stack is through placeCards, which, like other placeCards methods we’ve seen, peels cards off of a Stack and transfers them to the current Stack. Because cards can come in facing either up or down, placeCards has to test them and flip them if they’re up. Finally, it makes all cards but the top one invisible. Remember that every visible card gets drawn by the StageManager. With no displacement between cards, we don’t want to waste a lot of time drawing cards that aren’t actually visible.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Not all of the code here is familiar, however. Most noticeably, StockpileCardStack implements EventInterface, the interface that allows objects to register with the game framework’s EventManager. It registers by calling notifyForEvents during its constructor. It must do this because, unlike the card stacks we’ve written so far, StockpileCardStack must respond to mouse events when the stack is empty. This can happen after all of its cards have been transferred to the face-up stockpile and the user wants to browse through them again. As an EventInterface, StockpileCardStack must implement methods for all possible input events. It is interested only in mouseDown, so it just returns false from all the others. I didn’t bother showing them to you. In its implementation of mouseDown, it calls a method called refreshStockpile, which belongs to a class called SolitaireGame. This is a class we haven’t met yet, but we will soon. Before we can, though, we have to meet just one more card stack. FACEUPSTOCKPILECARDSTACK This stack is where the cards land when you flip them off of the stockpile. So right off the bat, we know that this class has to be able to receive groups of cards, turning them over and displaying them in the reverse order from which they were received. Other requirements include: • Cards are displayed with no horizontal or vertical displacement. • The top card is draggable. • Cards may not be dropped on a FaceupStockpileCardStack. By now, meeting these requirements is simple. I won’t even show you most of the code for this class. Listing 12.4 shows the only method of interest to experienced card-stack designers like us. Listing 12.4 The FaceupStockpileCardStack placeCards method. void placeCards(Stack cards, int n) { SolitaireCard card = null; for (int i = 0; i < n; i++) { card = (SolitaireCard) cards.elementAt(0); cards.removeElementAt(0); addCard(card); card.turnOver(); card.visible = true; } card.draggable = true; } Notice that FaceupStockpileCardStack pulls cards off of the Stack passed to it differently from the other CardStacks we’ve seen. Were we to use our usual Stack.pop method, the cards would be displayed in the same order in which they were received, effectively reversing the order of each group of three within the deck. By calling Vector.removeElementAt(0) instead, we maintain the correct order. Unlike StockpileCardStack, FaceupStockpileCardStack makes all of its cards visible. Why? So that when you drag the top card off of it, the number two card peeks out. To minimize painting activity, I could have added logic to ensure that only the top two cards are visible. If performance becomes an issue, maybe I’ll have to do that. After placing all of its cards, FaceupStockpileCardStack
makes the top one draggable. So far, we’ve defined all of the card stacks we’ll need to build our Solitaire game. Let’s create a container for all these stacks.
SOLITAIREGAME The container that holds all of the participants in our Solitaire game is a class called SolitaireGame (I hope the name achieves in clarity what it lacks in originality). SolitaireGame has two responsibilities. Its primary responsibility is, of course, to set the game up by creating a deck of cards and a bunch of card stacks, placing the card stacks on the playing area, and distributing the cards among them. Listing 12.5 shows the SolitaireGame’s init method, which discharges this first responsibility. Listing 12.5 The SolitaireGame init method. import java.awt.*; import java.util.*; class SolitaireGame extends Game { SolitaireCardPack cardPack; StockpileCardStack sStack = null; FaceupStockpileCardStack fsStack = null; boolean transferringStockpile = false; public void init() { super.init(); backdropManager.setTiled("images/felt.gif"); cardPack = new SolitaireCardPack(this, 52); cardPack.shuffle(); Stack cards = cardPack.getCards(); AlternateColorDescendingCardStack acdStack1 = new AlternateColorDescendingCardStack(this, 20, 140, 73, 97); acdStack1.placeCards(cards, 1); AlternateColorDescendingCardStack acdStack2 = new AlternateColorDescendingCardStack(this, 130, 140, 73, 97); acdStack2.placeCards(cards, 2); AlternateColorDescendingCardStack acdStack3 = new AlternateColorDescendingCardStack(this, 240, 140, 73, 97); acdStack3.placeCards(cards, 3); AlternateColorDescendingCardStack acdStack4 = new AlternateColorDescendingCardStack(this, 350, 140, 73, 97); acdStack4.placeCards(cards, 4); AlternateColorDescendingCardStack acdStack5 = new AlternateColorDescendingCardStack(this, 460, 140, 73, 97); acdStack5.placeCards(cards, 5); AlternateColorDescendingCardStack acdStack6 = new AlternateColorDescendingCardStack(this, 570, 140, 73, 97); acdStack6.placeCards(cards, 6); AlternateColorDescendingCardStack acdStack7 = new AlternateColorDescendingCardStack(this, 680, 140, 73, 97); acdStack7.placeCards(cards, 7); SameSuitAscendingCardStack ssaStack1 = new SameSuitAscendingCardStack(this, 350, 20, 73, 97); SameSuitAscendingCardStack ssaStack2 = new SameSuitAscendingCardStack(this, 460, 20, 73, 97); SameSuitAscendingCardStack ssaStack3 = new SameSuitAscendingCardStack(this, 570, 20, 73, 97); SameSuitAscendingCardStack ssaStack4 = new SameSuitAscendingCardStack(this, 680, 20, 73, 97); sStack = new StockpileCardStack(this, 20, 20, 73, 97); sStack.placeCards(cards, 24);
fsStack = new FaceupStockpileCardStack(this, 130, 20, 73, 97); } For our deck of cards, we create a SolitaireCardPack object. SolitaireCardPack behaves identically to the CardPack class with which you’re already familiar, except that, instead of creating a deck of 52 Card objects, it creates a deck of SolitaireCards. I’ll explain why plain old Cards aren’t good enough when we discuss dragging and dropping.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
But, first, we must finish our review of SolitaireGame’s duties. Remember when we were talking about StockpileCardStack’s implementation of mouseDown? That method called SolitaireGame.refreshStockpile. Because the mouseDown event has implications for both the stockpile and the face-up stockpile, Stockpile can’t handle the event single-handedly. It calls on its container and in effect says, “You handle this.” In fact, SolitaireGame has two methods for transferring cards between the stockpile and the face-up stockpile, one method for each direction. Listing 12.6 shows these two methods. Listing 12.6 The SolitaireGame transfer methods. public void transferStockpile() { if (!transferringStockpile) { int toTransfer = sStack.cards.size() < 3 ? sStack.cards.size() : 3; Stack transferCards = sStack.removeCards(toTransfer); fsStack.placeCards(transferCards, toTransfer); transferringStockpile = true; } } public void refreshStockpile() { if (!transferringStockpile) { Stack transferCards = fsStack.removeCards(); sStack.placeCards(transferCards, transferCards.size()); } } public void stopTransfer() { transferringStockpile = false; } The transferStockpile method is responsible for transferring three cards (or fewer, if there are fewer than three cards in the stockpile) from the stockpile to the face-up stockpile. It starts by testing the number of cards in the stockpile and setting the value of a variable called toTransfer to the number of cards to be transferred. It then calls the appropriate removeCards and placeCards methods to accomplish the transfer. Finally, it sets a boolean variable called transferringStockpile to true. This variable ensures that for each mouseDown event only one group of cards gets transferred and no cards get transferred back. The value of transferringStockpile gets reset to false in stopTransfer, which is triggered by the mouseUp event. As you can see, refreshStockpile is transferStockpile in reverse, except that it doesn’t pass a number to removeCards. The reason for this is simple: refreshStockpile wants to move all the cards.
TIP: My Next Enhancement In my ongoing effort to perfect my Java Solitaire, the next enhancement on my list is to make the number of cards that get flipped from the stockpile to the face-up stockpile configurable. I recommend it as an exercise for you, as well.
Figure 12.2 shows the initialized SolitaireGame ready to go.
Figure 12.2 A freshly dealt Solitaire game.
MAKING IT MOVE So far, we have a Solitaire game that’s primed for action. If you try playing the game using just what we’ve defined so far, you would find a surprising amount of drag-and-drop functionality already in place, thanks to the game framework. Our next task will be to define the missing functionality. Table 12.1 shows the desired behavior for all the Event/CardStack combinations that interest us, as well as indicating whether the desired behavior is provided as a default by the game framework. In addition, if the top card of an AlternateColorDescendingCardStack is face down, a mouseDoubleClick on it should turn it over and assign its value to both currentCard and baseCard. This requirement is so specific that I couldn’t justify adding another row and another column to Table 12.1. Table 12.1Drag-and-drop behavior required by Solitaire.
Event mouseDown
mouseDrag
mouseUp
Empty StockpileCardStack
SolitaireGame.refresh Stockpile
–
–
Populated Stockpile CardStack
SolitaireGame.refresh Stockpile
–
set transferringStockpile to false
FaceupStockpile
startDrag (default)
dragTo (default)
stopDrag (default)
currentCard of Alternate ColorDescending CardStack
startDrag (default)
dragTo (default)
stopDrag (default)
baseCard of Alternate ColorDescending CardStack
startDrag on all face-up cards
dragTo on all face-up cards
stopDrag on all face-up cards
SameSuitAscending CardStack
–
–
–
Looking at this table, it quickly becomes apparent that much of the functionality we need is either part of the default game framework or is easily implementable by adding a simple event handler in the right spot. For example, to implement the required behavior for the mouseDown event on an empty StockpileCardStack, I added the following method to StockpileCardStack.java: public boolean mouseDown(Event anEvent, int x, int y) { if (containsPoint(x, y) && cards.empty()) ((SolitaireGame) theGame).refreshStockpile(); return false; } In all other cases, the card itself should catch the event. This is the reason that Solitaire requires its own specialized card class, called (surprise!) SolitaireCard. This class implements event handlers for mouseDown, mouseDrag, and mouseUp. The tricky part is handling the drag-and-drop behavior from an AlternatingColorDescendingCardStack. There are two reasons for this. First, the game framework has no facilities for moving groups of actors as a unit. Second, the currentCard is also part of the group to be moved when the user drags the baseCard.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
I addressed both of these difficulties by creating a special class, called ACDDragDropManager, for managing dragging and dropping for groups of cards. Each AlternateColorDescendingCardStack has an ACDDragDropManager. The ACDDragDropManager maintains a list of every face-up card in the stack. The card stack’s addCard method registers each new card with the ACDDragDropManager, while the removeCard method is responsible for removing it from ACDDragDropManager’s list. The ACDDragDropManager manages all dragging and dropping for the group of cards. It does not delegate any work to the official DragDropManager. While this introduces a certain complexity, it reduces the frequency of screen refreshes because, in its current implementation, the framework’s DragDropManager redraws the screen for each movement of each actor, which can result in very sluggish response. When the user selects a point within the overlap of the currentCard and the baseCard, the SolitaireCard is responsible for deciding how to respond. In our initial definition of the game, we decided that this case is considered as a selection of the currentCard, so the event handlers in SolitaireCard must test the coordinates that were clicked against currentCard before they test them against baseCard. On the other hand, if the baseCard was selected, SolitaireCard must ensure ACDDragDropManager gets control of all face-up cards, including currentCard, with no interference from the overall DragDropManager. As an example, Listing 12.7 shows the mouseDown event handler code from SolitaireCard.java. Listing 12.7 SolitaireCard’s event-handling methods. public boolean mouseDown(Event anEvent, int x, int y) { if (getBoundingBox().inside(x, y)) { myCardStack = cardStack == null ? oldCardStack : cardStack; if (myCardStack instanceof AlternateColorDescendingCardStack) { AlternateColorDescendingCardStack myacdStack = (AlternateColorDescendingCardStack) myCardStack; if (myacdStack.baseCard != null && name == myacdStack.baseCard.name && !myacdStack.currentCard.getBoundingBox().inside(x, y)) { myacdStack.acdDragDrop.startDragAt(x, y); return true; } } if (myCardStack instanceof StockpileCardStack && cardStack.isTopCard(this)) { ((SolitaireGame) theGame).transferStockpile(); return false; } } return false; } This method identifies the two cases of mouseDown events for which the default behavior must be overridden. The first case is the one we were just discussing, when the user selects a point within the baseCard that is not within the currentCard. In this case, we call ACDDragDropManager’s startDragAt method. This method primes all face-up cards in the stack for dragging and does all the housekeeping needed to reset the drag if necessary. After calling startDragAt, the event handler returns true, to prevent further processing. It is essential to keep the card stack’s private ACDDragDropManager and the game’s overall DragDropManager from interfering with each other. The second case where the default mouseDown behavior must be overridden is when the user selects a point on the top card in the stockpile. In this case, the event handler must tell the Solitaire game to transfer three cards to the face-up stockpile. It does this by
calling SolitaireGame.transferStockpile. It then returns false, because it is indifferent to further processing. Any other case will cause the event handler to return false, allowing the default behavior to proceed. ACDDRAGDROPMANAGER Now that we’ve defused the default drag-and-drop behavior, we have to provide a substitute. The ACDDragDropManager handles these responsibilities. Its implementation parallels that of the default drag-and-drop manager, with the adaptations necessary for moving multiple actors. The major change is that each step of the startDrag/moveTo/stopDrag process must be applied to every actor in ACDDragDropManager’s private Stack of face-up cards. As an example, Listing 12.8 shows the methods that initialize the face-up cards for dragging. Listing 12.8 The ACDDragDropManager drag initialization methods. void startDragAt(int x, int y) { int size = faceupCards.size(); int zorder = ((Actor) faceupCards.elementAt(0)).zorder; for (int i = 0; i < size; i++) { System.out.println("dragging element " + i); startCardDragAt((Actor) faceupCards.elementAt(i), i, x, y, zorder); } oldCurrentCard = theStack.currentCard; theStack.currentCard = null; oldBaseCard = theStack.baseCard; dragging = true; theGame.actorManager.sortByZOrder(); theGame.stage.repaint(); } void startCardDragAt(Actor draggedActor, int disp, int x, int y,int zorder) { draggedActor.zorder = zorder + disp; draggedActor.setFixed(false); originalx[disp] = draggedActor.getX(); originaly[disp] = draggedActor.getY(); dx[disp] = x - originalx[disp]; dy[disp] = y - originaly[disp] + (20 * disp); // note hard-coded 20 SolitaireCard sCard = (SolitaireCard) draggedActor; sCard.oldCardStack = theStack; theStack.removeACDCard(sCard); } The startDragAt method passes each face-up card to startCardDragAt. It uses the baseCard’s zorder as a base value, setting the zorder of each card in the stack to this base value, plus its displacement from baseCard within the stack. This technique has the effect of preserving the stacking order while the cards are moving. The stopDrag and resetDrag methods for this manager are responsible for either placing each dragged card on its destination stack or restoring it to its source stack. Figure 12.3 shows a single card being picked off a group of face-up cards, while Figure 12.4 shows an entire group of face-up cards in transit.
Figure 12.3 Picking the top card off a pile.
Figure 12.4 Dragging a set of face-up cards.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
AFTER THE MOVE When a card is successfully placed on a new stack, the framework’s default behavior removes it from its previous stack. But there may be additional cleanup work to do on the old stack. For example, after the currentCard is removed from an AlternateColorDescendingCardStack, the values for currentCard (and possibly baseCard) have to be reset, and the card has to be removed from the ACDDragDropManager’s set of face-up cards. To accomplish this, every card stack with special cleanup needs implements a removeCard method. After accepting a card, the acceptDrop method of every drop site calls SolitaireCard’s removeFromPreviousStack method. It examines the card and, based on its class, chooses the appropriate removeCard method.
SUMMARY You can play my Solitaire game in your browser by opening runSolitaire.html. I’ll warn you from the outset that the only reward for winning is the inherent satisfaction. No dancing cards or fireworks. It just ends. But not without showing that you can use the game framework to build a full-scale card game fairly simply. Let’s just review the major steps I took in building my Solitaire game. First, I defined all the card stacks the game requires. For each, I implemented the appropriate AcceptDrop behavior. Then, I created a Game class that places the proper combination of stacks on the playing area. Next, I analyzed the game’s behavior in response to mouse events and compared it to the game framework’s default behavior. I discovered that while most of the required behavior was already implemented by the game framework, some of it had to be added. I therefore defined a class called SolitaireCard that trapped the events requiring custom behavior. One of those behaviors involved moving groups of cards between stacks. The game framework doesn’t have any facility for dragging groups of actors (at least not yet), so I created a new DragDropManager to manage groups of cards.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 13 MOVIN’ AND THINKIN’: GIVING YOUR GAMES SOME SMARTS STEVE SIMKIN
W
ith all the buzz about Deep Blue, Artificial Intelligence (AI) has once again been prominent in the news lately. Although the
machine ultimately lost, the fact that it was even competitive on a world championship level reopened the “Can machines think?” question in the popular press. I won’t waste either your time or this book’s paper with my own speculation on AI theory, but I will point out the undeniable fact that Deep Blue’s programmers successfully taught it to engage in an activity which to many people symbolizes pure intelligence. In this chapter, you’ll learn to teach your games to do that, too. First, a definition. In this chapter, I use the term intelligence very loosely. That isn’t an insult to you or your game. It’s just that intelligence is the best collective term I could think of for the repertoire of techniques your game can use to behave like a worthy opponent. As we’ll see, the guideline for programming your game’s competitive behavior should be: Make your game challenging enough to be interesting, while keeping it easy enough that the human player stands a chance. Remember, your real goal is not to win the game, but to win market share. Depending on the type of game, its intelligence can find expression in a variety of ways. The most obvious of these is choosing the next move in an alternating-turns game of skill, such as chess, Abalone, or Connect4. But expressing intelligence can also include the movements of automated, pursuing, lethal enemies, and the structure of the game’s levels of increasing difficulty. We’ll start our lesson with a discussion of these last two features.
THE ENEMIES OF PIXEL PETE In Chapter 1, I mentioned an amusing little game of pursuit called Iceblox, in which evil flames chase an innocent, adorable penguin (“Pixel Pete”) around a maze, while the penguin tries to extract golden coins from ice cubes. Iceblox makes very effective use of simple techniques for keeping the action interesting. We’re going to analyze Iceblox’s intelligence, such as it is, for two reasons. First, because there isn’t much of it, which makes it a great place to start; and second, because, despite being a little light on brains, Iceblox is a very satisfying game to play. By properly combining a few simple elements, Karl Hornell (http://www.tdb.uu.se/~karl), the creator of Iceblox, has helped a little go a long way. Figure 13.1 captures Pete with the flames in cold pursuit.
Figure 13.1 Panicked Pete and his persevering pursuers. At the heart of Iceblox’s competitive spirit is the pursuit behavior of the evil flames. These baddies live by the easily expressed
maxim: “If Pete’s around, chase him down.” The code snippet in Listing 13.1 shows the Java formulation of the flames’ rule to live by. Listing 13.1 Dance of the flames. final int animF[]={32,33,34,35,36,35,34,33}; int x[],y[],dx[],dy[],motion[],look[],creature[],ccount[],actors,flames; Math m; switch(creature[i]) { case 4: // Flame look[i]=animF[counter%8]; if (motion[i]==0) motion[i]=(int)(1+m.random()*4); if ((x[i]%30 == 0)&&(y[i]%30 == 0)) // Track penguin { if (((x[i]-x[0])='0') && (key0 ) writeEventConnectionDirect( (GameEvent) writeEventQueue.remove() ); } else { // Process read message. if ( processGameEvent( e ) == false ) passEventUp( e ); // Event type not supported by this // level. } } } public synchronized GameEvent retrieveEvent( ) { while ( true ) { if ( eventQueue.size()>0 ) return (GameEvent) eventQueue.remove(); if ( writeEventQueue.size()>0 ) return null; // Return null to signal an outbound event // pending. // Wait for someone to store an inbound or outbound event. try { wait(); } catch ( InterruptedException ie ) {} } } The normal GameLayer.run() logic has been replaced with the runReadWrite() method. Like its predecessor run(),
runReadWrite() spends most of its time blocked within a call to retrieveEvent(). When retrieveEvent() returns, runReadWrite() tests for a null GameEvent. This is the signal from retrieveEvent() that an outbound GameEvent is waiting. The change to retrieveEvent() is relatively minor. It now checks for the presence of queued GameEvents in both the outbound queue, writeEventQueue, and the inbound queue, eventQueue, before blocking. As I mentioned, retrieveEvent() will return null to signal that an outbound GameEvent is pending. The sequence of checking the event queues will define the behavior of the application with respect to GameEvent IO. The method is coded to process inbound events before outbound events. This is a result of having the eventQueue tested first in the retrieveEvent () loop. The tests were ordered this way because the processing of an inbound event will generally take a predicable and short period. This is not the case for writing game events. In fact, this is exactly the reason why we had to decouple writing GameEvents from the user interface. Had we reversed the tests, a game that generated a steady stream of outbound events might possibly fail to process inbound events in a timely manner, because successive calls to retrieveEvent() would always return null.
DEALING WITH NETWORK ERRORS In Chapter 16, we described how the GameLayer/GameEvent framework would detect IOExceptions and proceeded to reconnect the client and server. This approach provided a nice new conduit for sending information between the two sides of the game. Unfortunately, when the game and server actually reconnect after a network disturbance, the result is not always what you might desire. The applet talks to the server, and the server talks back. The problem is that neither side understands what is going on! When a network error occurs, it is quite possible that the failure will “consume” an in-transit GameEvent. If this event happened to signal a change of state from, say, deployment mode to attack mode, then confusion reigns. When the connection is re-established the applet happily sends messages specific to the attack mode. Meanwhile, the server never transitioned out of deployment mode. Now, the deployment logic in the server is being bombarded with attack mode events that it just ignores. You can address this situation in several ways. After a reconnect, you might choose to have both the server and the client reset themselves to a “known sane state.” Possibly, the beginning of the current player’s turn. This approach seems quite simple on the surface. At the beginning of a player’s turn, a snapshot of the game could be saved. If the connection is reset, then both sides would revert to the snapshot state, and the applet would have to apologize to the player about undoing all her hard won victories. Of course, if the one message that gets lost just happens to signal the change of players turns… The “known sane state” approach also happens to violate one of the guiding rules of the GLE framework. Remember this statement from Chapter 16:
Note: Within the GameLayer/GameEvent realm, there are two types of network errors. Those that can be fixed, and those that cannot. If a network error can be corrected, it will be, and the game logic will remain unaware of the problem. Only the non-recoverable network errors will surface within the game logic layers.
These were big words, and we intend to live by them. What this excerpt means to us as game writers is that we can free ourselves from the concern of always second-guessing the network connection and providing an appropriate recovery path. So what is the GameLayer/GameEvent answer? The problem lies in lost messages. So that is what GLE will prevent. Our next task will be to extend the GameLayer/GameEvent framework to provide “registered” GameEvents. These events have the pleasant property of guaranteed delivery. The GameEvent class has been modified to contain a boolean value registered describing whether the event is registered or not. All GameEvents default to being registered. You must explicitly reset the registered flag with a call to GameEvent.nonRegistered(). Before we go into the implementation of registered events, we’ll briefly touch on which events should and should not be registered.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Register pretty much everything—unless you are absolutely sure it is non-essential to the state of the game or the players’ happiness. The implementation of registered messages incurs a fixed, 12-byte overhead for each GameEvent that hits the network, whether it is registered or not. The only additional resources consumed by registering a GameEvent is the caching of the GameEvent within the ConnectionLayer that sends the event. The caching only represents a minor delay in the events march to that great garbage collector in the sky, not a permanent stay of execution. The secret is out. GLE caches registered events so that they may be re-sent if there are network difficulties. Let’s examine the implementation. The registered message algorithm has the following elements: • • • •
Sending a registered GameEvent Reviving a registered GameEvent Detecting missing messages Maintaining the cache
Before we trace through the GameEvent’s new adventures, let’s set the stage with some background information. Conceptually, we are dealing with the passage of events between two ConnectionLayer objects. Between these connection layers, there is a path across the network in the form of a socket connection. We wish to have each connection layer cache the registered events that it sends so that it can resend them if needed. In addition to this, the connection layer must be able to communicate the following pieces of information: • Is the current message registered? • How many registered messages have been sent? • How many registered messages have been received? Each connection layer has a counter to track the number of registered messages it has sent. This counter is the regIdSendStream member. As a registered event is about to be sent, regIdSendStream is incremented, and the event is “tagged” with the new value as its ID; non-registered events are “tagged” with zero as the ID. Both the event tag and the current value of regIdSendStream are sent with each GameEvent as part of an extended header. The last part of the extended header is the ID of the last registered message that was successfully received by the sending connection layer. This counter is the regIdRecvStream member. It acts as an acknowledgment that all registered messages up to and including the specified value have been successfully received. This acknowledgment data is crucial to allow the sender to discard registered events from its cache. The three values describing the registered message status are packaged within a RegGameEventData object. Prior to sending any GameEvent, the ConnectionLayer will create a RegGameEventData. It will be initialized to represent the state of the sending connection and passed to the GameEvent for streaming across the network as part of the header. Figure 17.5 shows the new layout of the GameEvent data as it passes across the network. At the other side of the network, a GameEvent reads the header information and stores the values in another RegGameEventData ready for the destination ConnectionLayer to validate the overall state of communications.
Figure 17.5 Layout of a GameEvent with registered event information. That’s the overview. Now, let’s dig a little deeper into the details.
SENDING A REGISTERED GAMEEVENT Earlier in this chapter, we saw how the writing of GameEvents was decoupled from the user interface thread. Outbound events were stored in the writeEventQueue and retrieved by the ConnectionLayer object within the ConnectionLayer.runReadWrite() method. This method passed the event to the writeEventConnectionDirect() method. It is here that we continue to follow the GameEvent on its journey. The writeEventConnectionDirect() method contains the sending-side logic for the registered event algorithm. Listing 17.4 shows how a RegGameEventData object is created. The object is populated with the ID of the event. Registered messages are assigned ++regIdSendStream; non-registered messages are assigned zero. The current values for the number of sent and received registered messages are also set. If the event is registered, it is added to the cache of registered events stored within the connection layer. Finally, both the GameEvent and the RegGameEventData are passed to the method writeEventConnectionNet() to be written to the network. Listing 17.4 The ConnectionLayer.writeEventConnectionDirect() method. protected synchronized boolean writeEventConnectionDirect( GameEvent e ) { RegGameEventData rged = new RegGameEventData( isRegistered? ++regIdSendStream : 0, regIdSendStream, regIdRecvStream ); // If this is a registered message, store it in case we must resend it. if ( e.isRegistered() ) addCached( rged,e ); return writeEventConnectionNet( e, rged ); } RECEIVING A REGISTERED GAMEEVENT AND DETECTING ERRORS Sending a registered event was pretty trivial. What could go wrong from the time we call writeEvent() until writeEventConnectionNet() is invoked? We haven’t even touched the network yet. By the time the receiving ConnectionLayer sees the event, it must be prepared to deal with anything the network can think of. The ConnectionLayer.storeNetworkEvent() method, shown in Listing 17.5, is ready to deal with all these eventualities. The storeNetworkEvent() method is called after the Connection object has successfully read a GameEvent from the socket via its DataInputStream. The reading process has tested the guard bytes in the header and the trailing blocks that surround the GameEvent data. In addition, the three long integers that were packaged in a RegGameEventData and sent as part of the extended header have been retrieved and stored in another RegGameEventData. So, the GameEvent and RegGameEventData data now arrive at storeNetworkEvent(). The first check performed within storeNetworkEvent() is to determine if the event is a resend request. The purpose of the resend request is to request the retransmission of a set of registered GameEvents. As we’ll see in a moment, resend requests are generated within a call to storeNetworkEvent() for the connection layer at the other end of the socket.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
In a world where data may be re-sent, we may experience duplicate events. The next test determines if a registered event has an ID less than regIdRecvStream. If it does, the event must be a duplicate and is discarded. Note that we do not check for retransmission of non-registered events. However, because the events are non-registered, they are non-cached, which means they cannot be re-sent. Next, it is time for a little house cleaning. Stored within the idRecvStream member of the RegGameEventData object is the ID of the last registered event that was accepted by the other side of the connection. As I mentioned earlier, this is basically an acknowledgment, and it signals to the connection layer that it can discard all registered events in the cache, up to and including this ID. The test ( (rged.registeredId==regIdRecvStream+1) ) is used to confirm that the current event is in fact the next registered message that the connection layer is expecting. If the test passes, the connection layer makes note of the arrival by updating the regIdRecvStream. This test is important because it ensures that registered messages must be received in the correct sequence. At any time, the only acceptable registered event is the next (regIdRecvStream+1) event. What happens if we see an event out of sequence? Earlier, we detected and discarded duplicate events where the event’s registeredId was less than expected. Now, it is time to detect missing events. The test for missing events is based on a comparison of rged.idSendStream and regIdRecvStream. These two values represent the number of registered events the sending ConnectionLayer has sent, and the number of registered events the receiving ConnectionLayer has received. If the test fails, it means we have missed one or more registered events. The only satisfactory course is to request the retransmission of all the missing events. This is accomplished by sending a RESEND_REQUEST event type back to the sending ConnectionLayer. The current event is discarded because it is out of sequence. Once the event has successfully navigated all the validity tests, it is stored within the ConnectionLayer so that it can be processed as normal. Listing 17.5 The ConnectionLayer.storeNetworkEvent() method. public void storeNetworkEvent( GameEvent ge, RegGameEventData rged ) { lastReadEventTime = System.currentTimeMillis(); clNotify.doNotify( CL_EVENT_READ, ge ); // Is this a resend request? if ( ge.getEventType()==GameEvent.RESEND_REQUEST ) { resendRegistered( rged.registeredId ); return; } // Is this a duplicate copy of an earlier event? if ( (rged.registeredId!=0) && (rged.registeredIdregIdRecvStream ) { // Request missing messages, ignore the event we have in our // hands... rged.registeredId = regIdRecvStream; rged.idSendStream = rged.idRecvStream = 0; ge = Game.createGameEvent( GameEvent.RESEND_REQUEST ); writeEventConnectionNet( ge, rged ); } else { // All is good. StoreEvent. storeEvent( ge ); } } MANAGING THE REGISTERED GAMEEVENT CACHE The only way to assure that all registered GameEvents are received is to provide a rigorous acknowledgment method. We have accomplished this by tacking 12 additional bytes of management information to the transmission of every GameEvent. This data is present whether the event is registered or not. Non-registered events are still tagged with regIdSendStream and regIdRecvStream values so they can signal retransmission of registered events and flushing of the registered event cache. In light of the caching of registered events, the lowly HEART_BEAT event takes on a new significance. You may recall from Chapter 16 that this event was sent by the ClientConnection when the connection experienced periods of inactivity. In Chapter 16, the purpose of this event was solely to detect dropped connections and initiate reconnection. Now we have another reason for the existence of the HEART_BEAT event. If the flow of information within the game is one directional, the lack of events traveling in the reverse direction will prevent any event acknowledgment from happening. This will result in a sending cache that continues to grow without bounds. To prevent this, the flow of HEART_BEAT events was made bi-directional. Now, both client and server connections can send HEART_BEAT events if there are no other events being sent. This change was achieved by moving the heart beat functionality from the ClientConnectionLayer class to the ConnectionLayer class. We are now certain that there will be a bi-directional flow of events, which ensures the proper flushing of registered events from the caches.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
Earlier, we saw how the detection of a mismatch in the number of sent and received events would trigger the retransmission of missing events. The retransmission of registered GameEvents is also triggered as the last step of the reconnect process for both the client and server side of a connection. This is a little proactive programming. It is not necessary because the next event to pass across the connection would cause the detection of a inconsistency. Still, sending the events will prime the system to get back to where it should be quickly. If any of the re-sent events had successfully navigated the connection prior to the connection failure, they will merely be detected as duplicates and discarded. That brings to a close our discussion of registered GameEvents. Armed with the GameLayer/GameEvent framework, we can now focus on writing our network games and leave the network to the ever vigilant ConnectionLayer class and its cohorts.
SUMMARY Within this chapter, we extended Domination with the goal of making it more player and network-friendly. We looked both externally and internally to smooth out some of the potential problems. With our focus on the user interface, we strived to keep the player both informed with the StatusCanvas and entertained with features like game watching and inter-player chat. Inside Domination, we focused on improving the flow of events by decoupling the writing of events from the user interface or game logic threads. We enlisted the connection layer objects to handle the extra duty. On the client side, this approach eliminated the chance of the user interface locking up. The server side benefited by distributing the load of writing events to multiple connection threads operating in parallel, rather than the old sequential model. Finally, we took some time out to delve deeper into the solution for network difficulties. This took the form of guaranteed GameEvent delivery. Now, it’s time for something completely different. In the next chapter, we’ll leave the familiar realm of Domination behind and return to the maze world to see the evolution of Chapter 14’s MazeWars into a network game.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CHAPTER 18 NETWORK MAZEWARS CHRIS STRANC
I
n Chapters 15 and 16, we focused on the network game Domination. Now we’ll turn our attention back to MazeWars, which we
discussed back in Chapter 14. In case you’ve forgotten, let me take a moment to refresh your memory. MazeWars is a standalone hunting game that takes place within a maze world. The maze is full of characters whose only interest is to prematurely end your life. Coming back to you? Good. In this chapter, we’re going to take MazeWars to the next level and create a networked version of the application. MazeWars presents a whole new set of challenges in the realm of network game programming. The continuous action of MazeWars will force us to explore new methods for connecting applications, or sessions, that are playing a shared game. We will also be examining how to integrate networking capability into an existing application rather than building a network game from the ground up. So, let us delve into the maze world once again and see how we can adapt it to the bigger picture of a networked gaming environment.
MAZEWARS NETWORK ARCHITECTURE The goal of our work on MazeWars is to enable two or more players to hunt each other within a shared maze environment. To accomplish this, we must make many things happen. We must create the communication between the different sessions within the shared game. We must also find an efficient means of transferring static and dynamic game information across the network. Figure 18.1 shows three sessions of network MazeWars in action.
Figure 18.1 Three MazeWars sessions in action. Previously, we have been using the star connection topology to link the Domination applets and DominationServer (see Chapter 15 for a discussion of connection topologies). This was very convenient because the DominationServer could centrally manage all the game states. The applets performed most of the user interaction, sending infrequent messages to the server to perform operations affecting the game state. The nature of Domination led to minor CPU use on both the clients and server, and relatively small network communication loads. The nature of MazeWars, however, is substantially different. Events are not triggered by a relatively slow human interaction with the user interface. Instead, MazeWars must pump out network events describing the motion of numerous actors running around the maze. Each actor is capable of moving every 75 milliseconds. This information must travel between the different MazeWars sessions and be rendered to the screen in a prompt manner or the players will get frustrated from playing a sluggish or, worse yet, desynchronized game.
Network support for the MazeWars application is provided by the GameLayer/GameEvent framework. The MazeWarsEvent class is used to transfer information back and forth between applications. To achieve improved network performance, we use a new connection topology—the interconnect topology. This topology is characterized by direct connections between each of the sessions playing the game. The first step on the road to Network MazeWars is providing a mechanism to get the MazeWars sessions to find each other.
CONNECTING TO A MAZEWARS GAME To connect the MazeWars sessions is not as trivial as you might expect. MazeWars will be an application, not an applet. Although this lifts the restrictions imposed on us by a browser’s security policy, we cannot find any computers running sessions of MazeWars without external help. The networking primitives within Java are based on sockets. The Java ServerSocket class allows an application to establish a connection point that other applications can attach to. This connection point is described by the IP address (or host name) of the computer where the application is running and a port number. The port number is nothing more than a pre-specified integer value. Given these two pieces of information, a Java socket can connect to another application though its ServerSocket. Unfortunately, even this simple operation poses a problem for the MazeWars application. We have no means of locating the IP addresses of other machines running sessions of MazeWars. There are no networking primitives that can “scan” the immediate area looking for machines running a MazeWars session. To solve this problem, we will create a MazeWarsServer application. THE MAZEWARSSERVER APPLICATION The MazeWarsServer application acts as a central registry for all of the games that people are playing. It tracks the game definition and the list of active players for each game. By centralizing this information within the server, we provide a locating service to new sessions. They may now list the games that the server has registered. The information managed by the MazeWarsServer application is presented to the player by the MazeWars Welcome dialog box, which is shown in Figure 18.2. While the dialog box is active, it registers with the MazeWarsServer so that its user interface can display an accurate summary of the games and players connected to the server.
Figure 18.2 The MazeWars Welcome dialog box in its Create and Join modes. A MazeWars application must know the IP address where it can connect to the MazeWarsServer. This approach is considerably simpler than locating individual MazeWars sessions because the server is normally configured to run on specific computers. The host name for the server is provided as an argument to the MazeWars application. The MazeWarsServer pales in comparison to DominationServer. To fulfill its humble role, the MazeWarsServer needs only to respond to the seven events listed in Table 18.1. Table 18.1MazeWarsServer event types.
Event Type
Description
C_SET_PLAYER_DATA
Sets the player information, including the player name, computer IP address, and port number
C_BROADCAST_GAMES_START
Starts informing the client of any changes to the list of active games and their players
C_BROADCAST_GAMES_STOP
Stops informing the client of any changes to the list of active games and their players
C_CREATE_NEW_GAME
Creates a new game within the server and registers the first player
C_JOIN_GAME
Registers a player with an existing game
CONNECTION_DROPPED and EXIT_APP
Removes the player from any active game and frees all resources associated with the player connection
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
When a MazeWars Welcome dialog box determines the name of the player, it informs the MazeWarsServer. The server receives a C_SET_PLAYER_DATA event to register the player name and the connection parameters for the session. After setting the player data the dialog box signals the server with a C_BROADCAST_GAMES_START event. This event will cause the server to inform the session of any changes to the active games and their players. When the Welcome dialog box has selected the game, it closes and sends a C_BROADCAST_GAMES_STOP event to prevent further game notifications. Of course, every good relationship is based on give-and-take. In return for the game/player information from the server, the session registers which game it is playing. If the player chose to create a new game, the server will receive a C_CREATE_NEW_GAME event. Joining an existing game is signaled by a C_JOIN_GAME event. The final communication between a session and the MazeWarsServer is the EXIT_APP event. This event is generated as part of the application terminating. The event triggers the session to be removed from a game’s player list and all network resources to be discarded. Internally, the MazeWarsServer application manages the list of games as a vector containing ServerGameData objects. These objects live independently from the players that “play” their games. The player who creates a game has no special status within the ServerGameData object. A ServerGameData object remains defined within the MazeWarsServer for a fixed period after the last player leaves the game. LIFE BEFORE THE WELCOME DIALOG BOX As the MazeWars application starts, it performs many initialization tasks. In Chapter 14, we discussed the initialization and game creation procedures for the standalone version MazeWars. In this section, we’ll take a look at the additional tasks that must be performed to initialize the networking elements. Our first step is a call to initNetworkConnections() from the MazeWars.init() method. The initNetworkConnections() method is split into two try blocks, as shown in Listing 18.1. The first is responsible for initializing the connection to the MazeWarsServer. The second creates a ConnectionManager object to allow other MazeWars sessions to connect to this session. Listing 18.1 MazeWars network initialization methods. private void initNetworkConnections( String hostName, int port ) throws IOException { InetAddress address = null; try { address = InetAddress.getByName( hostName ); initGameLayers( address, port ); } catch ( IOException e ) { String HostName = address == null ? "unknown" : address.toString(); appendToStatus( "* Connect failed: " + HostName + " Unable to play." ); throw e; } try { connectManager = new ConnectManager(); connectManager.startConnectionManager( 0 ); } catch ( IOException e ) {
appendToStatus( "* ConnectManager init failed. Playing Local Game" ); connectManager = null; } } private void initGameLayers( InetAddress address, int port ) throws IOException { connectionToServer = new ClientToServerConnection( ); networkGameLayer = new MazeWarsGameLayer(); connectionToServer.createUpperLayers(); connectionToServer.setServerAddress( address, port ); connectionToServer.openConnection( false, false ); connectionToServer.startAllLayers(); } Initialization of the server communication is performed in the call to initGameLayers(). This method creates two GameLayers. The first, connectionToServer, is a ConnectionLayer that manages the connection to the MazeWarsServer. The second, networkGameLayer, handles all the inbound network events for the game. The call to connectionToServer.createUpperLayers() merely installs the networkGameLayer object as the layer above the connection. All the connections to external sessions use networkGameLayer as their final layer also. The remainder of the method is dedicated to opening the connection and starting each of the layers/threads. The second try block within initNetworkConnections() creates and starts a ConnectionManager object. The GameLayer/ GameEvent framework uses the ConnectionManager object to encapsulate a Java ServerSocket. In essence, ConnectionManager automates the process of accepting a client connection request, initializing an appropriate ConnectionLayer object, installing the socket, and then starting the stack of GameLayers. We normally see the ConnectionManager in the server side of a client/server interface, and this is no exception. The ConnectionManager object will allow other sessions to connect directly to this application so they can pass game data on a dedicated path. The call to ConnectionManager.startConnectionManager() is passed a zero as the port ID, which causes the ServerSocket to create a socket on any free port. It is important to allow the MazeWars connection manager the ability to select a free port because you might wish to run multiple sessions of the game on a single machine. If you used a fixed port, the first game to start on a machine would claim the port number. Subsequent games would fail to initialize the ConnectionManager, or they might pre-empt the original game. Either way, only one of the sessions would function as desired. The final piece of network-related initialization is performed by the Welcome dialog box. When the player accepts a valid player name, the dialog box generates a C_SET_PLAYER_DATA event. This event contains the specified player name, the IP address of the local computer, and the port number that the ConnectionManager object was assigned. This information is sent to the server, where it is filed away for later use.
Previous Table of Contents Next
Cutting Edge Java Game Programming by Neil Bartlett and Simkin Coriolis, The Coriolis Group ISBN: 1883577985 Pub Date: 11/01/96
Previous Table of Contents Next
CREATING AND JOINING GAMES After MazeWars has initialized the network connections, the init() method creates and displays the Welcome dialog box. This dialog box initiates the startup logic for the application. The logic can take two very different paths to initializing the game state. Figure 18.3 shows the flow of the startup code.
Figure 18.3 Network MazeWars startup logic. If the player decides to create a new game, he will be asked to specify a name for the game and select the size of the maze. This information is later presented to players that are interested in joining the game. A player that chooses to join a game is allowed to view a list of active games. Each game shows the maze size, name, and the set of current players. This list is kept up-to-date by the notifications from MazeWarsServer.
Creating A MazeWars Game The process of creating a new MazeWars game is very similar to the standalone version. The maze is populated with walls and open space. Actors are added to provide some initial opponents. Prior to activating the game, two new methods are called. The createGameInServer() method is responsible for transferring the maze definition to the server, and initBroadcastThread() initializes the thread that will eventually update all external sessions with the dynamic game data. The createGameInServer() method creates and populates a C_CREATE_ NEW_GAME event, which contains the static definition of the maze. This definition includes the name of the new maze, the dimensions of the maze, and the type (open space, walls, corners, etc.) of each cell in the maze. C_CREATE_NEW_GAME is then sent to the MazeWarsServer. Upon receipt of this message, the server creates an object to store the information and assigns the player to the game. The second method, initBroadcastThread(), creates the thread that is responsible for writing a summary of the game’s dynamic information to each of the other game players. The thread is a BroadcastThread object. This object’s run() method tests if there are any sessions connected to the game; if there are, it will generate a summary of the actor locations and send the event to the external sessions.
Joining A MazeWars Game When the player clicks on the Join button in the Welcome dialog box, many new and exciting things happen. The event handler for the Join button validates that a game is selected. If it is, the entire Welcome dialog box is disabled, a C_JOIN_GAME event is created and sent to the server, and the status message “Waiting for maze download...” is displayed. The dialog box is disabled to signal to the user that they must wait for the reply from the server.
Upon receipt of the C_JOIN_GAME event, the server will validate the existence of the game. In the unlikely situation that the game has been removed, the server will reply with a S_JOIN_GAME_FAILED event, triggering the dialog box to enable the user interface and explain the problem in the status field. If the server accepts the request to play the game, it will generate a reply containing two events. The S_JOIN_GAME_CONFIRM event signals the acceptance of the join request. This event carries with it the static definition of the maze that was “uploaded” by a call to createGameInServer(). This data is used to initialize the Maze object so that it has the correct maze definition. The second event, S_GAME_PLAYER_LIST, carries the list of host names and port numbers of all the current players of the game. When the S_GAME_PLAYER_LIST arrives, it triggers the session to create a connection to all the existing sessions. This logic is captured in the methods shown in Listing 18.2. We can see how each host name and port is used to call connectToPlayer(). This method creates a ClientClientConnection object and instructs it to connect to the other MazeWars sessions. This connection process is responsible for creating the dedicated links among all the sessions playing a shared game of MazeWars. In effect, this is the implementation of the interconnect connection topology. Listing 18.2 Establishing client connections to existing game sessions. protected void onGamePlayerList( MazeWarsEvent e ) { int playerCount = e.getStringCount(); for ( int i=0 ; i