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 <4, 0.05, 0.05> 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 <3.5, 1, 1> 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
{ { { { {
<-50.0, 100, -80.0> colour White } <50.0, 25.0, -100.0> colour White } <-50.0, -25.0, 100.0> colour White } <-50.0, 25.0, 100.0> colour White } <50.0, -25.0, -100.0> colour White }
camera { location <0.0, -22.0, 0.0> up <0.0, 1.0, 0.0> right <4/3, 0.0, 0.0> look_at <0, 0, 0> }
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 << 24)); } } Unfortunately, once an image is created using Java, you can’t change the image. To use the AlphaFilter, we must create a new image from an existing image. The ImageManager provides a helper method—the createFadeImages shown in Listing 4.10—that uses the AlphaFilter to create several new images from one single image. The createFadeImage method produces an array of images. The first image is an opaque image, the last image in the array is transparent. Images in-between have regularly spaced Alpha values. Figure 4.9 shows the results of running createFadeImages on an image.
Figure 4.9 A fading sequence of images. Listing 4.10 ImageManager method to create faded images. Image[] createFadeImages(Image image, int n) { Image[] images = new Image[n]; images[0] = image; waitForImageLoad(images[0]);
for (int i = 1; i < n; ++i) { ImageFilter filter = new AlphaFilter(256-(i*256/n)); ImageProducer src=images[0].getSource(); ImageProducer prd=new FilteredImageSource(src, filter); images[i] = theGame.createImage(prd); waitForImageLoad(images[i]); } return images; } OTHER EFFECTS Now that we know how to get hold of the pixels of an image, there are plenty of other effects we can perform. All we need to do is come up with an algorithm to manipulate the pixels. For more ideas, you might consider reading Fundamentals of Computer Graphics by Foley and Van Dam, the classic work on graphics. For a more Java-related feel, try Tonny Espeset’s book Kick Ass Java (Coriolis Group Books, 1996).
THE BACKDROPMANAGER To manage the backdrop to the stage, we are going to use a backdrop manager. We could have represented the background as a single image; however, this approach would limit our future growth of the framework. Anyway, a moving backdrop can greatly enhance the dynamic effect of a game. Think about where all those Nintendo-style games would be without side-scrolling graphics. Backdrop managers all conform to the BackdropManager interface shown in Listing 14.11. This interface only requires a single method, paint, to implement it. The paint method is called by the stage manager to draw the backdrop. The backdrop manager, for its part, guarantees to call the stage manager’s backdropChanged method if the backdrop changes. By default, the Game class creates a backdrop manager called ImageBackdropManager, which is also shown in Listing 14.11. This backdrop manager automatically paints the backdrop as an image. Other backdrop managers can be created to paint other types of backdrops. For instance, GameWorks also provides ColoredBackdropManager which draws the backdrop as a filled, colored rectangle. Other examples that could be implemented are a star-field backdrop or a side-scrolling backdrop. Listing 4.11 The BackdropManager and the ImageBackdropManager. interface BackdropManager { void paint(Graphics g); } class ImageBackdropManager extends BackdropManager Game theGame; Image backdropImage; ImageBackdropManager(Game aGame) { theGame = aGame; }
{
public void setImage(String anImageName) { Image image; image = theGame.imageManager.getImage(anImageName); setImage(image); } public void setImage(Image anImage) { theGame.imageManager.waitForImageLoad(anImage); backdropImage = anImage; theGame.stageManager.backdropChanged(); } public void setTiled(String anImageName) { Image image; image = theGame.imageManager.getImage(anImageName);
setTiled(image); } public void setTiled(Image anImage) { theGame.imageManager.waitForImageLoad(anImage); setImage(theGame.imageManager.createTiledImage(anImage, theGame.stageManager.size().width, theGame.stageManager.size().height)); } public void paint(Graphics g) { g.drawImage(backdropImage, 0,0, theGame); } }
SUMMARY We can now draw the stage and the actors on the stage. We have configured GameWorks’ drawing support to lean toward images. Images are advantageous because they can be designed using sophisticated graphics tools, that improve game load times and make better-looking actors and backgrounds. Along the way, we encountered problems caused by Java’s asynchronous, demand-loading of images. We solved the problems and developed special effects that we can use to enhance our game play.
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 5 INPUT HANDLING NEIL BARTLETT
O
ur games would be pretty dull if we just let the actors do their own thing. After all, we want to play the game, not have the
game play itself. We, the omnipotent beings, need to exert some form of control over our actors. In this chapter, we look at how to interact with the actors in a game. We start by examining how the Java input mechanism works, then we work out how we can adapt it to fit into our overall game framework. By the end of the chapter, we will have developed a consistent approach to dealing with input—an approach that we will extend in later chapters. The Java AWT input mechanism supports both keyboard and mouse input. The input mechanism is designed to deliver this input to the AWT components; the input mechanism is tightly bound up with the Component and Container classes. We use these classes to represent the stage on which the actors will live. Unfortunately, as we have previously discussed, we do not use them to represent actors. Therefore, we can use the existing mechanism to deliver input to the stage, but we will have to design an alternative mechanism to deliver input to the actors themselves. We will only be covering keyboard and mouse input. The standard Java input mechanism does not support any other input devices. It is possible to write platform-specific input device handlers, such as a joystick controller for Windows, to extend the input device support. The joystick controller, for instance, could be implemented as a native method running in its own thread. However, this would mean that the code would only work on the Windows platform. If we wanted to use the joystick feature on other platforms, we would need to write code for each platform we wished to support. The new input device handler would also not work with Web browsers which have strict security policies. For these reasons, I have decided to stick with using the standard input devices. Are you ready to get to the heart of the design? Hold on. First, we need to cover how the existing Java input mechanism works.
COMPONENT EVENT HANDLING User input, such as pressing keyboard keys, clicking mouse buttons, or moving the mouse, generates events. These events are passed on to the Java AWT user interface components. Events are received by a component by overriding event handler methods. Each event handler method corresponds to a specific user interaction. A list of the event handler methods for the mouse and for the keyboard is shown in Table 5.1. Table 5.1Mouse and keyboard event handler methods.
Event Handler
Called When
mouseEnter
The mouse enters the canvas
mouseExit
The mouse leaves the canvas
mouseMove
The mouse is moved and no mouse button is pressed
mouseDown
The mouse button is pressed
mouseDrag
The mouse is moved with the mouse button down
mouseUp
The mouse is released
keyDown
A key is depressed
keyUp
A key is released
If a component is interested in an event, it will implement the event handler method and return true; otherwise, the JDK will provide a default event handler that will return false, indicating that the component is not interested in the event. The basis of user interaction is, therefore, to override the event handlers for the events that need to be reacted to, do the work to react to the event, and then return true from the event handler. THE HANDLEEVENT METHOD The master event handler, the handleEvent method, is responsible for calling the event handler methods, such as mouseEnter. The handleEvent method can be used to alter the default behavior. It can also be used to implement generic event handling. As we will see in a moment, handleEvent is very important because it is the entry point event handler: The first event handler that is called for any component for any event. EVENT OBJECTS When an event handler method is called, an object called an event object is passed in as a parameter to the method. The event object is an instance of the Event class. It is generated by the Java runtime system whenever input occurs. Event objects contain a complete description of the user input. For instance, a keyboard key will generate a keyboard event. The keyboard event will contain information about what happened, including which key was pressed, which keyboard modifier keys were pressed at the same time as the key, and at what time the key was pressed. The Event class source code is contained in Java source that ships with each Java release. The Event class, which is found in the file src/java/awt/Event.java, contains: • • • • • • •
The type of the event (keyboard, mouse, change focus, etc.) Keyboard constants for decoding which key and modifiers were pressed Timestamp of when the event occurred The target component for the event Positional information; used for locatable events, such as mouse events A general object for carrying event specific information Helper methods
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
TYPES OF EVENTS You might imagine that an object-oriented system like Java would include one class for each type of event that can occur—a KeyDownEvent class, a MouseMoveEvent class, etc. However, this is not the case with Java; it has just one event class, Event. To find out what type of event occurred, you must decode the type yourself. The type of event is determined by the value of the int id member variable of the Event class. The Event class contains a list of constant values that id can take. Each constant specifies a different event type. By comparing the value of id with the constants, we can determine what event occurred. Table 5.2 shows some of the constant values. Table 5.2Event idvalues.
Event Name
Event id Value
ACTION_EVENT
1001
GOT_FOCUS
1004
KEY_ACTION
403
KEY_ACTION_RELEASE
404
KEY_PRESS
401
KEY_RELEASE
402
LIST_DESELECT
702
LIST_SELECT
701
LOAD_FILE
1002
LOST_FOCUS
1005
MOUSE_DOWN
501
MOUSE_DRAG
506
MOUSE_ENTER
504
MOUSE_EXIT
505
MOUSE_MOVE
503
MOUSE_UP
503
SAVE_FILE
1003
SCROLL_ABSOLUTE
605
SCROLL_LINE_DOWN
602
SCROLL_LINE_UP
601
SCROLL_PAGE_DOWN
604
SCROLL_PAGE_UP
603
WINDOW_DEICONIFY
204
WINDOW_DESTROY
201
WINDOW_EXPOSE
202
WINDOW_ICONIFY
203
WINDOW_MOVED
205
For each hardware interaction that happens, an event is generated. Events can be very granular. For instance, a keyboard event is generated on both the up stroke and the down stroke of a key. A keyboard event is also generated each time the keyboard auto-repeat happens. HOW A COMPONENT RECEIVES AN EVENT Take a look at Figure 5.1 to see how the events flow through the user interface looking for a component to handle the event.
Figure 5.1 Event flow. Events are generated by the system and are first sent to a target component. Typically, the target component is the component that has the keyboard focus. When the target component receives an event, it initiates a drill-down/drill-up algorithm to search for the component that wants to handle the event. The drill-down/drill-up algorithm selects components one-by-one. It calls each component’s event handler method until one returns true. If no event handler method returns true, the algorithm stops when the outer-most containing component is reached. The drill-down/drill-up algorithm works first by drilling down to the lowest level of component inside the target component that contains the event. For example, if the target component is a container, such as a Panel, it will pass the event down to the lowestlevel component inside that container. The algorithm finds the lowest-level component by looking at the positional information stored inside the event. It then looks to see which component contains this position. This is the component that gets a chance to handle the event first. If this component does not handle the event, the drill-up portion of the algorithm is then started. The drill-up works by simply finding the parent of the current component and calling its handleEvent method. It stops when the handleEvent call returns true or the high-level parent is reached. One side effect of this form of component selection is that some components may not get to see an event. If a component does not contain the drilled-down-to component, then that component will not see the event even if no other component wants it. EVENT THREADING One thing we need to consider carefully is the time when events are generated. Events are generated in a multithreaded environment. All operating system activity runs in a system thread. Event generation is no different. All events are generated in the system thread. Consequently, events may be considered as happening asynchronously.
GAME FRAMEWORK EVENT HANDLING Now that we have an understanding of how the existing event mechanism works, let’s look at how we can adapt it to the task at hand. We need to design the framework so that actors can receive input. We need to support a wide variety of input. The key features we need are: • Dynamic actors can receive events quickly
• Focus should not determine which actor gets the event • Existing Java GUI components can take part in this system How much of the existing input code can we reuse? Regardless of how we internally deal with events, the Java toolkit will be delivering us event objects. Therefore, it makes sense that we reuse this concept. Also, the concept of the handleEvent method is simple. An actor will have a handleEvent method. When it receives an event, it can decode the event in the same way that a component would. Conversely, what existing code will we not use? Components use a focus model to decide which component to send an event to. An event, such as a mouse or a keyboard event, is only sent to the component that has keyboard focus. Is this something desirable for a game-based system? Not really. The concept of an actor having the focus is not something we will need. Therefore, we can drop the concept of focus and the drill-down/drill-up mechanism that is used to send events to components. We can adopt what I will call non-focused event distribution. By this, I mean that all events coming into the game are sent to all the actors that wish to see them—not just those that are in a particular place on the screen.
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
AN EVENTMANAGER Our event handling will use the same event mechanism as components, but without the concept of focus. Events will come into the system and be distributed to the actors. The distribution will happen via an EventManager object. The EventManager will be responsible for implementing the policy of the event management, that is, which actors receive the events. Figure 5.2 shows the CRC card for the EventManager.
Figure 5.2 EventManager CRC card. This approach to event management has a number of advantages: • The EventManager can provide services that are not part of the standard services, such as double click detection. • The EventManager is a highly defined role. We can implement different event manager algorithms, each fine-tuned to a game. The game might have a lot of actors, only one actor (the player’s actor) needs to see all the events. For this situation, we can implement an event clipping strategy where only “registered” actors see events. • We can change the EventManager object at runtime. A game—as it changes phases—can use different event models. Consider a game that allows for different perspectives on the same scene. It might be that you are in charge of a number of space vehicles. From one view, you can alter the formation of the group of space vehicles. From another view, you might be in control of a single space vehicle within the formation. Each view might require a different distribution of events. In the first case, each of the vehicles will see the events that are sent. In the second situation, only the chosen vehicle will see the events. By allowing the EventManager to be changed at runtime, we can very cleanly write code to change the event distribution without having to clutter up the actor event code with knowledge of which view is being used. Figure 5.3 shows an object message diagram for event handling. It shows that GameApplet and GamePanel generate handleEvent calls to the EventManager. The EventManager then calls event handlers for each of the registered EventInterface objects. The Game object contains a handle to the current EventManager. By changing the handle, the Game object can change how event handling occurs. The CRC card for the EventInterface is shown in Figure 5.4.
Figure 5.3 An event processing object message diagram.
Figure 5.4 EventInterface CRC card. Listing 5.1 shows the default EventManager. This is the standard implementation; other event managers can be written for more
specific needs. Listing 5.1 EventManager.java. import java.awt.*; import java.util.*; /** * A Default Event Handler * * @version 1.0, 31/04/96 * @author Neil Bartlett */ class EventManager { /** * The list of objects to notify of events. */ protected Vector objs; /** * The last time a mouse was clicked. */ static long lastMouseDownTime=0; /** * Time interval to qualify for a double click. */ final static long DOUBLE_CLICK_TIME=500; //milliseconds; /** * The Enter/Return Key */ public static final int KEY_ENTER=10; /** * The TAB Key */ public static final int KEY_TAB=9; /** * The Escape Key */ public static final int KEY_ESCAPE=27; /** * The Delete Key */ public static final int KEY_DELETE=127; /** * Constructs a new EventManager. */ public EventManager() { objs = new Vector(); } /** * Adds an object to the list of objects * notified when an event happens. * @param anObject the object to notify * @see #removeNotification */ public void addNotification(Object anObject) { objs.addElement(anObject); } /** * Removes an object from the list of objects * notified when an event happens. It is not an * error to remove an object that is not in the list.
* @param anObject the object to remove * @see #removeNotification */ public void removeNotification(Object anObject) { objs.removeElement(anObject); } /** * Handles the event. Calls the appropriate helper * function of each object in the list of objects * to be notified. * @param anEvent the event */ public boolean handleEvent (Event anEvent) { boolean ret=false; for (int i = 0; i < objs.size(); i++) { EventInterface ei = (EventInterface) objs.elementAt(i); ret = callEventHelper(ei, anEvent); } return ret; } /** * Handles the event. Calls the appropriate helper * function of each object in the list of objects * to be notified. * @param anEvent the event */ boolean callEventHelper(EventInterface ei, Event aEv) { switch (aEv.id) { case Event.MOUSE_MOVE: return ei.mouseMove(aEv, aEv.x, aEv.y); case Event.MOUSE_DOWN: long l; l = lastMouseDownTime; lastMouseDownTime = aEv.when; if ((lastMouseDownTime - l) < DOUBLE_CLICK_TIME) return ei.mouseDoubleClick(aEv, aEv.x, aEv.y); else return ei.mouseDown(aEv, aEv.x, aEv.y); case Event.MOUSE_DRAG: return ei.mouseDrag(aEv, aEv.x, aEv.y); case Event.MOUSE_UP: return ei.mouseUp(aEv, aEv.x, aEv.y); case Event.KEY_ACTION: case Event.KEY_PRESS: return ei.keyDown(aEv, aEv.key); case Event.KEY_ACTION_RELEASE: case Event.KEY_RELEASE: return ei.keyUp(aEv, aEv.key); case Event.MOUSE_ENTER: return ei.mouseEnter(aEv, aEv.x, aEv.y); case Event.MOUSE_EXIT: return ei.mouseExit(aEv, aEv.x, aEv.y); default: return ei.handleEvent(aEv); } } } The EventManager is instantiated in the Game object when the game is initialized. The Game object supports a method called createEventHandler:
protected void createEventManager() { eventManager = new EventManager(); }
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
This method is called from the Game object’s init method. Games that subclass the Game class can override the createEventHandler method if they wish to install a different default event manager. When an actor is created, if it wants events, it registers itself with EventManager: class TestActor extends Actor implements EventInterface { TestActor(Game aGame) { theGame.eventManager.addNotification(this); } } Similarly, when the actor is removed from the game, it will remove itself from the EventManager. The main purpose of the EventManager is to implement the handleEvent method. This method is called via either the GameApplet or the GamePanel. handleEvent then calls the appropriate event handler for each object in the list of registered objects. Each of these objects must support the EventInterface interface shown in Listing 5.2. Listing 5.2 EventInterface.java. import java.awt.*; interface EventInterface { /** * Handles the event. Returns true if the event is handled. * @param anEvent the event */ public boolean handleEvent(Event anEvent); /** * Called when the mouse enters the component. * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseEnter(Event anEvent, int x, int y); /** * Called when the mouse exits the component. * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseExit(Event anEvent, int x, int y); /** * Called if the mouse moves (the mouse button is up).
* @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseMove(Event anEvent, int x, int y); /** * Called if the mouse is down. * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseDown(Event anEvent, int x, int y); /** * Called if the mouse is double clicked. * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseDoubleClick(Event anEvent, int x, int y); /** * Called if the mouse is dragged (the mouse button is down). * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseDrag(Event anEvent, int x, int y); /** * Called if the mouse is up. * @param anEvent the event * @param x the x coordinate * @param y the y coordinate * @see #handleEvent */ public boolean mouseUp(Event anEvent, int x, int y); /** * Called if a character is pressed. * @param anEvent the event * @param key the key that's pressed * @see #handleEvent */ public boolean keyDown(Event anEvent, int key); /** * Called if a character is released. * @param anEvent the event * @param key the key that's released * @see #handleEvent */ public boolean keyUp(Event anEvent, int key); }
TIP: Inheriting And The implements Keyword Don’t forget to include the implements keyword on subclasses when inheriting. The implements keyword is not inherited. If you want to cast a subclass to be the same as an interface, you must add implements to each level of the
inheritance. If you do not do this, you will get a cast exception.
One noteworthy point: The EventManager does not deal directly with the actors in the game. It works on objects. These objects are stored in a Vector. Why not use actors directly? Well, we’re trying to keep EventManager as general as possible. The idea is that you want to send events to something other than actors. You might want to send events to user interface components, like an airplane control panel, or you might want to send the events to a more abstract piece of code, such as an airplane cockpit view switcher, that changes the view of the game that the player sees. Neither of these pieces of code are actors, but they both want to be able to see events. Therefore, the EventManager is written with these other objects in mind. This idea is summed up in Figure 5.5.
Figure 5.5 Event distribution.
Java Perks: Is handleEvent A Good Name To Use? We used the name handleEvent for the method in the EventInterface class. You might be thinking that this is not such a hot idea because the name matches the one used by the Java AWT components. Won’t it conflict if we want to incorporate a component as an EventInterface? The answer is no. Java will correctly call handleEvent in each case, even though the handleEvent is “defined” in two places. Consider the following code: class BaseClass { public void aMethod(String s) { System.out.println("Base class called"); } } interface InterfaceClass { public void aMethod(String s); } class DerivedClass extends BaseClass implements InterfaceClass { public void aMethod(String s) { System.out.println("Derived Class aMethod called via "+s); } } class Test { public static void main(String args[]) { DerivedClass d = new DerivedClass(); InterfaceClass i = (InterfaceClass) d; BaseClass b = (BaseClass) d; d.aMethod("Derived Class"); i.aMethod("Interface"); b.aMethod("Base class"); } } Assume that each class is in its own file. DerivedClass inherits a method called aMethod from BaseClass. However, DerivedClass also implements the interface InterfaceClass, which also has a method with the same signature as aMethod. In other words, DerivedClass gets the aMethod signature from both BaseClass and InterfaceClass. Java allows the interface and the base class methods to share the same name. When you run the program, you get this output: D:\Projects\GameBook\DEV\tip1>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 <= 126) str += new Character((char) key).toString()+" ("+key+")"; else str += "Ascii Value "+key; System.out.println(str); return true; } } Let’s look at how the decode works step by step.
Decoding Special Keys The first part of the decode is to look for the action keys, which include such keys as the Home key or the arrow keys. The Event class contains a list of constant values for the action keys. Table 5.3 shows these values. Table 5.3The action key values.
Name
Value
Meaning
HOME
1000
Home Key
END
1001
End-Of-Line Key
PGUP
1002
Page Up Key
PGDN
1003
Page Down Key
UP
1004
Up Arrow
DOWN
1005
Down Arrow
LEFT
1006
Left Arrow
RIGHT
1007
Right Arrow
F1
1008
Function Key 1
F2
1009
Function Key 2
F3
1010
Function Key 3
F4
1011
Function Key 4
F5
1012
Function Key 5
F6
1013
Function Key 6
F7
1014
Function Key 7
F8
1015
Function Key 8
F9
1016
Function Key 9
F10
1017
Function Key 10
F11
1018
Function Key 11
F12
1019
Function Key 12
I have decided in the EventManager class to fold in the action keys with the ‘normal’ keys. Therefore, the action keys are sent to the keyDown and keyUp methods just like any other key. The special key decode method, isActionKey, simply uses a switch statement to test the key value for each action key value in turn. You may notice that the table lacks a few interesting keys, such as Enter and Tab. It is possible to decode these values. We will look at this issue shortly.
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
Decoding The Key The KeyTestActor only attempts to decode the ASCII values. It does not attempt to decode the full Unicode character set. ASCII values are a subset of the Unicode standard. ASCII values assign values to the first 256 characters to the Unicode standard. ASCII assigns values to all the standard keyboard keys, including A through Z, a through z, 0 through 9, and all the punctuation characters. To save us from having to know what all the ASCII values translate to, we can use the Character class’s toString method. We can cast the key value to a char value and use the toString method to decode the value to a String value we can recognize.
TIP: Decoding Keys During Game Play During game play, you will not normally do a full decode of the value. Typically, you will place the value of the key you want to detect (for example, 97 for the a key) in the keyDown method. This technique removes the overhead of the translation using new Character((char) key).toString(). You can use the KeyTestActor to determine the values to use.
I have deliberately restricted the key values that get passed to the Character.toString method to be between 32 and 126. This is the range of values for the punctuation and alphanumeric keys. Key values below 32 and above 126 are used to represent other keyboard values. Table 5.4 lists some of these values and the keys they represent. Table 5.4Useful values of extra keys.
Key
Decimal Value
Convenience Symbol
Tab
9
EventManager.KEY_TAB
Delete
8
EventManager.KEY_DELETE
Enter
10
EventManager.KEY_ENTER
Escape
27
EventManager.KEY_ESCAPE
You can use these values to test for the specific keys. Unlike the action keys, the Event class does not store named values for these keys. Therefore, as an added convenience, I have added these values to the EventManager class. Table 5.4 also shows these values. You can then use them directly in your code to test for keys: public boolean keyDown(Event anEvent, int aKey) { boolean ret=false; if (aKey == EventManager.KEY_ENTER) ret = onEnterKeyPressed(); return ret; }
Decoding The Modifiers
In addition to the key value, an event object has a public variable called modifiers. This variable shows which keyboard modifiers (shift, control, or meta keys) were depressed at the time the event happened. As an example, in Listing 5.3, the haveModifiers method uses the modifiers variable to decode which modifiers were pressed when an event occurred.
MOUSE EVENTS The Java event mechanism uses a least-common denominator approach to the mouse. Rather than allowing the mouse to be recognized as a multiple-button mouse, the mouse is modeled as a single-button device. The same events will be generated regardless of which mouse button is pressed. The purpose of this restriction is to ensure that single-button mouse machines, such as the Mac, are treated in the same fashion as multiple-button mouse machines. Remember, the Java libraries are trying their best to work on as many platforms as possible. DETECTING A SINGLE MOUSE CLICK Mouse button clicking is detected by the mouseDown and mouseUp events. To process these events, simply override the mouseDown or the mouseUp methods to do what you want to do and then return a boolean value of true.
Java Perks: Breaking The One-Button Model I don’t recommend this, but if it is truly essential that you provide multiple-mouse-button support, you can write code that takes a good guess at which mouse button was pressed. I say a good guess because there are configurations of the mouse and the keyboard that can fool the code. The mouse button can be detected by looking at the event object modifiers variable when the mouse event happens. The modifiers variable is set to 0 for the left mouse button and 2 for the right button. Note, however, that this technique conflicts with the setting of the modifiers for some of the keyboard modifiers—pressing the keyboard modifier and pressing the left mouse button will give a false right mouse button press. This approach, which I have used successfully only on NT and Windows 95, will not work on all machines, especially the Mac. The mechanism shown next is a standalone program called WhichButtonTest.java. Compile the program with javac, and run it with java. import java.awt.*; class WhichButtonCanvas extends Canvas { public boolean mouseDown(Event evt, int x, int y) { System.out.println(evt.toString()); System.out.println("Key "+evt.key); System.out.println("Modifiers "+evt.modifiers); if (evt.arg != null) System.out.println(evt.arg.toString()); return false; } } public class WhichButtonTest extends Frame { public WhichButtonTest() { setLayout( new BorderLayout()); add("Center", new WhichButtonCanvas()); resize(550, 550); show(); } public static void main(String args[]) { Frame f = new WhichButtonTest(); } } When the code is run, a frame window is displayed. Click the mouse buttons in the window, and the code will write out to
standard output which mouse button it believes was pressed.
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
DETECTING A MOUSE DOUBLE CLICK You might have noticed that the mouse events are rather lacking in a double click event—and you would be correct. If we want to detect double clicks, we have to do so ourselves. To test for this condition, we need to use the timestamp that it placed inside events. All events are timestamped with a count of the millisecond at which the event occurred. The timestamp is recorded by the Event class’s when public variable. To detect a double click, we can look at two incoming Event.MOUSE_DOWN events and determine if they happened inside of a brief enough period of time. If they did, we can call this a double click. The typical time interval for a double click is about 500 milliseconds. Listing 5.4 shows how I have incorporated this into the framework by adding a variable to track the last time a button was pressed, a constant to store the maximum interval value, and some code to call the mouseDoubleClick method in the EventInterface. Listing 5.4 Double click detection. class EventManager { static long lastMouseDownTime=0; static long clickInterval=0; static boolean isDoubleClick=false; final static long DOUBLE_CLICK_TIME=500; public boolean handleEvent (Event anEvent) { boolean ret=false; if (anEvent.id == Event.MOUSE_DOWN) { clickInterval = anEvent.when-lastMouseDownTime; isDoubleClick = (clickInterval < DOUBLE_CLICK_TIME); // stop the third click being a double click lastMouseDownTime = (isDoubleClick) ? 0 : anEvent.when; } for (int i = 0; i < objs.size(); i++) { EventInterface ei = (EventInterface) objs.elementAt(i); ret = callEventHelper(ei, anEvent); } return ret; } boolean callEventHelper(EventInterface ei, Event aEv) { switch (aEv.id) { case Event.MOUSE_DOWN: if (isDoubleClick) return ei.mouseDoubleClick(aEv, aEv.x, aEv.y); else return ei.mouseDown(aEv, aEv.x, aEv.y); } } }
Notice that this double click mechanism is really only detecting a mouse button press followed quickly by another mouse button press. On a multiple-button mouse, if the one mouse button is clicked quickly followed by another mouse button, a double click will be registered. One deficiency of this double click detection mechanism is that it is not integrated with the operating system. Quite often operating systems, such as Windows, will provide a utility to set the double click duration because the desired speed of a double click is often a matter of personal taste. Java does not provide an interface to such programs. Therefore, Java-based programs will not observe the setting provided by this utility.
SUMMARY We looked at input, focusing on keyboard and mouse input. Then, we developed an input event handling scheme for actors that is both flexible and easy to implement. In the next chapter, we will look at the issues of following the mouse around. In particular, we will look at drag-and-drop.
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 6 MOVEMENT NEIL BARTLETT
I
magine a game with no movement. Not much of a game is it? Movement is fundamental to any game. Even something as static as
a Spassky versus Fischer chess game is still highly dependent on the eventual movement of a chess piece. Movement is a complex design issue. It can range from the simple dragging of an actor using the mouse, to artificial intelligenceenhanced actor movement. Such movement, of course, requires heavy-duty algorithms to allow the performance of the movement to be good enough for reasonable games play. We’re not quite there, yet. We are going to start first with the simple stuff. We will be picking up an actor, making its movement smooth, and putting it down where we want it. In short, we will be exploring all the types of movement that you will need to manage board games and card games.
MOVING AN ACTOR To move anything around the screen, there are two basics steps: 1. Position the actor 2. Redraw the stage Nothing could be simpler. Indeed, our actors already support positioning and drawing. All we need to do is hook in the input stuff from the last chapter and away we go. Sounds like this will be a very short chapter. Fooled you. This chapter is going to be just as long (hopefully, enjoyably so) a chapter as usual. The reason can be stated in one word: performance. As soon as your game incorporates more than a couple of actors, actor movement seriously slows down the game. Each actor takes a certain amount of time to draw. The more actors, the more time it takes to draw, and the slower the game appears. We’ll look at a collection of techniques which we can incorporate into the framework to improve performance. Using these techniques, the performance will be acceptable, even for a relatively large number of actors. For now, though, we will ignore the debilitating effects of performance and implement a generic drag-and-drop mechanism available to all the actors in the framework.
DRAG-AND-DROP Drag-and-drop means that we can select an actor from the stage and move it by dragging it to a different part of the stage. Drag-anddrop is very useful in interactive games, such as card games and board games, where pieces need to be picked up and placed on a
different location on the stage. There are three phases to a drag-and-drop cycle, as illustrated in Figure 6.1: • Pick the actor • Drag the actor • Drop the actor
Figure 6.1 Drag-and-drop phases. We are going to incorporate drag-and-drop into the framework. We are going to design it in such a way that all actors can be dragged and dropped. DRAG-AND-DROP MANAGER In the true spirit of this games framework, we will place the handling of the drag-and-drop under the auspices of a drag-and-drop manager. The drag-and-drop manager is responsible for initiating the drag-and-drop, feeding back update positions to the actor, and figuring out where it is acceptable to put down the actor. The CRC card for the DragDropManager is shown in Figure 6.2.
Figure 6.2 DragDropManager CRC card. The drag-and-drop manager interfaces with a number of other classes. The interactions between the DragDropManager and these classes is shown in Figure 6.3. We will be looking at the interactions in the next few sections of this chapter.
Figure 6.3 DragDropManager message interactions. DETECTING INPUT To receive input, the drag-and-drop manager is registered with the EventManager. This registration is done by the game object. It will automatically create and register a drag-and-drop manager using the factory method, createDragDropManager. Games such as arcade style games will not require a drag-and-drop manager. These games can simply override the factory method to do nothing. The salient features from the Game class are shown in Listing 6.1. Listing 6.1 Partial Game class. class Game { DragDropManager dragDropManager=null; void init() { createDragDropManager(); } protected void createDragDropManager() {
dragDropManager = new DragDropManager(this); eventManager.addNotification(dragDropManager); } } The input events we are going to use to control the drag-and-drop mechanism are the mouseDown, mouseDrag, and mouseUp events—mouseDown initiates the drag-and-drop, mouseDrag controls the movement as the actor is dragged, and mouseDown terminates the drag-and-drop. One point to remember is that this is only one set of possible control events. We could also design a drag-and-drop manager in which a letter is used to pick an actor for dragging, the arrow keys are used to move the actor around, and the enter key is used to drop the actor. We will design the drag-and-drop manager so that it supports our default case directly, but also so that it is easy to configure it to work with other schemes. The drag-and-drop manager implements three protected methods to control the three phases of a drag: startDrag, dragTo, and stopDrag. These methods are called by the appropriate event handler for the events that control the phases of the drag-and-drop. For our scenario, we will use the following code: public boolean mouseDown(Event anEvent, int x, int y) { return startDragAt(x, y); } public boolean mouseDrag(Event anEvent, int x, int y) { return dragTo(x, y); } public boolean mouseUp(Event anEvent, int x, int y) { return stopDrag(x, y); } If other events were used to control the drag-and-drop, we would place the calls to the methods in the event handler for that event.
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
STARTING THE DRAG The first thing to do is to determine which actors to drag. The fundamental technique for doing this is called picking. The process of picking is to ask each of the actors in turn whether it is the actor that the user picked. For instance, if we are doing a mouse-based pick, we would ask each of the actors in turn if any part of them is at the mouse point. Of course, if actors overlap, a number of actors may be under the mouse. To help with this, the pick chooses the first actor that meets the requirements. This means that the order in which we ask the actors is important. We will come back to this point later in this chapter when we look at Z-order. For now, assume that the actors are in the correct order. The pick is accomplished with the aid of the actor manager. The drag-and-drop manager asks the actor manager for a list of all of the actors under the mouse point. The actor manager in turn asks each of the actors in its acto†e actors list if any part of them contains the mouse point. The code for the pick, from the ActorManager class, is shown in Listing 6.2. Listing 6.2 Picking an actor. Vector actorsAt(int x, int y) { Vector picklist = new Vector(); for (int i = 0; i < actors.size(); i++) { Actor a = (Actor) actors.elementAt(i); if (a.containsPoint(x, y)) picklist.addElement(a); } return picklist; } Once the drag-and-drop manager has the actor, it can set up for the drag. The complete startDrag method, from the DragDropManager, is shown in Listing 6.3. Listing 6.3 Starting the drag. protected boolean startDragAt(int x, int y) { Vector picklist = theGame.actorManager.actorsAt(x, y); for (int i = picklist.size() - 1; i >=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
How quickly each object is drawn How many objects are drawn How much of each object is drawn The total number of drawing operations
The more we can reduce each of these effects, the faster our redraw will be. Remember, though, that there is a law of diminishing returns. Once we get to the 30 redraws per second region, we have no more need to improve the redraw times. Happily for the algorithm-hungry people, there is often plenty of need for tricks just to get the speed close to 30 frames per second.
First, let’s explore how many objects are drawn. The big gain that we can make with the framework is to reduce the number of objects that we draw. There are two key features that we can take into account here: visibility and whether the object is moving. Clearly, if an actor is not visible—because it is obscured behind another actor—then we should not draw it. For example, consider a card game. We typically have at least 52 actors on the stage: the cards. If we take a straight redraw scenario, we will always draw all 52 cards to the screen (double-buffered, of course). However, even on a fast Pentium running the appletviewer, this is unacceptable. A dragged card will quickly lose pace with the mouse. The thing to note, though, is that often most of the cards are not visible—they are piled under other cards in stacks. By making the cards that are in piles marked as invisible, we can dramatically reduce the number of cards that are redrawn.
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
FIXED-ACTOR CACHING When dealing with more static games, such as board games and card games, the biggest win comes from this simple rule: “If an actor is not moving then incorporate the actor as part of the background.” Why is this such a big speedup? Again, consider a card game. If we are dragging only a single card, then the fixed-actor rule will give us an over 50 times redraw improvement: We don’t redraw 51 of the cards. This is significant. It is the difference between being shown a series of holiday photographs once every two seconds or being shown a video of the holiday. The only deficiency with fixed-actor image caching is that it is only useful in situations where there are fixed actors (board games and card games). Most arcade style games don’t have too many fixed actors. Listing 6.7 shows the StageManager upgraded to incorporate fixed actors. The background and the fixed actors are all drawn—in the appropriate order—into the fixedActorsImage. This image is used rather like the double-buffered offscreenImage. We create an image, and we get a graphics context to draw into the image. The timing of when the background and the fixed actors are drawn into the fixedActorsImage is controlled by the redoFixed boolean variable. If this variable is set to be true, then the fixedActorImage will be refreshed on the next paint. The redoFixed image is set to be true when any of the following conditions are met: • When the StageManager is initialized • When an actor changes its fixedness • When the background changes To ensure that the actors cooperate in our endeavor, I have added a couple of things to the Actor class: the boolean variable fixed and the method setFixed, which will inform the StageManager whenever the fixed variable is changed: protected boolean fixed = true; public void setFixed( boolean newvalue) { fixed = newvalue; theGame.stageManager.actorChangedFixed(); } I have then added calls to setFixed in the startDrag and stopDrag methods. The startDrag method sets fixed to false and the stopDrag method sets fixed to true. This means that whenever dragging starts or stops, the complete scene will be redrawn. Listing 6.7 Highly optimized StageManager. class StageManager extends Canvas { protected Game theGame; private Image offscreenImage; private Graphics offscreenGC;
private boolean redoFixed=true; private Image fixedActorsImage; private Graphics fixedActorsGC; public StageManager( Game aGame ) { theGame = aGame; offscreenImage = theGame.gameAdapter.createImage( size().width, size().height); offscreenGC = offscreenImage.getGraphics(); fixedActorsImage = theGame.gameAdapter.createImage( size().width, size().height); fixedActorsGC = fixedActorsImage.getGraphics(); } Dimension size() { return theGame.stage.size(); } void actorChangedFixed() { redoFixed = true; } public void backdropChanged() { redoFixed = true } public void paint( Graphics g) { if (redoFixed) { fixedActorsGC.drawImage( theGame.backdropManager.getBackdrop(), 0, 0, theGame.stage); for (int i = 0; i < theGame.actorManager.numActors(); i++) { Actor actor = theGame.actorManager.actor(i); if (actor.visible && actor.fixed) { actor.paint(fixedActorsGC); } } redoFixed = false; } offscreenGC.drawImage(fixedActorsImage,0,0,theGame.stage); for (int i = 0; i < theGame.actorManager.numActors(); i++) { Actor actor = theGame.actorManager.actor(i); if (actor.visible && !actor.fixed) { actor.paint(offscreenGC); } } g.drawImage(offscreenImage, 0, 0, theGame.stage); } } MORE OPTIMIZATION A careful study will reveal that there is still some more optimization to be had. You will notice that we are copying the whole of offscreenImage to the screen. Unless the redoFixed code has been activated, this is copying more than we need. When the object is dragged, we need only copy the area covering the new position of the actor and the area covering the repaired area of the background from the old position, as shown in Figure 6.6.
Figure 6.6 Making minimal repairs. We won’t go into detail with this problem here. However, I will be supplying a neat solution—dirty rectangles—in Chapter 8, the chapter about sprites. Dirty rectangles allow us to only draw the area that has changed. The technique also copes with more than one moving actor, which is why we will be examining it in the sprites chapter.
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
LAYERING A common problem with actors is knowing who is on top of whom. I’m not talking Hollywood gossip here, I’m talking about the drawing order of the actors. If the actors are allowed to overlap, we need to know which must be drawn on top. This is especially true when we can drag actors and drop them on top of each other. In these circumstances, the order of drawing the actors will change. We need a way of modeling this so that we always can draw and pick actors in the correct order. This is very important when dealing with card games and games like Mahjong where there are lots of overlapping actors. To deal with this situation, we’ll take a leaf out of a windowing system programming manual—literally. If you think about it, overlapping windows are very similar in concept to what I have just described. Admittedly, actors are not always rectangular like windows, but the problem is the same. The way windowing systems solve this issue is to maintain what is known as a Z-order for each window. The Z-order is simply an integer value that signifies which plane a window is on. Windows with low Z-order are near the back planes, windows with high Zorder are near the front planes. A window with a higher Z-order than another will sit on top of the other window. Figure 6.7 shows the Z-order concept.
Figure 6.7 Scattered cards with Z-ordering applied. Before we draw the actors, we need to sort the actors according to their Z-order. We draw those actors with lower Z-order first so that the higher Z-order actors will be drawn over the top of them if they overlap. Listing 6.8 shows an actorManager method that sorts the active actors list by Z-order. Listing 6.8 Sorting by Z-order. public final void sortByZOrder() { for (int i = 1; i < actors.size(); ++i) { Actor ai = (Actor) actors.elementAt(i); int j; for (j = I -1; j >= 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) <= dx ) ? x : x1+dx; y1 = (Math.abs(y-y1) <= dy ) ? y : y1+dy; actor.moveTo(x1, y1); if ((x1== x) && (y1==y)) { actor.resetDrag(x, y); theGame.stage.repaint(); clock.stop(); clock = null; } 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
Mover creates a clock and adds itself as a clock watcher. The clock is set up to animate the movement at an appropriate speed. The animation moves the actor one increment at a time back to its original location, as shown in Figure 7.3. Once the animation has returned the actor back to its original location, the clock is stopped.
Figure 7.3 Moving the actor back into place. A Mover object is created in the drag-and-drop manager when the drag is stopped and no drop site will accept the actor, as shown in Listing 7.6. Listing 7.6 Creating a mover object. 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; } } new Mover(theGame, draggedActor, originalx, originaly, x-dx, y-dy); } return false; } This concept of animating an actor under the control of a clock is touching on the concept of sprites, which we’ll cover in the next chapter.
SUMMARY Speaking of clocks and time, this chapter sure was short. Of course, time is of the essence, but it doesn’t take long to explore the concept of clock implementation. Java makes it easy.
We’ve learned how clocks can increase the excitement of a game by displaying the time or counting down until sure death. We’ve also seen how using clocks can change the speed of the game, make it more difficult, or pause the play. I also made good on my promise (as all good gamers do) to show you how to write a fancy return animator for drag-and-drops.
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 8 WORKING WITH SPRITES NEIL BARTLETT
S
prite? We’ve mentioned sprites before, but just in passing. Do you remember what it is? Give up? It’s an irregularly shaped
object that moves non-destructively over a background. Quite a mouthful, isn’t it? Let’s put it another way. A sprite is your typical space alien: It has an irregular shape, and it wanders around the screen without taking big chunks out of the background. Don’t sprites sound remarkably like the actor objects from Chapters 2 through 7? That’s because sprites are simply actors by another name. I’m using the fancy term, sprites, to clue in those of you who’ve done some game programming before. Sprites are very common in game programming. Yes, our actors are sprites, but there are a few more pieces of the coding jigsaw to add before actors can assume the full mantle of sprites. The following is a list of things that a fully fledged sprite actor must do: • Draw Itself—The actor must be able to draw itself. We covered drawing actors in Chapter 4, when we discussed painting, image handlers, and transparency. We said that actors use their paint method to draw themselves at their current position. They use image handlers to help paint images, and they use transparency to stop the images from looking like rectangular blobs. • Maintain Z-order—When two actors pass over each other, one must pass on top. But which one? Actors use Z-order to determine who comes out on top. Z-order is a number that indicates how close an actor is to the viewer—the higher the number, the closer the actor. A Z-order of zero is furthest away. When two actors pass over each other, the actor with the higher Z-order is drawn on top. We saw how to implement Z-order in Chapter 6. • Move Autonomously—Also in Chapter 6, we saw how an actor can be dragged around the stage by the player using the mouse. We need to improve on this. To qualify as a sprite, an actor must be able to move without any user interaction. The actors must know in which direction they are traveling—and at what speed. • Stay within the Boundaries of the Game—An actor must be able to limit where it roams. It has a boundary of movement. When an actor gets to the boundary, it might do one of several things. For instance, it might bounce off the boundary, or it might wrap around to another part of the stage (like in Asteroids), or it might explode. • Collide with Other Sprites—Most of the fun in an action game is blowing up the bad guys. This happens when two sprites—a missile and the a bad guy—collide with each other. Of course, not all actor collisions are destructive. Often, friendly-fire (when the player’s missiles hit the player’s spaceship) will not destroy the player’s spaceship. A good sprite actor will know what to do when it collides with other actors. If actors implement all of these features, then they’re fit to be called sprites. We already covered some of the features when we looked at other parts of the framework. If we cross out what we have already done, we only need to implement independent movement, boundary detection, and collision detection to have complete sprite actors. Oh, and there’s one other thing: When we have a lot of actors roaming the screen, performance becomes a drag. So, we will investigate further performance optimizations.
MOVEMENT Smooth actor movement is really an illusion. When it moves, an actor jumps from pixel to pixel on the screen. The illusion is to redraw the actor frequently enough to make it look, to the human eye, like it is moving smoothly. To create this illusion, we need to poke the actor regularly and tell it to reposition and redraw itself. If we do this faster than 12 redraws per second, then the illusion will fool the human eye into seeing smooth movement. For extra smoothness, we can use even faster redraw rates. Television
refreshes at a rate of 30 times per second, and a typical movie flashes 24 frames per second. We will use 25 fames per second because it is easy to calculate: It is once every 40 milliseconds (1/25 seconds). In Chapter 7, we saw how to regularly poke an actor—by using a clock. GameWorks provides a master clock that pokes all the actors 25 times a second. On each tick, the master clock calls the tick method of the game object. This calls the tick method for the ActorManager which, in turn, calls the tick method for each actor in its list. Inside the actor’s tick method, the actor moves to its next position. The master clock is running at 25 ticks per second—so we get smooth actor movement. A CALCULATED MOVE The actor now knows when to move to the next position, but how does it know where to move? Simple, it applies some physics. Stop that yawning; this is going to be very elementary. Each actor has a position and a velocity. The position is a point on the screen, and the velocity is the number of pixels to move in the next second. Therefore, if the actor counts the number of seconds between clock ticks, the actor knows how far to move. The formula is simple: Velocity × Time. Providing we measure time in seconds, the actor knows exactly how many pixels to move. To keep things simple, the actor maintains two separate velocities: one in the x direction and one in the y direction. It does two velocity calculations, one for each direction.
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 MOVEMENT This calculation of distance based on velocity and time was first discovered by an English guy named Sir Issac Newton. Newton was, as I’m sure you know, the antisocial Cambridge Math professor who, in the 17th century, discovered the math to predict movement. He paved the way for realistic sprite movement. In honor of Newton, I have named the class that calculates the actor’s next position NewtonMovement. This class is shown in Listing 8.1. The heart of the class is the update method. It calculates the next position of an actor, based on the time elapsed between clock ticks and the current velocities in the x and the y directions (vx and vy, respectively). When the actor is created, it creates a NewtonMovement object to help it calculate its movement. On each clock tick, the actor calls the update method from its tick method. Listing 8.1 How Newton calculates the next position. class NewtonMovement implements MovementHandler { Game theGame; NewtonMovement(Game aGame) { theGame = aGame; } public void update(Actor actor) { double dt = theGame.clock.timeSinceLastTick()/1000.0; actor.oldx = actor.x; actor.oldy = actor.y; actor.x += (actor.vx * dt); actor.y += (actor.vy * dt); } } Why have I wrapped up the calculation in a separate class? Why didn’t I implement it directly in the actor’s tick method? To answer that, ask yourself one further question: Was Newton correct? With historical hindsight, we know that Einstein came on the scene 200 years later and pronounced a new theory of movement: General Relativity. Of course, we don’t have to wait around for the next Einstein to dream up other movement schemes. Newton’s calculation is good for moving in a straight line, but, suppose we don’t want our sprite to move in a straight line. Maybe it is a beetle zigzagging around the stage, or an atom moving under random Brownian motion. The point is that we might need to implement different movement schemes. The separate movement class allows an actor to move differently just by constructing a class with a different movement calculation. Yet, at the same time, it allows a range of actors to share common movement schemes, such as Newton’s movement. The trick to using the replaceable class is to implement an interface. All movement classes implement the MovementHandler interface: interface MovementHandler { void update(); } The actor chooses the movement handler it wants to use by calling the factory method createMovementHandler. (We covered factory methods in Chapter 3.) By default, the actor uses the NewtonMovement class. Listing 8.2 shows the movement-related code from the Actor class. Notice that the call to the update method is preceded by a test to check that there is a movement handler. This check allows stationary actors the freedom to not create a movement handler. They do this by using an empty factory method, as
shown here: protected void createMovementHandler() { } The movement variable will remain at the value null, and so the movement code will not be called. Listing 8.2 An actor must know how to move well. class Actor { protected protected protected protected protected protected protected protected
Game theGame; MovementHandler movement = null; double x; double y; double oldx; double oldy; double vx; double vy;
Actor(Game aGame) { theGame = aGame; createMovementHandler(); } protected void createMovementHandler() { movement = new NewtonMovement(theGame); } public void tick() { setVelocity(); if (movement != null) movement.update(this); } protected void setVelocity() { } } In the real world, velocity is not constant: Things accelerate and decelerate. You can simulate this by changing the values vx and vy in the actor’s tick method. The setVelocity method is provided as a suitable place to code these changes.
Java Perks: Using double X And Y Coordinates In the Actor class, the x and y coordinates of the actor are declared as type double. Now, this might surprise you. After all, a screen coordinate is at most, say, 1600, which can very easily be stored in an int (which has a maximum value of 2147483647). So why the overkill of using a double? The answer lies in calculating the next position. To calculate the next position, we use an equation, such as one of Newton’s Laws. The equation doesn’t necessarily calculate the position as a whole number; it might produce a fraction. For example, think of an actor that is moving very slowly at, say, one pixel every five seconds. The actor is, on average, moving 0.2 pixels per second. If we store this position in an int, we’d lose the fraction part. For instance, if the actor is at position 10 and we calculated that it should move to position 10.2, the conversion to the int would round down to position 10. Next time we do a calculation, we are still at position 10, and the actor hasn’t moved. We use the double type to correct this. The double type can record the fraction. We calculate the new position, 10.2, and store it in the double. We still need to convert to an int because pixel positions are whole numbers, so we convert the double to an int to find the current screen pixel, 10. But, and it is a big but, the double still records the fraction. Next calculation is started from position 10.2. Eventually, the fractions will accumulate, and the double to int conversion will round to the next pixel, moving the actor slowly from pixel to pixel. Effectively, we are using the double to record the intermediate results of the calculation.
Phew, I need a drink after that explanation: Make mine a double.
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
BOUNDARY CONDITIONS Ask any real-life actor what it’s like to fall off the stage, and he will surely reply: It hurts. Well, our actors may not break bones, but we want them to stay on the stage. Actors should be limited where they can go on the stage. We don’t want them wandering off, and we don’t want to have to write game code to keep tabs on actors. If we tell our actors where they can roam on the stage, they will stay within the boundaries. Of course, once an actor reaches its boundary, it needs to know what to do, so we will tell the actor that, as well. To implement the boundary code, we will once again employ the replaceable class technique that we used for movement. We want flexibility to create different sizes and shapes of the boundary and to perform different actions when the actor reaches the boundary. Listing 8.3 shows one such boundary class, WrapRectBoundary. This class ensures that the actor remains within a rectangular boundary. If the actor tries to leave, the boundary class wraps the position of the actor so that it re-enters the stage on the opposite side. Listing 8.3 Let’s all wrap. class WrapRectBoundary implements BoundaryHandler { Game theGame; Rectangle r; WrapRectBoundary(Game aGame, int ax, int ay, int aw, int ah) { theGame = aGame; r = new Rectangle(ax, ay, aw, ah); } public boolean check(Actor actor) { double x = actor.x; double y = actor.y; if (actor.x < r.x) actor.x = r.x+r.width; if (actor.y < r.y) actor.y = r.y+r.height; if (actor.x > 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])<3)&&((x[0]-x[i])<3)) { if (y[i]>y[0]) motion[i]=3; else motion[i]=4; } else if (((y[i]-y[0])<3)&&((y[0]-y[i])<3)) { if (x[i]>x[0]) motion[i]=1; else motion[i]=2; } if (playArea[j+sideIX[motion[i]]]!=0) motion[i]=0; } break; } To put this code in context, I should tell you that Iceblox is written in classic sequential programming style. Rather than define a separate class for each kind of actor, Karl keeps everything in one huge class. He controls the action through a continuous loop that polls the actors (whose “class” is stored in an int array called creature), deciding on each iteration what each should look like and how it should move. It controls each actor’s look by selecting frames out of a multiframe Image object, much as we did in the dice game discussed in Chapter 11. Each time through the loop, Iceblox assigns each flame actor a random value from the set of frame displacements that point to flame images (represented by animF). Controlling motion takes slightly more work. The rest of Listing 13.1 is concerned with deciding how the flame should move. The actors’ motion is controlled in an array of ints called motion. Each entry in the array corresponds to the actor’s own entry in creature. The motion variable can take the values 0 through 4 (representing stationary, left, right, up, and down, respectively). The first decision in the snippet is to test whether the flame is currently moving. If not, Iceblox assigns it an arbitrary direction, using the Math.random method. The next test is whether the flame is sitting on a grid of 30 by 30 pixels. If so, the flame switches into pursuit mode by comparing its own position to the penguin’s. If the flame finds itself within three pixels of its quarry, it resets its direction to “follow that penguin,” and the chase is on. Finally, it tests whether the direction it has chosen for itself will take it outside the playing area or into an ice cube, in which case it resets motion to 0. The value assigned to motion has its effect at the very beginning of the event loop, when each actor is moved to a new position. Listing 13.2 shows how the actor is moved horizontally or vertically by a displacement whose value is contained in either dx or dy. Listing 13.2 Motion code and level variables. switch(motion[i])
{ case 1: x[i]-=dx[i]; break; case 2: x[i]+=dx[i]; break; case 3: y[i]-=dy[i]; break; case 4: y[i]+=dy[i]; break; default: break; } The values of dx and dy are set according to the level of play. In fact, the displacement value (which is another way of saying the speed) of the pursuing flames is one of just four elements that combine to raise the difficulty of each successive level. The following snippet shows the four variables that control the level of play: final int levFlame[]={2,3,4,2,3,4},levRock[]={5,6,7,8,9,10}; final int levSpeed[]={3,3,3,5,5,5},levIce[]={35,33,31,29,27,25};
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
Each of these variables is an array with six elements. They determine the number of flames, number of rocks, speed of flame pursuit, and number of ice cubes for the first six levels of play (every level after six is a repeat of level six). Rocks, which limit the penguin’s freedom of movement, increase in number. On the other hand, ice cubes, which limit only the flame’s freedom of movement (and which the penguin can actually use to douse the flames and win points), decrease. I chose Iceblox as a starting point—and spent so much time analyzing it—to make two points. First, I want ot make the general observation that extremely simple techniques can produce effective results. Despite its almost trivial algorithms, Iceblox is a satisfying game to play. The reason for this has to do with my second, more specific observation. When the heart of your pursuit algorithm is essentially a direct lunge for the quarry, you must do something to mediate its lethal effect. No one would want to play Iceblox if Pixel Pete always got toasted right out of the starting block. The human player must be given a fighting chance. In Iceblox, this is accomplished in two ways. The first is by having the flames wander aimlessly until Pete passes close to them. This allows the player to wander around for a while, pick up a coin or two, and maybe even roll an ice cube over one flame, completely undisturbed. Eventually Pete will have to face life and expose himself to danger, or risk spending his days at level one, a sort of penguin purgatory. The second effect that mediates the simple pursuit algorithm is the presence of rocks, which penguins can hide behind, and especially ice cubes, which stop flames but not penguins, and which penguins can actually use to defend themselves. MAKING PURSUERS MORE INTERESTING Iceblox is a good example of a maze game, and within the world of the maze, an actor can get away with moving either horizontally or vertically on each iteration of an event loop. But most action games demand more realistic (or at least less predictable) motion than that. You’ve already seen how Neil added a random element to the movement of his Asteroids game in Chapter 11. Other techniques you can use include giving your pursuers a sense of self-preservation, so that they try to balance the advantage of giving chase against the danger of exposing themselves to the human player’s weapons. Or, you could introduce some non-linearity into your pursuers’ motion. This would simultaneously make their movement appear more realistic and give the human player more time to respond.
REAL ARTIFICIAL INTELLIGENCE Good pursuit algorithms give games a sense of liveliness and unpredictability, but not real intelligence. But when a game is called upon to make decisions, it had better have a reservoir of real (artificial, that is) intelligence to call on. The classic example of decision-making occurs in games in which players alternate making moves. Like humans, automated players need a mechanism for analyzing and comparing the moves available to them. This mechanism must enable them to answer questions such as: • • • •
Will any move available to me enable me to win on this turn? Will any move available to me enable my opponent to win on the following turn? Of the moves available to me, which will leave me in the strongest position? Which moves available to me will strengthen my opponent even more than they strengthen me?
When we play board games, we humans run through a set of questions such as these on each turn. For simple games, such as tic-tactoe, we may not even be aware of the process of testing and eliminating possibilities. But as the game gets more sophisticated, the process becomes more conscious. By the time I get to games such as Go and chess, I become acutely aware of my own strategic limitations. This frustration has two aspects. First, my addled brain can hold only a finite set of “If I do this, then she’ll probably do this” computations. Thus, I don’t always consider all the moves available to me, or I consider them, but not to sufficient depth.
Second, when none of the moves that I have managed to analyze possesses an obvious advantage, I don’t know how to evaluate and compare each one’s more subtle strengths and weaknesses. Experience certainly helps me to eliminate obviously useless moves from consideration, and it strengthens my intuition about the relative value of different configurations. I suspect, though, that really skilled players have both the ability to analyze more moves to a greater depth than I do and more formalized methods for evaluating configurations. If, like me, you can’t do it for yourself, you can at least teach your computer how to do it. The field of Artificial Intelligence has given us a relatively simple technique for analyzing and comparing your options. The technique is called Min-Max (I’ve also seen it called MiniMax), and you can use it for any game that alternates turns between human and automated players. The first step in implementing Min-Max is to develop a heuristic, or rule of thumb, for assessing the value of any given game configuration. Remember, you have to explain this to your computer, to whom intuition means nothing. Your computer likes numbers, and in Min-Max, that’s what you have to give it. For example, in tic-tac-toe, you could add one point to a board’s value for every row (horizontal, vertical, or diagonal) that contains only your side’s shape, while subtracting one point for every row that contains only your opponent’s shape. In addition, add 100 points if there is a row containing three of your shape, and subtract 100 for three-in-a-row of your opponent’s shape. In Connect4, you could add 1 for every single token of your color that has the potential of growing to four-in-a-row, 10 for every pair of tokens with that potential, 100 for a threesome, and 1000 for four-in-a-row. Subtract these values for your opponent’s color. Figure 13.2 shows some tic-tac-toe and Connect4 configurations with their scores.
Figure 13.2 Tic-tac-toe and Connect4 configurations with their scores.
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
Min-Max involves creating a tree of all the possible moves you can make and then all moves that your opponent could make in response. This process is repeated to an arbitrary depth. The choice of depth is an important decision. On the one hand, the farther forward you look, the more likely you are to avoid a decision that will eventually lead to your opponent’s victory. On the other hand, the number of configurations to be examined goes up exponentially with each additional level of depth, and your human opponent may just decide not to wait around. So, a balance must be struck. Besides, give the poor human a chance. Once you have your configuration tree, go to the bottom level, and evaluate each set of “sibling” configuration leaves, meaning each set on the same branch. This level represents your turn, so evaluate each sibling, and choose the one with the highest value. After all, you’re hear to win, and the high score represents the configuration most advantageous to you. Assign that score to the branch node one level up. Now comes the interesting part. The node one level up is also a sibling. This level represents moves that your opponent could make, and each set of siblings represents your rival’s possible responses to one of your moves. Within each set of siblings, you are interested in the one with the lowest value. Why? Because to make the optimal decision, we must assume that your opponent will always make the best possible move. And because we add points for elements of the configuration that are to your advantage, while subtracting points for those that are to your opponent’s advantage, the leaf with the lowest score represents the best move your opponent can make. So take that minimum value, and pass it up to the next level, where the node with the highest value represents the move on your part that will leave your opponent in the weakest position, even if he makes his best possible move. Clever you. From that level, choose the maximum value and pass it up. Continue this alternation of minimum and maximum until you get to the root level (no bonus points for guessing why this method is called Min-Max). It will then be clear to your program which move it should make. Figure 13.3 shows Min-Max at work in a tic-tac-toe game, using the heuristic I mentioned earlier.
Figure 13.3 Applying Min-Max to tic-tac-toe. The key to a winning Min-Max implementation is in the choice of heuristic. Heuristics for sophisticated games such as chess are beyond the scope of this discussion, but for simple games, simple heuristics play surprisingly well. For example, the scoring system I mentioned for Connect4 will beat most humans, even if it looks ahead only one round. MINMAX IN THE GAME FRAMEWORK The game framework includes a generic implementation of the Min-Max algorithm. It includes a public method called getBestMove that accepts an argument describing the current state of a game and returns an object describing the highest rated available move. Listing 13.3 shows the code for the MinMax class constructor and the getBestMove method. Listing 13.3 The MinMax class constructor and getBestMove method. public class MinMax { GameMoveManager manager; int defaultMin; int defaultMax; private static final int COMPUTER_PLAYER = 1;
private static final int HUMAN_PLAYER = -1; public MinMax(GameMoveManager aManager, int aDefaultMin, int aDefaultMax) { manager = aManager; defaultMin = aDefaultMin; defaultMax = aDefaultMax; } public GameMove getBestMove(GameState state, int depth) { GameMove quickie = manager.quickMove(state); if (quickie != null) return quickie; return findMax(state, depth * 2); } The class’s instance variables explain a great deal about how it works. Remember that in order to be generic, MinMax can’t contain any attributes or behavior specific to a particular game. Therefore, any game wishing to make use of it must have a separate class to implement the game-specific behavior. To accommodate this need, the game framework includes a class called GameMoveManager, which offers a number of services commonly required by games with automated players who take turns making moves. We’ll take a closer look at GameMoveManager when we’re done examining MinMax, but because MinMax calls a few of the GameMoveManager’s methods, I have to tell you about it now. The MinMax constructor accepts a GameMoveManager argument so that it can call back to its methods. It also accepts two int arguments for the default minimum and maximum values that will be used as starting points for the maximum and minimum comparisons, respectively. The getBestMove method accepts two arguments. The first is a GameState object. The GameState class is defined in the game framework and is used to describe the configuration of a game at a moment in time. Each game is responsible for extending GameState to add the game-specific attributes it needs. The second argument indicates the requested depth of the Min-Max search. We’ll pass over the quickMove call for now and discuss it when we talk about GameMoveManager. The getBestMove method passes its arguments on to a method called findMax, which actually starts the Min-Max cycle. Note that it doubles the value of depth before passing it on. This is because depth has a different meaning to the program calling MinMax than it does within MinMax. The program calling MinMax regards each level as one round of the game, with every player getting a turn. But internally, when it’s building its tree, MinMax considers each level of the tree. I could have kept the meaning of depth consistent, adding a separate variable to indicate whether I was on the level’s Min turn or Max turn, but the code came out much simpler by just doubling depth. The findMax method itself (together with its partner, findMin) implements the Min-Max algorithm in a very simple, non-optimized way. Listing 13.4 shows how it works.
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 13.4 The findMax method. private GameMove findMax(GameState state, int depth) { GameMove bestSoFar = null; int bestResultSoFar = defaultMin; Vector possibleMoves = manager.getPossibleMoves(state); int moveCount = possibleMoves.size(); for (int i = 0; i < moveCount; i++) { GameMove testMove = (GameMove) possibleMoves.elementAt(i); GameState testState = manager.applyMove(state, testMove, COMPUTER_PLAYER); if (depth == 0 || moveCount == 1) testMove.value = manager.evaluate(testState); else { GameMove searchMove = findMin(testState, depth - 1); testMove.value = searchMove.value; } if (testMove.value > bestResultSoFar) { bestResultSoFar = testMove.value; bestSoFar = testMove; } } return bestSoFar; } The findMax method uses two variables to track the progress of the Min-Max search. GameMove is an object from the game framework that contains the data needed to describe a move, plus an int indicating the value assigned to the move by the heuristic evaluation function. The bestSoFar object contains the turn with the highest score encountered by any given point in the search process. The bestResultSoFar variable contains the value of that move. It is initialized to defaultMin to serve as a starting point for comparison with the first move encountered. To start the search, findMax calls the GameMoveManager’s getPossibleMoves method, which accepts the GameState as an argument and returns a vector whose elements are all the possible moves the player could make. The search consists of selecting each move and applying it to the current game state by calling the GameMoveManager’s applyMove method. The next step is to assign a value to the move. If we are at the bottom level of the search tree (that is, depth == 0), then we do this by passing the testMove object to the GameMoveManager’s evaluate method. This method is where the heuristic evaluation logic lives. If, on the other hand, we are above the bottom level, we decrement depth and pass it—along with testState—to findMin. This method will build the search tree for the next level and pass each of the opponent’s possible moves back to findMax until we reach the bottom level. Eventually, findMin passes searchMove—the best-rated move on that branch of the search tree—up to findMax at the top level. If the value assigned to testMove is the maximum so far, testMove becomes the new best so far.
The complementary method, findMin, looks just like findMax, except that because it only handles odd-numbered levels, it never calls evaluate. That’s all there is to MinMax. As I mentioned earlier, it is generic and entirely unoptimized. An obvious enhancement would be to prune branches of the tree whose values make them irrelevant. For example, suppose I’ve already passed a value of -50 from the bottom level up to one branch of the next level. If the first leaf of the next branch has a value of -25, I needn’t bother to continue testing that branch. Why? Because at this bottom level, I’m looking for the maximum value, which will be at least -25. But at the next level I’m looking for the minimum, which will be at most -50. So the remainder of the leaves on the second branch are irrelevant, regardless of their value. THE GAMEMOVEMANAGER GameMoveManager is an abstract class defining the common set of methods needed for computerized players to determine their next move. Listing 13.5 shows the code for GameMoveManager. Listing 13.5 The GameMoveManager class. public abstract class GameMoveManager { MinMax myMinMax = null; public GameMoveManager(int defaultMin, int defaultMax) { myMinMax = new MinMax(this, defaultMin, defaultMax); } public GameMove getBestMove(GameState aState, int depth) { return myMinMax.getBestMove(aState, depth); } abstract Vector getPossibleMoves(GameState aState); abstract GameState applyMove(GameState aState, GameMove aMove, int player); abstract int evaluate(GameState aState); public GameMove quickMove(GameState aState) { return null; } } GameMoveManager is an abstract class. That means that it does not supply default behavior for all of the methods it defines. For some of them, it merely declares their signatures. This means that any game wishing to use this class must extend it into a subclass and implement at least the abstract methods. GameMoveManager’s constructor creates a MinMax object, passing it values for defaultMin and defaultMax. It uses the MinMax object in its implementation of getBestMove. Subclasses of the GameMoveManager that want to use a different selection algorithm can override getBestMove. GameMoveManager defines three abstract methods: getPossibleMoves, applyMove, and evaluate. They are all declared abstract because they are completely dependent on game-specific knowledge. There is no sensible default behavior for any of them. In a few more pages, we’ll see a sample implementation of each of them. The last method in GameMoveManager is called quickMove. This is the place to implement shortcuts that could cut down your search time significantly. For example, if you’re writing a Java Connect4, you may want to do an initial check of whether the computer can win on the next turn before you construct the search tree. The default implementation of quickMove is to return null.
This way a game that isn’t interested in using it doesn’t have to do anything.
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
TICTACTOEGAMEMOVEMANAGER Let’s look at a simple implementation of a GameMoveManager. For demonstration purposes, we’ll stick with our TicTacToe example. The first step is to decide how to represent board configurations and moves. The obvious choices for TicTacToe will do just fine, as Listing 13.6 demonstrates. Listing 13.6 The TicTacToeGameState and TicTacToeGameMove classes. public class TicTacToeGameState extends GameState { int square[][]; public TicTacToeGameState() { square = new int[3][3]; for (int x = 0; x < 3; x++) for (int y = 0; y < 3; y++) square[x][y] = 0; value = 0; } } public class TicTacToeGameMove extends GameMove { int x, y; public TicTacToeGameMove(int aX, int aY) { x = aX; y = aY; value = 0; } } The TicTacToeGameState can be represented with a three-by-three array. All members of the array are initialized to 0. As moves are made on the board, the corresponding squares will be set to 1 for the computer’s side and –1 for the human’s side. A TicTacToeGameMove can be represented even more simply, as a pair of ints. The TicTacToeGameMoveManager is more interesting. It must implement at least the abstract methods from the GameMoveManager class. Listing 13.7 shows how it acquits its responsibilities. Listing 13.7 The TicTacToeGameMoveManager class. public class TicTacToeGameMoveManager extends GameMoveManager { private static final int DEFAULT_MIN = -1000; private static final int DEFAULT_MAX = 1000; public TicTacToeGameMoveManager() {
super(DEFAULT_MIN, DEFAULT_MAX); } public GameState applyMove(GameState state, GameMove move, int player) { TicTacToeGameState TTTstate = (TicTacToeGameState) state; TicTacToeGameMove TTTmove = (TicTacToeGameMove) move; TicTacToeGameState newTTTState = new TicTacToeGameState(); for (int x = 0; x < 3; x++) for (int y = 0; y < 3; y++) newTTTState.square[x][y] = TTTstate.square[x][y]; newTTTState.square[TTTmove.x][TTTmove.y] = player; newTTTState.value = TTTmove.value; return newTTTState; } public int evaluate(GameState state) { TicTacToeGameState TTTstate = (TicTacToeGameState) state; int value = 0; value += evaluateRow(TTTstate.square[0][0], TTTstate.square[0][2]); value += evaluateRow(TTTstate.square[1][0], TTTstate.square[1][2]); value += evaluateRow(TTTstate.square[2][0], TTTstate.square[2][2]); value += evaluateRow(TTTstate.square[0][0], TTTstate.square[2][0]); value += evaluateRow(TTTstate.square[1][0], TTTstate.square[1][2]); value += evaluateRow(TTTstate.square[2][0], TTTstate.square[2][2]); value += evaluateRow(TTTstate.square[0][0], TTTstate.square[2][2]); value += evaluateRow(TTTstate.square[0][2], TTTstate.square[2][0]); return value;
TTTstate.square[0][1], TTTstate.square[1][1], TTTstate.square[2][1], TTTstate.square[1][0], TTTstate.square[1][1], TTTstate.square[2][1], TTTstate.square[1][1], TTTstate.square[1][1],
} private int evaluateRow(int first, int second, int third) { boolean openSpace, oneValue = false; int value = 0; int row[] = {first, second, third}; /* test for three of a kind */ if (row[0] != 0 && row[0] == row[1] && row[0] == row[2]) return 100 * row[0]; for (int i = 0; i < 3; i++) if (row[i] == 0) openSpace = true; else if (oneValue && row[i] != value) return 0; else { oneValue = true; value = row[i]; } return value; }
public Vector getPossibleMoves(GameState state) { TicTacToeGameState TTTstate = (TicTacToeGameState) state; Vector possibleMoves = new Vector(9); for (int x = 0; x < 3; x++) for (int y = 0; y < 3; y++) if (TTTstate.square[x][y] == 0) possibleMoves.addElement(new TicTacToeGameMove(x, y)); return possibleMoves; } } As a general comment, notice that the first thing most of these methods do is to cast their generic GameState and GameMove arguments to the more specific TicTacToeGameState and TicTacToeGameMove types. This is necessary in order to enable them to conform to the inherited signatures from GameMoveManager, while at the same time receiving game-specific arguments. With the general comment out of the way, we can take a look at how TicTacToeGameMoveManager implements each of GameMoveManager’s abstract classes. The applyMove method does its job by creating a new TicTacToeGameState. It then copies the values of the row array from the original object to the new object. Finally, it sets the square in the new object that corresponds to the x and y values in TTTmove, copies TTTmove.value, and returns the new object. The evaluate method uses a helper method, evaluateRow. This method accepts three int arguments and tests for a couple of conditions. First, if the first argument has a non-zero value, and all three arguments are equal, evaluateRow returns the value of the first argument times 100. This ensures that the overall value of any board configuration that includes three in a row, will far exceed (in one direction or the other) the value of a configuration that doesn’t. The second condition is when there is at least one open space in the row and all the filled spaces have the same value. In this case, evaluateRow doesn’t distinguish between one and two filled spaces. The heuristic seems to work well enough as is.
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
As for evaluate itself, it simply feeds each row on the board to evaluateRow, tallying the results. The effect is that the value of the rows to the human player’s advantage is subtracted from the value of the row’s to the computer’s advantage. The getPossibleMoves method creates a vector called possibleMoves. It then examines each square on the board. Every time it encounters an open square, it creates a new TicTacToeGameMove for that square and adds the new object to possibleMoves.
SUMMARY We’ve learned a few techniques for giving your games some intelligent behavior. For action games, we’ve looked at a simple pursuit algorithm and shown how—in combination with some limitations on the pursuers’ freedom of movement—even this simple algorithm produces a very satisfying game. We also gave some suggestions for raising the unpredictability level of pursuers’ movement, thus giving it a more life-like appearance. We also stuck a toe (a TicTacToe, that is) into the waters of Artificial Intelligence proper. We saw that the game framework includes an implementation of the Min-Max algorithm, which you can combine with a game-specific GameMoveManager to help your automated players decide what to do next.
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 14 AUTOMATED PLAYERS AND WEAPONS CHRIS STRANC
I
n this chapter, we will explore an implementation of automated players and weapons. We’ll use the MazeWars applet as an
environment for developing little software life forms and tools to eliminate them. Along the way, we’ll investigate a gaming architecture that allows us to ensure all the players within the game are living by the same set of game rules. So settle down, and read on. It’s time to enter a world of sharks, hunters, and neutron bombs. By the end of this chapter, you will see these entities in a different light.
MAZEWARS MazeWars is a maze hunting applet/application. When you start MazeWars you are dropped into a maze and left to fend for yourself. Your goal is to wander around the maze and bomb, shoot, or knife everything in existence. Around you are creatures with a similar mentality. Sharks are placed in the maze looking for a quick snack—they wander around and eat anything they see. Hunters love the thrill of the chase, the rush of adrenaline, and most of all, they love throwing bombs and shooting. There are a host of other automated players waiting for you. To see them in action, you can select your favorite class from the list in the top-right corner of the applet and click on the Create button. The maze is not a very friendly place, but who ever said games should be friendly? When you are in the maze, you can pit your human skills against the algorithms within your digital opponents. That is the essence of automated players, and the implementation of these players is our focus in the first part of this chapter. Figure 14.1 depicts life within the maze. As usual, I am wreaking havoc with my trusty flame thrower.
Figure 14.1 Life within MazeWars. THE MAZE WORLD The playing area of the MazeWars world is maintained as an array of cells. Each cell is a particular type. The valid types include vertical and horizontal walls, four types of corners, and open cells. Players are restricted to moving within the open cells. When the game starts, the player selects the size of the maze, then the application populates its contents with random walls to generate a new maze for each game. The array of cells is maintained within the MazeWarsGameState object.
The actors within the maze are all derived from the MazeActor class. The AutoActor class extends MazeActor. These two classes provide the infrastructure for implementing automated players. This includes support for animating the actors and allowing them to interact within the MazeWars world. The only actor within the maze that is not derived from AutoActor is the ConsoleActor object. This actor is the game player’s surrogate within the harsh reality of maze life. SO MANY ACTORS, JUST ONE APPLICATION It is said that variety is the spice of life. This holds true in the gaming world. Players would soon get bored with a game if they were always faced with the same opponent throughout the entire game. Today’s games need multiple levels of play. Each new level needs to supply more dastardly and cunning opponents. MazeWars is no exception. It must host an endless variety of automated players. To integrate this diverse set of actors within the application, I isolated two interfaces. The first interface allows the game logic to query and manipulate the actors in a generic manner. We’ll call this the GameToActor interface. The second interface allows an actor to interact with its environment. It provides methods for looking around the maze, locating opponents, moving, and firing weapons. We’ll call this set of methods the ActorToGame interface. The location of these interfaces and the flow of information within the MazeWars application is demonstrated in Figure 14.2.
Figure 14.2 The GameToActor and ActorToGame interfaces. To implement these interfaces within MazeWars, I considered three different approaches: • Defining the interfaces using the Java interface mechanism • Creating a bridge class that encapsulates the interfaces • Defining the interfaces as protected members within the MazeActor class; these members would operate on private data members within each MazeActor object and make requests of the game state as appropriate I chose to use a bridge class to implement the ActorToGame interface and protected methods within MazeActor to implement the GameToActor interface. The choice of two different interfacing mechanisms reflects the different requirements of the two interfaces. The GameToActor interface is used by the game logic. The game logic performs a sequence of well-defined operations to change the state of the game. The GameToActor interface is a direct reflection of the decision points within this sequence of operations. Coding the interface into the base class is an indication of the stability and consistency of the logic within the game. In effect, it guarantees that all actors will be treated identically by the game logic. The ActorToGame interface must support a far greater load. It may be called upon to perform any of the following roles: • • • •
Provides the actor with a single interface to the game state Validates of the actor actions Implement a health and resource tracking model Custom manage individual actors
Knowing the large scope of the ActorToGame interface, it was important to separate it from the implementation of the MazeActor. After all, in this world of double standards, it is foreseeable that we might need to supply different interface behaviors for different actors within the maze. The future use of the interface to support actor action validation also suggested that the interface might need to store some actor state information. For these reasons, I chose a bridge class over a dataless interface.
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
Tables 14.1 and 14.2 describe some of the methods within the GameToActor and ActorToGame interfaces. Table 14.1The GameToActor interface
Method
Description
void onJoinGame();
Informs the actor that it has entered the game
void onLeaveGame();
Informs the actor that it has left the game
void onKilled();
Informs the actor that it has been killed
boolean isKillable();
Determines if the actor can be killed; bullets and bombs are examples of actors that cannot be killed
boolean doesKillActor(MazeActor)
Allows the actor to determine if the supplied actor is a potential target
String getKillString();
Specifies the mode of attack in a textual form
void justKilled(MazeActor)
Informs the aggressor of its victory
Table 14.2The ActorToGame interface.
Method
Description
int getDirection()
Determines the actor’s current direction of travel
void setDirection(int dir)
Alters the actor’s direction of travel
Cell getCell()
Retrieves the location of the actor’s current cell
Cell getNextCell()
Retrieves the location of the actor’s next cell
int getNextCellType()
Retrieves the type of cell the actor moves to next; this might be a wall, corner, or open space
boolean killActors( int range )
Instructs the game to kill any actors within the specified range
boolean moveToNextCell()
Moves to the next location in the maze, based on the current location and direction
boolean fireGun( int direction, int range )
Shoots a gun in the specified direction, with a particular range
boolean throwBomb( int direction, int range )
Throws a bomb in the specified direction, for a specified distance
int getSightingDistance( int dir )
Determines the range to the nearest killable object in the specified direction
MazeActor getSightingActor( int dir )
Returns the nearest actor in the specified direction, or null if no actors are visible in that direction
If we examine these interfaces, we can see they provide groups of functionality. These groups reflect the division of responsibilities
for the actors and the game. The game must manage the list of actors and perform “kill detection.” The actors must move around and, hopefully, kill other actors. For the AutoActor, these two interfaces are the entire game. They represent the contract the AutoActor must fulfill to exist within the maze and the full set of tools the AutoActor has to eliminate its opponents. Because these interfaces define the AutoActors environment, we’ll take some time to become more familiar with them.
The GameToActor Interface: Actor Management The game uses the GameToActor interface to notify actors of important events. As a new actor is added to the game, it is notified with a call to onJoinGame(). When it leaves, it is notified with a call to onLeaveGame(). By invoking these methods, the game allows the actor to perform initialization and cleanup operations. For the AutoActor class, these notifications provide the means to register with a central clock service. This clock will notify the actors periodically, allowing them to execute their game logic and animate themselves. The final notification sent to the MazeActors is onKilled(). While processing this event, the actor is meant to decide its fate. It can choose to die and leave the game or come back to life by calling the appropriate methods within its ActorGameBridge object.
The GameToActor Interface: Killing Actors Killing actors. It’s a way of life in the maze. The process of “killing” is the game’s second area of responsibility. MazeActors can request the game to kill anything within a certain distance by calling the killActors( int range ) method for its ActorGameBridge object. The bridge object will then request the game to perform the killing logic. To determine if an actor will kill anything, the game first tests if the actor is capable of killing. This test involves the getKillString() method. The getKillString() method must return a non-null string to indicate the mode of attack. This string is used to generate the status message, “PlayerX shot PlayerY.” For example, it allows a little green slime monster to declare “SlimeCreature#1 osmosed Chris.” The game will target each of the actors within the game and see if the aggressor is interested in killing them by calling doesKillActor ( targetActor ). If this succeeds, then the range is tested, and the target is killed.
The ActorToGame Interface Let’s turn our attention to the methods within the ActorToGame interface. A subset of these methods is listed in Table 14.2. These methods are dedicated to letting the AutoActor look around, move, and attack. These methods are all the actor has to wage its little war. It looks around the maze with methods like getNextCellType() and getSightingDistance(), moves with moveToNextCell(), and attacks things using killActors() (for the up-close and personal approach) or fireGun() and throwBomb() (for the long-distance attack). We’ll explore these methods in greater depth later in this chapter, as we begin to implement ever more intelligent AutoActors to populate the maze.
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
BRINGING IT ALL TO LIFE We now have a maze game with a variety of actors ready to reach out and annihilate each other. All that’s missing is the spark of life. That spark takes the form of the onTick() method within the AutoActor class. The onTick() method stores the logic for the actors’ actions. This might include calls to look around the maze, move, and shoot weapons. The life of an AutoActor is summarized in Figure 14.3.
Figure 14.3 The life of an AutoActor. Within the onTick() method, the actor can do many things, but the most fundamental is motion. Motion is achieved by a call to the moveToNextCell() method. This will move the actor one cell in the current direction. A simple onTick() method that just called moveToNextCell() would result in an actor that races across the maze, one cell every TICK_TIME (75) milliseconds, until it hits something. Not a very inspiring opponent. If you want to see one in action, start MazeWars, select the Splat class from the Choice box, and click on the Create button. All the AutoActors described within the remainder of the chapter are also available within this Choice box. A Splat stops when it hits something. We can fix that. Listing 14.1 shows the onTick() method for a new class Bouncer. Before bouncing on its way, a Bouncer object will check to see if it has been stopped by hitting a wall. If this has happened, it will choose a new direction prior to moving. Listing 14.1 The Bouncer.onTick() method bounces the actor through the maze. protected void onTick( long tick ) { if ( getDirection() == Maze.MD_STOPPED ) setDirection( getRandom( Maze.MD_MOD ) ); moveToNextCell( ); } So far, both Splat and Bouncer have moved around the maze at the maximum speed of one cell per tick. That’s kind of fast, after all, they don’t even look where they’re going. Not to worry. We can slow things down a bit. Here’s how. The speed of an actor is determined by the number of ticks between successive motions. To create an actor that travels half as fast as a Bouncer, all you need to do is call moveToNextCell() once for every two calls to onTick(). To perform the more general case of setting an arbitrary speed for an actor, you need some members to store the desired speed and the time of the last movement. The need to control actor speed is shared by all automated actors, so its implementation is included in the AutoActor class. Listing 14.2 details the interface for speed control. For an actor to move at a specified speed, all it needs to do is call setSpeed(), then use the moveAtSpeed() method to perform any motion. Listing 14.2 Speed and motion-related methods in AutoActor.
class AutoActor extends MazeActor { long tickCounter; long lastMoveTick; long ticksPerMove; /* Implementing speed for AutoActors ***************************/ protected final void setSpeed( long tpm ) { ticksPerMove = tpm; } protected final boolean isTickToMove() { return lastMoveTick+ticksPerMove<=tickCounter; } protected final void moveNow( ) { moveToNextCell(); lastMoveTick = tickCounter; } protected final boolean moveAtSpeed( ) { if ( isTickToMove() ) { moveNow(); return true; } else { return false; } } } MansBestFriend. It’s a dog, and it’s also the name of our next automated actor. An object of this class will run around the maze looking for its master (the ConsoleActor). When the dog finds his master, it will run towards him and try to stay by his side. To implement this class, we must introduce a method for looking around the maze for other actors. The getSightingActor() method looks in a specified direction for an actor. It will return the nearest actor, or null if no actors are visible. The speed of the dog is varied with calls to setSpeed(). The dog selects its speed based on the distance to its master. Listing 14.3 shows the MansBestFriend.onTick(). The method begins with the dog determining what direction to travel in. If it is not pointing towards the ConsoleActor, then it will look around. If the dog locates its master (sightingDistance>=0), then the dog will select a speed and move in that direction. If the dog does not see anyone, it will wander around until it hits something. Listing 14.3 The MansBestFriend.onTick() method. protected void onTick( long tick ) { Cell myCell = getCell(); // Find the current cell and direction. int direction = getDirection(); // Are we next to someone? MazeActor ma = getSightingActor( direction, false ); int sightingDistance = (ma!=null) && (ma instanceof ConsoleActor) ? myCell.getDist( ma.getCell() ) : -1; if ( sightingDistance < 0 ) { // We must look for our master! for ( direction=Maze.MD_FIRST_DIRECTION ; (direction<Maze.MD_MOD)&&(sightingDistance<0) ; direction++ ) { ma = getSightingActor( direction, false ); if ( (ma!=null) && (ma instanceof ConsoleActor) ) { sightingDistance = myCell.getDist( ma.getCell() ); break; } } } // We saw something, set direction and speed.
if ( sightingDistance>=0 ) { setDirection( direction ); if ( sightingDistance < 2 ) setSpeed( 100 ); // Eventually we will jump on him. else if ( sightingDistance < 5 ) setSpeed( 1 ); // Move quickly while we are close. else if ( sightingDistance < 10 ) setSpeed( 3 ); // Speed up. else setSpeed( 6 ); // Move slowly forward. moveAtSpeed(); } else { // Didn't see anything, keep walking until we see something // or bump into a wall. if ( getDirection() == Maze.MD_STOPPED ) setDirection( getRandom( Maze.MD_MOD ) ); setSpeed( 3 ); moveAtSpeed(); } }
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 have succeeded in creating a faithful companion who will follow us around the maze. That’s really great, but we’re in the maze for some action, not for companionship. I think it’s time to introduce our little green friend—the Hunter class. The Hunter is right at home in the maze—he walks around, throws bombs, shoots things, and, if anything gets too close, he knifes it. I’m glad this guy exists within a game that I can close when I turn my back. The Hunter starts by looking for a target with a simple scan—using getSightingDistance()—in the four directions of the maze. If the Hunter fails to locate a target, it will keep moving with its current direction and speed until it collides with a wall. At this point, the game will stop the Hunter, and then he will slowly start to walk in a random direction. So far, this guy has a broken nose. That’s not good. It just makes him more angry when he finally spots you. When the Hunter spots a target, he changes direction to chase the target. The rest of his logic is based on the distance to the target. If he is within knifing range (3 cells) he will run at full speed towards his opponent, trying to stab him. If he is within bullet range (10 cells), he will alternate between moving forward and shooting. The Hunter wishes to preserve his ammunition so he fires more frequently the closer he gets to his opponent. If his opponent is within bombing distance(20 cells), he will throw a bomb occasionally. Sightings beyond bombing range will inspire the Hunter to run forward at full speed. The Hunter can release a fair number of bombs and bullets in its quest for a trophy. This can pose a health hazard. If the Hunter is not careful, he is quite capable of chasing an opponent and following right behind one of his own bombs. Hence the Hunter’s motto: “You play with fire, you may get bombed.” Listing 14.4 shows the Hunter philosophy on life, expressed as Java code. Listing 14.4 The Hunter.onTick() method. protected void onTick( long tick ) { // Look around for a target. int direction = Maze.MD_STOPPED; sightingDistance = -1; for ( int i=0 ; (i<4) && (sightingDistance<0) ; i++ ) { direction = (getDirection()+i) % Maze.MD_MOD; sightingDistance = getSightingDistance( direction, true ); } if ( sightingDistance<0 ) { // Nothing around, move in old direction. if ( getDirection() == Maze.MD_STOPPED ) { setDirection( getRandom( Maze.MD_MOD ) ); setSpeed( 7 ); // Move slowly. } moveAtSpeed(); } else if ( sightingDistance 2*sightingDistance ) { fireGun( direction, sightingDistance+2 ); lastBullet = tick;
} else { setSpeed( 4 ); moveAtSpeed(); } } else if ( sightingDistance 30 ) { // Bomb time. throwBomb( direction, sightingDistance ); lastBomb = tick; } else { // Not time to bomb yet. setSpeed( 3 ); moveAtSpeed(); } } else { // Run in their direction. setDirection( direction ); setSpeed( 1 ); moveAtSpeed(); } } We have created an automatic player who is quite proficient at removing its opponents from the maze. Let’s now turn our attentions to developments outside the maze. In particular, we’ll explore Java’s dynamic loading capabilities, and their implications for a new breed of gladiators! THE ULTIMATE OPPONENT Nobody said games had to be just for humans. With our definition of the AutoActor and ActorGameBridge, we have the capability to allow MazeWars to act as a battle ground for Java-savvy programmers like you. By deriving a class from AutoActor, you can pit your mind against the guys within the game without worrying about which keys do what or selecting how far to throw bombs. Yes, here is the opportunity to prove that you can think like a better maze hunter, even if your fingers are not up to the chore of fighting it out in realtime. But, before you pull out you favorite Java compiler, remember, things look different from within the maze. AutoActors can only “look” in four directions, they cannot peek over walls. Only us humans have the bird’s eye view of the world. The AutoActor perspective of the maze and its inhabitants is shown in Figure 14.4.
Figure 14.4 AutoActor vision restrictions. By now, you have probably run (at least I hope you have) MazeWars and created the various types of AutoActors. I’m sure you noticed the <ExternalClass> entry, and you probably figured out that it gives you the chance to load an AutoActor extending class. The Create button uses the MazeWarsGameState.createAutoActor() method. In fact, all AutoActors within the maze are created using this method. The createAutoActor() method, shown in Listing 14.5, uses the Class.forName() and Class.newInstance() methods to take a simple string and generate an AutoActor object ready for plugging into the maze. Dynamic loading at its simplest. This capability allows us to create a truly open gaming environment where programmers can compete. Think of the potential for late nights and epic battles. Listing 14.5 MazeWarsGameState.createAutoActor(). public void createAutoActor( String className ) { try { Class moClass = Class.forName( className ); AutoActor aa = (AutoActor) moClass.newInstance(); aa.init( null );
addMazeActor( aa ); } catch ( ClassNotFoundException cnfe ) { MazeWars.statusMessage( className+" Unable to locate class.\n" ); } catch ( IncompatibleClassChangeError icce ) { MazeWars.statusMessage( className+" Not derived from AutoActor class.\n" ); } catch ( Exception e ) { MazeWars.statusMessage( className+" Unable to instance.\n" ); } }
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
YOU MAY BE FAST, BUT NOT THAT FAST When you write automatic actors, it’s easy to get a bit overzealous. Why shoot one bullet when you can keep the entire maze filled with projectiles? Why bother crawling along at the tiresome pace of one cell per tick when you can call moveToNextCell() twice in an invocation of onTurn()? Why bother returning from onTurn() at all until you have systematically covered the entire maze and destroyed everything? I know, you’re an honest programmer trying to make an honest killing with a basically nice AutoActor. Still, it is tempting to shoot at everything you can see prior to leaving the safety of onTurn(). To keep the maze a safe place for actors to fight, we need a set of rules that controls what the actors can do. Fortunately, this was the ulterior motive for implementing the ActorGameBridge class. Though it was initially designed to hide the actors from the details of where the game stores all its information, ActorGameBridge can also act as a validation layer to ensure that no actors get out of hand. The ValidatingAGB class extends ActorGameBridge and tracks the last time the actor moved. When the actor requests another motion, the ValidatingAGB will determine if enough time (one tick) has passed and then either perform the motion or ignore the request and return false. Identical logic is performed to validate weapons fire and calls to killActors(). You can activate the ValidatingAGB class by checking the “Validate actor actions” check box on the MazeWars login dialog. The most obvious effect of this choice is restricting the speed of the ConsoleActor. Since the ConsoleActor is moved by key presses, rather than clock ticks, it is capable of moving much faster than the AutoActor objects. The ValidatingAGBs prevent the ConsoleActor (and all others) from moving faster than one cell per system clock tick. All actors are bound by the same rules, and this prevents the ConsoleActor from running at twice the speed of a flying bullet. Listing 14.6 demonstrates the validation applied to a killActors() call. The first check ensures the range is reasonable. If it passes this test, then the time since the last killActors() call is checked. The test ((lastKillTick+range) < getTickCount()) forces actors to wait longer periods for larger kill ranges. This prevents an actor from wandering around killing everything within three cells at every clock tick. Listing 14.6 Keeping the actors honest with ValidatingAGB.killActors(). protected boolean killActors( int range ) { if ( range > MAX_KILL_RANGE ) return false; // Don't get too greedy. if ( (lastKillTick+range) < getTickCount() ) { lastKillTick = getTickCount(); return super.killActors( range ); } return false; } We have discovered how the MazeActor and AutoActor classes form the foundation for implementing automated players within the MazeWars application. Together, these classes supply the interfaces to exist within the gaming environment and invoke the onTick() method to allow the actors to animate themselves. We can now fill the maze with Hunters, Sharks, Bouncers, and Splats. All our game is missing are bigger and better weapons to terminate them.
WEAPONS What game would be complete without an AK-47 or a flame thrower? In a typical arcade game, the actors are only half the story.
Weapons are the other half. I find it much more satisfying to run around a maze with a flame thrower than a knife. To keep our audience happy, it is time to discover how to integrate assorted weapons into MazeWars. Earlier, we described the fireGun() and throwBomb() methods as part of the ActorToGame interface. These methods are provided to allow simple automatic players to use light weapons. The MazeActor class also supports methods for selecting and firing any weapon derived from the Weapon class. The ConsoleActor, the actor who represents the game player within the maze world, uses this weapon interface to perform all its weapons activity. The weapon interface within MazeActor is listed in Table 14.3. Table 14.3The MazeActor weapon methods.
Method
Description
boolean canPickupWeapon()
Determines if the actor can pick up and use weapons
Weapon addWeapon( Weapon w )
Adds a new weapon to the actor’s weapon list, or transfers ammunition to an existing weapon of the same type
void removeWeapon( Weapon w )
Removes a weapon from the actor’s weapon list
void selectWeapon( ... )
Selects a weapon from the actor’s weapon list
Weapon getSelectedWeapon()
Retrieves the currently selected weapon
void fireSelectedWeapon( int direction )
Fires the currently selected weapon in the indicated direction
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 MazeActor weapon interface is quite simple. Conceptually, an actor can keep having weapons added to his arsenal with calls to addWeapon(). Before this happens, the canPickupWeapon() is called to check if the actor is capable of manipulating weapons using the generic Weapon interface.
TIP: The Mystery Class Weapons are introduced to the players via objects of the Mystery class. At random times, the game will create a Mystery object, install some form of weapon within it, then place it in the maze. The Mystery object will wink out of existence after a random amount of time. If an actor catches the Mystery object before it disappears, then the weapon is transferred from the Mystery object to the actor. As you might guess, the Mystery class is derived from AutoActor. Its onTick() method tests for an actor at its location, then performs the weapon transfer using the MazeActor.addWeapon() method.
The addWeapon() method is not as simple as you might suspect. As you add a weapon to an actor, the addWeapon() method will first check if the actor has an existing weapon of the same class. If it does, the ammunition is transferred from the new weapon to the existing weapon, and the new weapon is discarded. This allows the weapons scattered around the maze to act as either weapons or “ammunition packs,” based on the players current arsenal. The selectWeapon() method will select a weapon from the actor’s weapon list. The fireSelectedWeapon() method will fire the selected weapon in the specified direction. Note that, unlike the fireGun() and throwBomb() methods, fireSelectedWeapon() does not accept a range parameter. The range a weapon will fire is maintained as part of the individual weapon data and adjusted independently from the instruction to fire. This is important because an actor will probably have more than one weapon at its disposal. The actor assumes that each weapon will use the appropriate range setting. The fireGun() and throwBomb() methods, however, assume the actor is willing to calculate the range each time the weapon is to be fired. HOW DO THE WEAPONS WORK? Before we can describe how weapons work, we must investigate what a weapon must do. To interact with the game, a weapon must be able to paint itself while firing and indicate to the game which actors it has killed. These are the lowest common denominators in the conceptual model of a weapon. How this functionality is achieved depends on the particular weapon. Within the MazeWars context, a weapon of mass destruction must assume a greater responsibility for its actions that a lowly handgun. The reason for this lies within the tradeoff between coding and performance. We may implement simple weapons by leveraging existing mechanisms within the game. Our friend the AutoActor class allows us to both animate objects and kill other actors. The resulting performance is quite acceptable for low to moderate actor counts. Weapons that affect a large number of cells would be too great a burden if they were to be managed by the game with an actor for each cell. Instead, the weapon must perform the painting and logic itself. The Gun class is used to implement a handgun. When the player fires a Gun, the weapon instances a Bullet object and sends it on its way. The Bullet class is nothing more than an AutoActor that travels in a set direction until its range expires, or it hits a wall. The duties of painting the screen and killing people are effectively delegated from the Gun weapon to the Bullet actor. Because the bullet is an AutoActor, the painting and kill detection are performed within a trivial onTick() method.
The NeutronBomb class represents a weapon of mass destruction. Depending upon the range setting, a NeutronBomb can obliterate anything within 2 to 20 cells. Because of the large numbers of cells involved, we cannot use AutoActor-derived objects to perform the painting and kill testing. At its peak, the explosion could require 400 (20×20) AutoActors to cover all the exploded cells. This number of actors would overwhelm the game logic and cause the game to become unresponsive. Instead, NeutronBomb must perform the entire blast operation itself. To do this, the NeutronBomb must be able to paint directly to the screen, kill actors, and possibly animate itself during its firing phase. To fulfill its duties, the NeutronBomb must make extensive use of an interface to the game and screen. This interface is encapsulated in the WeaponGameBridge class.
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 WEAPONGAMEBRIDGE CLASS The WeaponGameBridge class fulfills a similar role to the ActorGameBridge class. It provides a single interface for a Weapon to interact with while performing its diabolical duties. Table 14.4 describes some of the interface methods within the WeaponGameBridge class. Table 14.4Interface methods within the WeaponGameBridge class.
Method
Description
void initFiring( )
Indicates the weapon has started firing
void endFiring( )
Indicates the weapon has stopped firing
void addMazeActor()
Adds an actor to the game
void removeMazeActor()
Removes an actor from the game
void drawCell()
Draws an image to a cell on the screen or the screen buffer
void refreshArea()
Refreshes an area from the screen buffer or from the game state information
void killActors()
Kills all actors at a cell
Each weapon has an associated WeaponGameBridge to act as its interface to the game. When a weapon is fired, it indicates the beginning and ending of being fired with calls to initFiring() and endFiring(). The time between these calls may be as little as a single method call, to many seconds, depending on the weapon. While a weapon is being fired, it receives special privileges. It can make calls to addMazeActor() and removeMazeActor() to introduce AutoActors, like the Bullets we discussed earlier. It also receives notifications similar to the onTick() method of the AutoActor class. The tick method for a Weapon is called onFiring(). The onFiring() method is designed to give weapons that cannot use AutoActors a chance to interact with the game on a regular basis. Within this method, the weapon can call WeaponGameBridge.drawCell() and refreshArea() to manipulate the screen directly. In addition, the weapon can kill actors at any cell with the killActors() method. Let us return to our friend the NeutronBomb. Listing 14.7 details the onFiring() method. The method uses a counter, tickCount, to choose between the two images of the armed bomb. It then draws the image to the screen using getWGB().drawCell( cell, mci, true ). This statement retrieves the WeaponGameBridge and draws the image. The boolean value at the end of drawCell() indicates to draw directly to the screen, bypassing the screen’s double buffer. Alternating the drawing of the two images has the effect of flashing the bomb on the screen. A little warning of the mayhem to come. The onFiring() method then calculates the time to detonation. This value cycles from five to zero, with zero being detonation. When the time changes from its previous value, a countdown sound is played, and when the time reaches zero... kaaBoom(). After exploding, the bomb indicates it has completed firing with a call to getWGB().endFiring(). Listing 14.7 Wiping out enemies with the NeutronBomb.onFiring() method. public synchronized void onFiring() { // Flash the Neutron Bomb! MazeCellImage mci = ++tickCount%2==0 ? mazeCellImage1 :
mazeCellImage2; getWGB().drawCell( cell, mci, true ); // Calculate the time. long seconds = (System.currentTimeMillis()-startTime)/1000; long time = 5-seconds/2; if ( time < 0 ) time = 0; // Play the countdown. if ( time != lastTime ) { countDownSounds[(int)(time)].play(); lastTime = time; } // The grand finale. if ( time == 0 ) { kaaBoom(); getWGB().endFiring(); } } The kaaBoom() method obliterates the area around the bomb. The painting of the bomb explosion is performed by drawing expanding rectangles of destruction about the bomb. The rectangles are drawn into the maze’s double buffer with calls to drawCell ( cell, image, false ). When the cells have been rendered to the double buffer, a call to refreshArea() transfers the buffer to the screen in a single operation. This generates a nice explosion effect. Actors are killed during the same process with calls to killActors ().
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
INTERFACING WITH WEAPONS Controlling the functionality of a weapon provides a unique challenge. Programatically, a class derived from Weapon can expose all its configurable aspects using methods. AutoActor classes could then be programmed to utilize these interfaces to their fullest extent. Those actors that do not recognize a particular Weapon class would have to settle for using the methods within the base Weapon class and forgo any of the advanced features. This approach poses a problem for the ConsoleActor class. As the players interface to the game, ConsoleActor must provide the ability to exercise any weapon option, and yet, we cannot pre-code it to support all Weapon classes. Thankfully, the solution to our dilemma lies in Chapter 5 and the EventManager class. The application uses an EventManager object to act as a distribution point for all mouse and keyboard messages. The ConsoleActor object is connected to the application’s event manager, so that it can receive keystrokes to control its actions. Similarly, the selected weapon for the ConsoleActor is also connected to the event manager. While a weapon is selected by the ConsoleActor, it sees all mouse and keyboard events. This allows us to develop new Weapons with a keyboard interface for the ConsoleActor. This interface can support any form of weapon configuration, including selecting types of ammunition or adjusting the range, dispersion, and intensity of flames of a flame thrower. Each of these features would be activated by using a set of keystrokes. When a weapon fires or transfers ammunition, it also broadcasts notification describing the event. The status panel at the top of MazeWars registers for notification from the currently selected weapon. This way, it can always keep an accurate reflection of the weapons ammunition.
SUMMARY As this chapter draws to a close, we can look back on the lessons we have learned from life within the maze. We have examined how the MazeActor and AutoActor class interface with the game and provide a platform for coding the behavior of automatic players. We have also seen how to integrate weapons into the maze world. Together these subjects provide the functionality for implementing arcade-like games. In the following chapters, we’ll focus our attention toward a different type of opponent as we delve into the world of programming networked games and connecting to live opponents.
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 15 NETWORK GAME PROGRAMMING CHRIS STRANC
J
ava and networks. The two words go hand in hand. When you take the plunge and start sending data down sockets, you enter a
realm of uncertainty and complication. Your game no longer lies within the relatively safe realm of an application memory context. Now, it must live as an entity that traverses the ethers. At each stage of play, your application must be concerned with network delays, dropped connections, and a host of network-related errors. Still, one of the lures of writing in Java is its inherent ties to the network and the power of the network to bring people together. This chapter is the first in a set of network- and multiplayer-related gaming chapters. Within these chapters, we’ll be examining some of the issues inherent in programming networked games. This chapter will provide some fundamental insights into the realm of writing multiplayer network games. I’ll be introducing the concepts that will provide a basis for discussing the implementation of two multiplayer games that we will develop later in the book. First on our agenda are those fundamental insights I mentioned. In this chapter, we’ll be discussing the following five aspects of network game programming: • Application Connection Topology—This subject describes the flow of communication between the applications that are cooperating to play a game. • Input/Output Model—The set of inputs and outputs for a networked game must be extended to include network-based information. How you structure your game to receive this information will have a major impact on the code required to produce an efficient network game. • Network Error Handling—Sending information across the network exposes the application to network errors. You will be pressed to adopt a consistent approach to allow the game to detect and recover from these errors. • Game State Management—Networked games require that all the applications participating in the game coordinate their efforts in determining who controls the state, and how much of the state is sent to each of the applications. • Human Factors—Connecting applications on different machines introduces some issues in the design of games that do not exist in single-player games. We’ll take a look at these issues and provide general guidance on keeping your players happy. (That is the goal of gaming, right?)
APPLICATION CONNECTION TOPOLOGY When two or more applications cooperate to play a single game, they must share information about the state of the game. In order for this sharing to take place, the applications must create channels for communicating between applications and computers. The layout of these channels is called the connection topology because it describes how the applications are connected. The connection topology is used to describe only the logical connections established between applications—not the underlying physical connectivity (cabling and routers, for example). Game connection topology is independent of the network topology; it is quite possible for all the applications participating in a game to reside on the same physical hardware. The most popular network topologies are star and interconnect. Figure 15.1 illustrates both the star and interconnect topologies. The star topology divides the applications between a server (the center of the star) and all the clients (the points of the star). The
interconnect topology has each application maintain a connection to each of the other applications.
Figure 15.1 Application connection topologies. When choosing between communication topologies, you need to consider the following factors: • • • •
The normal and maximum number of game players The criticality of inter-player game state synchronization Simplicity of game development Applet networking restrictions
Secondary considerations might include: • Volume of network traffic • Robustness of the gaming environment THE STAR TOPOLOGY The star topology, shown in Figure 15.1, is characteristic of an applet/server gaming model. In fact, it is the only topology that an applet can support. This restriction arises from the security controls placed on applets. Applets are unable to open connections to any machine other than the server of their host page.
TIP: Using A Non-Star Topology With Applets If you wish to implement a non-star connection topology using applets, you could structure your server to act as a network proxy and forward your requests to other network destinations. Of course, this is an inherently inefficient approach because each network access will incur twice the normal latency. Requests must first go to the server and then to the desired destination.
The star topology is particularly applicable in systems where there is a natural delineation between the clients (user-interface applications) and the gaming engine. Discrete turn-based games, such as network chess, dominoes, and most card games, frequently fall into this category. In this type of game, client applications send events to the server, which sequentially processes them and updates its own internal state based on their outcome. The server then reflects state changes back to the clients as needed. At any moment in the game, the server has the “master” copy of the game state. Localizing all state management and gaming logic greatly simplifies your coding efforts, freeing you from having to grapple with synchronizing the game state from multiple applications. The star topology may not be suitable in many situations where performance is a critical factor. In these situations, responsiveness might demand that the clients execute the game logic locally and update the player information directly with the client application. This contrasts with the process of formulating a message requesting the server to perform an action, sending the message, and waiting for the reply. Another case where the star connection topology might not be appropriate is in low player count games. If you expect the average game to run two players, then there’s not much point in creating a server application to arbitrate between them.
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 INTERCONNECT TOPOLOGY The interconnect topology, shown in Figure 15.1, has each application maintain connections to all others. This topology has the advantage of fast inter-client communication and good scalability. The communication speed is optimized because each application has a connection to the other clients. This allows direct data transfer with a single network pass. The scalability of the interconnect topology arises from avoiding a dedicated server. The existence of a dedicated server application on a single host computer can cause network and CPU contention as the number of concurrent games rises. The interconnect topology naturally distributes the network communication across all the computers involved, by using a serverless architecture or by nominating a client to perform the central logic. When nominating a client to act as a server, you should take CPU utilization into account. You want to make sure that the burden of executing core game logic is not placed on a single over-worked resource. The interconnect topology carries a fairly high per-player overhead, as shown in Table 15.1. You must manage each connection and broadcast local player state information repeatedly to each client. The additional network management pays dividends by reducing the network latency. Table 15.1COMPARISON OF CONNECTION COUNTS FOR CONNECTION TOPOLOGIES.
Number
Connections
of Players
Star
Interconnect
2
2
1
3
3
3
4
4
6
5
5
10
6
6
15
7
7
21
Consider a game where multiple players are hunting each other within some fictional world. The players are undoubtedly dispersed. Some players might be within shooting range, while others are completely beyond any senses. The most efficient use of network resources is to update those players who are closer more frequently than those that are far away. But what happens when many players enter a confined area? Unfortunately, in this situation, our solution degrades to the non-optimized network traffic solution caused by frequent updates to all of the players. You can adapt the interconnect topology to work with a server application. Under normal situations, the use of a server allows the programmer to simplify the game state management by centralizing it within the server. However, you can also use a server to provide a service for a brief period—during the initial startup phase while players are joining the game or as a resource to manage high score tables, for example. This approach simplifies the coding of the server-based phase, while allowing normal game play to run without causing an IO and CPU load on the server box. The net result is that the system can be scaled to run a larger number of simultaneous games without degrading. I must note that the interconnect topology is probably not viable if you are planning to embed your game as an applet on a Web page. In this situation, the browser that hosts your application will not allow you to connect to computers other than the source of the Web
page.
INPUT/OUTPUT MODEL The input/output model of a standalone application is probably familiar to you already. In the Java world, both mouse and keyboard data are organized by AWT and presented to the application as AWT events. In principal, adapting to a networked gaming environment should involve little more than reading and writing information to and from a socket. However, life always seems more complex than principals, and network programming is certainly no exception. Integrating network input within an application forces you to deal with architectural issues. Programming graphical user interfaces has long been the realm of the event-driven paradigm. Most windowed Java programs are built to respond to a stream of AWT events. Network input fits perfectly into the non-linear programming model that is the heart of an event-driven application. The question that must be answered is how to integrate this network input. Within the Java context, we can define two broad solutions: • Integrate network input into the AWT event model • Develop a separate mechanism to provide network events to the gaming logic INTEGRATING NETWORK INPUT INTO AWT To integrate network input into the normal AWT event stream, you could create a thread to block on network input. When the thread receives a block of data it would then package it and send an AWT event to a pre-specified component to indicate the arrival of the network data. This model, which is shown in Figure 15.2, is simple and efficient. It reuses an existing event management mechanism (AWT) and allows the application thread (in this case, the user interface thread) to block on a single source of input.
Figure 15.2 Integrating network events into AWT. Unfortunately, this implementation’s strength is also its weakness. The integration to the AWT forces you to use AWT on the server side, despite the lack of need for a user interface on the server. The implementation also seems less than elegant from an aesthetic sense. AWT components are user interface elements. It would be normal to expect them to receive user interface events. Adding network events to the normal stream of events seems to be artificially extending the types of inputs an AWT component might receive. Indeed, this inelegance might be a symptom of a more fundamental problem. By passing all the network events to a component, you are implying that the component embodies the heart of the game logic, and merging the user interface and the game logic code would violate many object-oriented principals.
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
INTEGRATING NETWORK INPUT WITHOUT EXTENDING AWT If we choose to manage network input without AWT, as shown in Figure 15.3, we have complete flexibility for the code used to retrieve and process events. This is the only requirement for coding the server side network interface. The client side, however, provides a greater design challenge. To provide a library that will allow us to express the game logic in a clear and complete manner, we must integrate both network input and user interface messages.
Figure 15.3 Integrating network events without extending AWT. Given this constraint, we can see the client application developing with multiple independent event-driven engines running. The game engine would be used to manage the overall activity within the application. It would embody the state of the game and all the networking details. The normal AWT engine is used to deal with the bulk of the user interface activity. In addition, the event handlers for the user interface will generate and submit events to the game engine. Given this functional layout, we have the game engine living between the user interface and the network. It would receive events directly from the network and “pre-digested” summary events from the user interface. NETWORK OUTPUT MODEL Output is the simple part of the network IO subject. All you need to do is to provide an accessible interface for writing data to the network connection. The application might choose to implement a set of methods for sending events to one or all clients of a game. Typically, each client is assigned a unique ID to allow the server a simple means of referencing it. The output method for each connection is best encapsulated within a single object. The writing method should be synchronized to prevent multiple threads from attempting to write information to the connection at the same time. Above this basic model, you may choose to implement protocols for ensuring message transmission and error checking. NETWORK DATA TRANSFER MODEL Information is sent between the client and server to perform numerous roles. The information might describe part of the game state, act as a request or confirmation, etc. The only thing we know about the data is that it is traveling between applications that are probably running on different computers, different operating systems, and quite probably, different hardware architectures. Dealing with such a diverse scenario can be a daunting task. Thankfully, Java comes to the rescue. The common definition of the Java base types across platforms greatly simplifies coding. In addition, Java standard libraries provide the DataInputStream and DataOutputStream classes, which deal with the same stream format on all platforms. This means that an int streamed through a DataOutputStream on an Intel x86 will be transferred to the wire in a neutral format that can then be read correctly through a DataInputStream on a Sun Sparc system. All issues with the internal layout of the fundamental types are managed automatically.
NETWORK ERROR MANAGEMENT
A complete strategy for managing errors within a networked application is essential. These errors range from lost or corrupt packets to dropped connections. Their presence has a dual impact on your program. First, your data may be corrupted or lost. Second, you must be prepared to handle network errors anywhere you read or write network data. This should have a profound effect on the way you code your network interface. You have several choices when it comes to dealing with corrupted or lost messages. You can choose to echo data (very inefficient); generate message check sums and cache old messages for possible retransmission; or adopt a variable reliability level where some messages are sent with no additional overhead. Then some are cached and guaranteed, and some are forced to have receipt acknowledgment. A good approach is to use all these mechanisms, deciding which one to use based on the frequency of the message type and the importance of the data. Status messages, or even some forms of game state information, may be a large portion of your games interapplication communication. In this case, you might choose to send these with little error checking/correction capability. However, a message to indicate a state change is probably critical and worthy of a guaranteed delivery. Your application must be able to handle a variety of network errors whenever data is read from or written to the network. The only sane way of dealing with this eventuality is the implementation of an interface whose responsibility is to deal with these issues without disturbing the gaming logic with a plethora of IOExceptions. This layer should be responsible for things like requesting retransmission, implementing the reconnect protocol, and detecting dead connections.
GAME STATE MANAGEMENT The game state is defined as the collection of static and dynamic information that describes the playing area and all the players. With standalone applications, this information resides solely within the memory allocated to the game and its data files. In a multiplayer network game, this simplicity is irrevocably removed. Because you have multiple clients playing the same game, each must have a view of the game state. The view that these clients get and the management of the overall state are fundamental questions in the design of a game. In particular, before you begin coding a multiplayer game, you should have a clear answer to the following questions: • Who will manage the game state? • What view of the game state will each of the clients have? • How accurate will the clients game state be? WHO IS MANAGING YOUR GAME STATE? Based on our definition of the game state, we have three possible candidates to manage the information that describes the playing area and all the players. If you are developing a game using a star connection topology with a server application, then the server is an obvious choice for managing most (if not all) of your game state and changes. If you choose the interconnect connection topology, then you can use a single client as a pseudo server with control over all the game state. The final option has all the clients managing the game state cooperatively, with little or no help from a server application.
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
Server Game State Management The server-managed game state is definitely the simplest way to write a multiplayer game. Because a single entity (the server application) stores and changes the game state, there are no synchronization problems. The server’s game state is, by definition, accurate. This delineation removes all need for code designed to resolve the slightly divergent worlds that would be inevitable if multiple applications were allowed to modify the game state. Centralizing the game state and logic within a server is not a reason for writing artificially thin clients. Typically, the clients need a view of the game state so they can present it to the user. This view is frequently complete enough to validate many of the user actions against the rules of the game. The client code should probably be smart enough to tell you when you have run into a brick wall or are attempting to attack a country that you already occupy. Having the server perform such tests may cause excessive network traffic, slowing down system response.
Nominated Client Game State Management The lack of a separate application whose sole purpose is to manage the game state does not preclude structuring the game with a server. It is quite possible to have identical clients within a game, but then nominate a single client to have the master copy of the game state. You might even choose the best client for this purpose by testing each client’s host for processing power and total network latency for contacting all the other participants. This mode of management has an advantage over the server game management: Because the clients are assigned the task of managing the game state, no single machine has to take on the workload of supporting all the games currently running. This flexibility removes a potential performance bottleneck for a large set of concurrent games.
Client Game State Management Having each client application cooperatively manage the game state is the most complex means of game state management. The complication arises from having to synchronize data that might be changed by multiple applications at the same time. You can reduce these problems to a minimum with careful planning and serious thought on which application connection topology to use. The interconnect topology cuts in half the time required for the star topology to propagate the game state between clients. Events are posted directly to other clients rather than posted to a server, then echoed to the other clients. Client game state management may also be used if the players are linked using a star topology. In this situation, the server application can be used to broadcast changes to the game state in response to messages from the clients. There is a delay in updating the other clients because of the additional network communication between the client and the server. This method of management might be useful for applet games. The local management of the game state will generate a responsive user interface within the applet. Because the applets are constrained to connecting only to their host computer, the star topology is predetermined. TIMING ISSUES FOR PLAYER FEEDBACK Local management of the game state has the great advantage of enabling instant feedback to the player. This is achieved by calculating the result of the player actions using the game state data stored within the local application. The user interface can then be updated in parallel to the network broadcast describing the change to the game state. This approach makes it ideally suited to continuous action games.
Centralized game management requires two network transmissions prior to updating the user interface. The first message would signal the action to the server, and the second would carry the result of the action back to the client. However, centralized game management does not imply that all player actions will require network activity to the user. Basic navigation around the game, and most forms of opponent independent action, could probably be carried out by logic on the client side, possibly updating the server as needed. In such a situation, requests to the server will only occur when an action will require the simultaneous accurate knowledge of two or more player states. Figure 15.4 illustrates the comparison between centralized and localized event handling.
Figure 15.4 Comparison of central and local event handling.
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 CLIENT GAME STATE VIEW When you divide the game into multiple client applications and a server, you must also choose which portion of the game state each client will see. The portion of the game state that is transferred is called the view of the game state. In general, your game design must balance how much of the game state to propagate. The main reason for restricting the view of the game state is the network traffic involved in the transfer. You can include static content—such as the physical model of the game world—in the game state view. You would only need to transfer the static content once; the client would reuse the content over and over (repainting the game image, for example). Dynamic aspects of the game state pose a greater risk. This information must be broadcast to multiple clients on a regular basis. This repeated broadcast can effect the performance of the server. Before adding dynamic information to the client view, I suggest you evaluate whether the logic that needs the dynamic information might be better placed within the server application where the complete game state is maintained. If you do choose to migrate dynamic game information, you should consider the rate of change of the data. A relatively high rate will increase network traffic and slow down your response time. An alternative is to send information to other players on a need-to-know basis. Unfortunately, migrating dynamic information to the client applications brings up synchronization problems. What happens when the outcome of an event needs to know information about two or more players at the same instant? Because of network delays, this is physically impossible. The transmission time assures that at least one set of player information is out of date. To decide the outcome, one of the states must be chosen and acted upon. One final reason for keeping dynamic data away from client applications is protecting the general public. Yes, there are people out there with too much time and a compiler. Their only goal in life is to write an automatic player that runs around shooting all the unsuspecting humans. The automation will win too because it can see all the information the user interface does not show the humans. Don’t laugh, I’ve done it.
HUMAN FACTORS As anyone who progams games knows, players are an increasingly demanding audience. If you want your games to be successful, listen to what players want; catering to their whims will set your game apart from dozens that do not. In this section, we’ll discuss some things your game can do to appease the fickle player. INCREASE THE INTERACTION BETWEEN PLAYERS A key philosophy for network programming is this: The network connects people as well as computers. When you combine this with the fact that a human/human interaction is typically more exciting than a human/machine interaction, you have given your application an edge over all those single-player games. Multiplayer gaming allows the player to compete against other people, and it’s always more fun to triumph over man than machine. It is important that you find paths to maximize the interaction between the players in your games. Your solution might be as simple as providing a high score table to allow players to show their greatness, or providing players with a means to chat while the game is in progress. BEWARE THE ETERNAL DOWNLOAD
A word of caution: If the cumulative downloading of your applet exceeds the player’s patience, you’ve lost a customer. You must be careful to keep your audience entertained; surfers are a group of folks who demand immediate gratificaion. If you absolutely have to download a large body of information, then do it in stages, and be sure to provide some form of entertainment during the process. START THE GAME ON TIME After your game has been downloaded from the network, your customer is ready to play. But, wait. There’s a hitch. We’re writing a multiplayer game. It’s fair to assume that we may also need to wait for at least one other player. For the gamer, this wait period may be even more frustrating then the download time. What can you do? You can’t exactly provide a progress bar indicating when the next player will show up. The solution to this lies in the nature of the game. Your game may allow new players to join running games. You may allow players to play practice games against automated players until a human arrives. One final possibility lies in organizing games independently of running the game itself. Players can register to play games with an external agent and receive notification (possibly by email) that a game will be available at a certain 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
KEEP THE USER INTERFACE RESPONSIVE The user interface should always be responsive. Thanks to Java threads, it is simple to separate repainting the user interface from the actual processing of the game logic. Is this enough? That is a question for the game designer and the players to answer. AWT does not provide a tremendously rich programming interface. Beyond the overused status message, there is precious little space for feedback. If your applet has distinct busy phases, or modes of play, you might choose to provide an animated bitmap in the upper-right corner (just like your favorite browser) to indicate the status of your applet. Any other clear form of feedback will always be appreciated by the user. PLAYERS LEAVE GAMES Oh dear, it’s 2:00 am again. Time to abandon the game.... It’s a fact of life that players will leave the game before it is completed. They may leave because they want to, because of network problems, or because they are sore losers. The one thing you can be sure of is that a player may exit the game at any time. Your game must be able to accommodate the premature departure of a player at any time. You owe it to the players who remain in the game. MAYBE THE MEEK WON’T INHERIT THE EARTH Writing network action games presents a unique challenge in satisfying the player. When a player performs an action (like shooting a weapon), they expect instant feedback. Unfortunately, network communication is anything but instantaneous. In fact, if there is anything a game programmer must know about network communication it is this: Transmission time varies, and it is always slow compared to operations on the local hardware. So to satisfy our players desire for rapid feedback, we may need to decide the outcome of the action locally, then broadcast the result of the action. Deciding the outcome of an action is local game state management, and it brings with it the specter of resolving divergent game states. I think an example will help you to understand. Consider this: Player One has decided to terminate Player Two. At the same time, Player Two gets an uneasy feeling and starts to run behind a nearby wall. Player One raises her gun and fires. What is the outcome? In the real world, Player Two would have made it behind the wall before the bullet came whizzing by. What happens in the cyberworld? Player Two sends a message indicating he started running. Player One sends a message indicating a shot was fired. Player Two gets shot, despite the fact that within his world he is safe. Why? When Player Two receives the message describing the trajectory of the bullet, he can calculate he is safe. However, Player One receives the message indicating that Player Two started running just before the shot was fired. If you were to update Player Two’s position immediately, he would escape by jumping unrealistically to the safety of the wall. This would not be gratifying to Player One—she wants to see the bullet impact. Indeed within her experience, she knows that Player Two cannot move fast enough to avoid the bullet. At the time of impact, the two players have desynchronized models of the world. The difference is slight, but it has a great effect on the health of Player Two. How should we resolve this difference? The answer boils down to whose model of the world is more important? It turns out to be the aggressor’s. Player One would be acutely aware of when the bullet was fired, the act of firing focused all her attention on that single event. It would disturb her greatly if the bullet did not reach its mark because Player Two blinked to a new safe position.
So what happens in Player Two’s poor world? He gets shot. We know that for sure. But everything else is up for negotiation. Once we decide that the aggressor’s world rules, we no longer need to send a message indicating the firing of the bullet. Instead, we can send the outcome of the event. In this case, Player Two receives the message telling of his misfortune. The user interface for Player Two can then make the noise of a weapon firing, wait, then inflict the appropriate damage on Player Two. Notice how we do not even try to tell Player Two when the bullet was really fired. We simply suggest that a bullet was fired just before he got hit.
SUMMARY Within this chapter, we have examined some of the basic concepts and issues surrounding multiplayer networked games. The topics ranged from the technical aspects of integrating network data into the application to the human aspects of resolving conflicting views of the world. The next chapter will focus on the actual implementation of a simple multiplayer applet game called Domination.
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 16 DOMINATION CHRIS STRANC
I
n the previous chapter, we studied the concepts of multiplayer networked games. Your reward for being such a patient student
comes in this chapter. Within these few pages, we’ll implement a multiplayer game called Domination, building on the concepts we discussed in Chapter 15. We will use Domination as a tool to explore some of the technical issues that arise while developing a multiplayer network game. The implementation uses a set of generic classes for providing network support and coding the game and server logic. I’d like to take a moment to give you a bit of background on Domination to get your competitive juices flowing.
INTRODUCING...DOMINATION Domination is a game of world conquest (now, if that doesn’t get your blood flowing, I’m not sure what will). The world remains divided into continents and countries. Players take turns waging wars on their neighbors with the eventual goal of world domination. Before playing Domination, a player must join or create an open game. When all the players in an open game indicate their desire to play, the game will close to new players, and normal play will begin. Figure 16.1 shows the Domination applet interface.
Figure 16.1 The Domination applet. When game play starts, Domination arbitrarily assigns the players to countries around the world. The initial distribution of a player’s armies among their countries is also assigned arbitrarily. The players then start taking turns attempting to better their position of world domination. Each turn consists of distributing newly created armies to re-enforce existing countries, followed by an attack phase. During the attack phase, the active player selects the attacking and defending countries, and the strength of the invading force. The game will then determine the losses to both the attacker and defender. If the defending country loses all its armies, then the attacker occupies the country with the remainder of the invading force. The game continues until the world is occupied by a single player. THE DOMINATION ENVIRONMENT The Domination environment, shown in Figure 16.2, is representative of simple multiplayer network games. The Domination applet is the user interface for the game. The DominationServer application acts as a server for creating and running individual games. The
applets connect to the server using a star connection topology.
Figure 16.2 The Domination game environment. The DominationServer application is responsible for managing the game state. All changes to the game state are made by the server and broadcast to the client applets. Typical state changes range from changing the active player to inflicting losses during a battle. The server is responsible for executing the actual game logic and controlling the flow of the game. In order to render the world map in a timely manner, the applet maintains a duplicate copy of the entire game state. The game state consists of both static and dynamic data. The static information includes the definition of the continents, countries, and valid paths of attack. The dynamic information details which players occupy which countries, and the size of the occupying force.
TIP: Coding Data Within Java Source Files I coded the static game state information as data and methods within a class that extends DominationWorld. This approach has numerous advantages. First, you can create and manage the world data in a human-readable form (Java source code). A simple compilation transformed the data into the portable compact binary form of a Java .class file. Second, the information is loaded across the network by a single call to Class.forName().newInstance(). The class manager assumes all responsibility for locating, transferring, and loading the class file into memory. Finally, the application does not need to parse the data file prior to its use. All that is needed are calls through the virtual interface of the DominationWorld object. This may seem like hard-coding your application. Is that wrong? That is a personal judgment. If your interface is clean (captured by an abstract base class), then there seems little concern, and great benefits for coding data within Java source files.
DominationServer and the Domination applet cooperatively manage portions of the game interaction. This cooperation takes the form of the server signaling an applet to perform a particular phase of the game. The applet will then enter a mode where it performs the requested duties. Typically, this entails gathering user input, validating against game rules, and sending messages back to the server. This approach has the beneficial effects of minimizing network traffic and ensuring good response times for the player. You now have a high-level view of how the Domination game operates. Next, we will describe the classes that form the core of the Domination applet and DominationServer application.
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 DOMINATION GAME The Domination applet and DominationServer application are built using a common set of core classes. These classes form the GameLayer/GameEvent toolkit (GLE toolkit), which is shown in Figure 16.3. The GLE toolkit encompasses the following functionality: • Provides an efficient and versatile network communication vehicle • Provides an event-based gaming environment that captures user interface events, network events, and game notifications • Allows the programmer a simple means of expressing the game logic
Figure 16.3 The GameLayer/GameEvent architecture. The GLE toolkit uses the GameEvent and GameLayer classes as the foundation of an event-based subsystem within both Domination and DominationServer. The GameEvent class provides an abstract definition for the events that flow through the GLE subsystem. The GameLayer class provides the framework for routing and processing GameEvents. Domination and DominationServer share an identical architecture. Input from the network and user interface is stored within GameEvents. These events are then passed through the stack of GameLayers. Each layer is free to use the event, pass it to the next layer, or both. Wrapping the user interface and network information within GameEvents allows us to manage all forms of input in a consistent and efficient manner. It should be noted that each layer within the application is actually an instance of a class that extends GameLayer. The derived classes add different functionality to the layers within the application. The stack of GameLayers within an application is used to isolate the game logic from other details, such as the network management. This generates a clean separation of the functionality within the application. The lowest layer is derived from the ConnectionLayer class and manages network connection. This function includes implementing the reconnect protocol, reading GameEvents, and acting as a conduit for sending GameEvents across the network to the other applications. The top GameLayer encodes the game’s logic. This logic is expressed in terms of handling GameEvents. Coding the game logic based on GameEvents provides a level of abstraction for the game itself. The user interface interprets a mouse click on the world map and generates GameEvents signaling the selection of countries. The same effect might be accomplished by selecting a country from a list or even by an automatic player with no user interface at all. Now that you have a basic understand of the Domination and DominationServer design, we’ll move our focus to the classes that make up the application. THE GAMEEVENT CLASS GameEvents are the life blood of both Domination and DominationServer. The entire event-driven architecture is based on the movement of GameEvents within the layers of the application. In reality, GameEvents cannot be instanced because they are abstract, and the core classes are actually manipulating objects of the derived class, DominationEvents. The GameEvent class captures very little information. It has an integer value (eventType) that describes the type of the event being performed and an integer value (clientId) denoting the source or target player identity. The event types are defined within both the
GameEvent and derived classes (DominationEvent). The player identity is assigned by the server application when the player connects. This value is guaranteed to be unique for the particular instance of the server. No space is set aside for the data contents within a GameEvent. Instead, the class defines two abstract methods, writeDataContents () and readDataContents(). These methods are called to allow derived classes to stream their contents to and from a data stream, as shown in Figure 16.4. These methods allow the GameEvent.writeEvent() method to stream its content to a network connection and readEvent() to initialize an existing object using the streamed data. This definition of GameEvent allows complete flexibility over the data that is to be managed by the gaming classes.
Figure 16.4 Layout of streamed GameEvent. When a GameEvent is sent across the network, it is packaged with the data contents surrounded by header and trailer blocks. These guard blocks are used to validate the GameEvent as it arrives. GameEvents are central to the flexibility of the GameLayer/GameEvent architecture. By abstracting the management of GameEvents from their data content, we have created a framework for the gaming engine for both the client and server side of the game, and the conduit between them. GameEvents provide more than a means of passing event data between multiple applications. They can also act as the interface data between the user interface and the game logic within a single application. The client application can store data in addition to the content that is streamed across the network. For example, the user-interface code may choose to reflect some user-interface events to the client-side game logic. In this case, a GameEvent extended class could contain an AWT event member. The user-interface code would populate this member and store it to the game layer for processing. The code to writeDataContents() and readDataContents() would ignore the AWT event member.
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 DOMINATIONEVENT CLASS The DominationEvent class extends the GameEvent class and supplies storage for the information that passes between Domination and DominationServer. The data needs of the Domination game require managing two dynamic arrays, one for integers and one for strings. The content of these arrays is context sensitive based on the event type value. This data is streamed to and from the network using the writeDataContents() and readDataContents(). Listing 16.1 shows the code for the DominationEvent. writeDataContents() method. Listing 16.1 DominationEvent.writeDataContents(). /** * Writes data to the specified stream. */ protected void writeDataContents( DataOutputStream outputStream ) throws IOException { // Write integer data. outputStream.writeInt( intCount ); for ( int i=0 ; i
16.2. The call to retrieveEvent() will either retrieve a queued event and return immediately or, if there are no queued events, call wait() to suspend the layer/thread. A suspended GameLayer will be notified when someone stores a GameEvent in its event queue using the storeEvent() method. This method calls notify() after storing an event for the targer layer. The call to notify() will awake the suspended thread and allow it to process the new event. Listing 16.2 The default GameLayer event-processing loop. public void run() { while ( keepRunning ) { GameEvent e = retrieveEvent( ); if ( processGameEvent( e ) == false ) passEventUp( e ); } } Blocking a GameLayer on the arrival of a GameEvent introduces a tremendous simplification in the system design. With this design philosophy, both local (mouse and keyboard) and remote (network) activity are translated into GameEvents. The game logic is written in terms of the arrival of game events, rather than the specific activities that generated them. When the game player selects an object, the user interface need only send a GameEvent indicating the object was selected. The user interface does not need to determine the current mode of the game to determine the appropriate action. A similar simplification is realized within the GameLayer. The GameLayer need not concern itself whether an event was generated by the user clicking on an image or a list, or by typing something at a prompt. It receives a simple notification that a specific event has occurred. Interface independence has been introduced. After retrieving an event, the GameLayer will normally call processGameEvent(). This method supplies default processing logic for the GameLayer. If processGameEvent() returns false, then the event is passed to the next layer in the chain. In this manner, the lower layers can manage the network connection and implement protocols surrounding specific event types while the upper layers can manage all the game-related messages.
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
CODING GAME LOGIC WITHIN A GAMELAYER During the course of a game, a GameLayer object will be expected to process many GameEvents. The response to these events is based on the event type and the state (or mode) of the application. Many of the event types are managed the same throughout the duration of game play. These event types are typically processed within the processGameEvent() method. Some event types are managed differently at different times during the game. This situation might arise when the client or server enters a special mode. The mode may have a unique set of event types that are not seen at other times during the game, or it may require the use of alternate processing for a common game event type. To implement a special mode within the game, all you need to do is examine and process events prior to calling processGameEvent (), as shown in Listing 16.3. This leads to a simple, maintainable expression of the possible state transitions within a game. You can use the same mechanism to code modes within both the server and client sides of the game. Encapsulating a game mode within a single method has many advantages. You can code any state variables that are specific to the mode as local variables within the method, rather than members of the game logic class. By examining the code within the event loop, you will see the additional functionality of the loop. The code before and after the event loop describes the initialization and cleanup for the mode. Listing 16.3 ClientGameLayer.onDeployArmies(), coding a mode for the game. /** * Enter a mode to deploy the reward armies. */ protected void onDeployArmies( DominationEvent e ) { // Tell everyone who gets what armies. Domination.setStatus( e.getString(0) ); // If we are not deploying armies this is the end... if ( e.getInt(0) != getClientId() ) return; // Drop into a loop to deploy Armies. int armiesRemaining = e.getInt(1); Domination.setStatus( "Deploy using click and shift-click...\n" ); while ( keepRunning && (armiesRemaining>0) ) { e = (DominationEvent) retrieveEvent( ); switch ( e.getEventType() ) { case DominationEvent.C_COUNTRY_SELECT : armiesRemaining = onDeployCountrySelect( e, armiesRemaining, true ); break; case DominationEvent.C_COUNTRY_SHIFT_SELECT :
armiesRemaining = onDeployCountrySelect( e, armiesRemaining, false ); break; default : // Allow my upper reaches to see // this game event, but bypass my implementation. super.processGameEvent( e ); } } if ( armiesRemaining==0 ) { // Confirm the deployment. e.setEvent( DominationEvent.C_DEPLOY_COMPLETE ); writeEvent( e ); } } CONNECTING TO THE NETWORK The GameLayer that exists closest to the network is called the connection layer. It is an object derived from the ConnectionLayer class. The duties of the connection layer include sending and receiving GameEvents, and dealing with network errors. A connection layer uses a Connection object to perform the actual input and output across the network, as shown in Figure 16.5. The Connection class extends Thread. Its run() method drops into a loop, which blocks trying to read a GameEvent from the network. When an event is read, it is stored within the connection layer for processing. Connection also provides a synchronized method for writing a GameEvent to the network. Connection is a private class implemented within the ConnectionLayer.java file.
Figure 16.5 ConnectionLayer/Connection interface. 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. This philosophy is captured within the ConnectionLayer and Connection classes. The private routines that are used to send and receive GameEvents can all throw IOExceptions. These exceptions are not passed beyond the public send/receive methods. Instead, the public methods used for GameEvent IO call the private members from within try blocks. If an error is detected while attempting GameEvent IO, the connection layer will call onReadEventError() or onWriteEventError() to attempt to correct the situation. The IO operation will then be retried until the operation succeeds or the error handler signals to abandon the connection. In the event of an abandoned connection, a GameEvent of type CONNECTION_DROPPED is generated and passed to the connection layer and onward. When game layers see the CONNECTION_DROPPED event, they will typically signal the end of their processing, and the threads will free their resources and end. Signaling a dropped connection as a result of a network error is the response of last choice. Prior to this end, the connection layer will have called onReadEventError() or onWriteEventError() to attempt to recover gracefully. These methods are abstract in the ConnectionLayer class. Within the DominationServer application, they are implemented within the DominationServerConnection class. Both methods call the onEventError() method in the generic class ServerConnection. This method merely waits for a second, then returns. The return code determines if the IO operation should be retried or the connection aborted. If there has been no successful activity on the connection during a pre-specified time, the return will indicate aborting the connection. The client response to network errors is more inspiring. As with the server code, both read and write errors are passed to a member called onEventError(). This member is implemented in the DominationClientConnection class. The method will attempt to reconnect to the server. If the reconnection fails, the connection is dropped; otherwise, the IO is repeated.
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 summary, when IO errors occur between the client and the server, the client will attempt to reconnect. The server will wait, hoping the client will successfully reconnect before a time-out occurs. Regardless of how the situation is resolved, all IOExceptions are caught by the connection layer, and the remainder of the game logic is either unaware of the activity or revives a GameEvent indicating the dropped connection. This solution proves to have the following advantages: • Network error handling is well-defined and isolated within four classes. • IOExceptions are managed entirely between the Connection and ConnectionLayer objects. This is important because the other game layers exist as separate threads operating in an asynchronous manner. This prevents the propagation of an exception from one layer to another. • The bulk of the game is uneffected by low-level network issues, unless the connection is dropped. The GameLayer/GameEvent design proves to be a powerful ally in the quest of developing a robust networking application. By concentrating all the network IO in a few classes, we can increase the chance that network problems will be handled in a consistent fashion throughout the entire application. THE DOMINATION APPLET Now that you have a good working knowledge of the GameLayer/GameEvent toolkit, it’s time to see how we can use its functionality to implement the Domination applet. We’ll examine the applet by discussing the various threads that are running within it. These threads are displayed in Figure 16.6.
Figure 16.6 Thread objects within the Domination applet. We’ll begin our exploration with the main applet thread. This thread is devoted to the user interface. It fullfils the typical role of processing AWT events and painting the screen. The image of the world is the focus of much of the activity. The world is painted using a canvas. The canvas responds to both mouse and keyboard activity. When the player clicks on the map, the canvas will detect which country was selected. It will then pass this information to the game engine by creating a DominationEvent of type C_COUNTRY_SELECT or C_COUNTRY_SHIFT_SELECT, and storing it to the ClientGameLayer object. Keystrokes are echoed to the game engine in a similar manner. The ClientGameLayer is responsible for encoding the client side of the game logic. The majority of the class is DominationEvent handlers. These methods are called as a result of receiving particular DominationEvent event types. These handlers will typically update some aspect of the game state and reflect the changes in the user interface. They are normally called from the GameLayer. run() main loop via a call to processGameEvent(). ClientGameLayer also has several handlers that implement special modes within the applet. ClientGameLayer.deployArmies() and ClientGameLayer.attackRegions() do not return immediately; instead, they drop into their own retrieve/process loop. Within these loops, they process the event types that are specific to the current mode of communication with the server.
We have described the role of the DominationClientConnection and Connection objects in supporting the network interface between Domination and DominationServer. That leaves the ClientHeartBeat thread. The purpose of this thread is to ensure that there is traffic over the network. Every GameEvent.HEARTBEAT_PERIOD milliseconds (10 seconds), this thread wakes up and sends a GameEvent.HEART_BEAT event to the server. This forced activity ensures both sides of the game can establish when the connection is broken. The client will know because the send will fail, and the server will know because there has been an explicit exception, or excessive idle, on the connection. That’s it. The Domination applet. It should now be clear how the GLE toolkit allows us to process user interface and network events in a consistent, efficient, and understandable manner. What more could we want? Our next step is to look at the other side of the network—the DominationServer application.
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 DOMINATIONSERVER APPLICATION The DominationServer runs on a central machine, ready to facilitate people who wish to play Domination. It must coordinate new applets joining and playing games, and it must facilitate the games that are in progress. Clearly, this is a more complex creature than the single-minded Domination applet. As with the Domination applet, we’ll investigate DominationServer by first examining the threads that run within it. DominationServer’s thread objects are shown in Figure 16.7.
Figure 16.7 Thread objects within the DominationServer application. The first group of threads within DominationServer—management threads—are responsible for overall management of the application and for providing central services to all the players. These threads start during the initialization of the server and exist throughout its life. The next group of threads—new player threads—are related to players that have connected to the server, but are not yet playing a game. Each player creates two threads on the server. The first manages the network connection, and the second contains the game logic for joining and starting to play a game. The final group of threads—playing threads—contains threads involved in playing games of Domination.
DominationServer Management Threads The main thread that is created to run the DominationServer application leads a simple life. It starts the other management threads, then it drops into a loop providing a console interface to the server. Through this interface, you can query various aspects of the server’s state using the commands shown in Table 16.1. This feature allows you to check on the state of the server application, which is something you should do during the development stage of the game. Table 16.1DominationServer console commands.
Threads
Describes the threads running within the server
Players
Describes the status of the players connected to the server
Games
Describes the status of the games running within the server
Quit
Terminates the server
Help
Lists the console commands and their purposes
The ConnectManager thread accepts connect and reconnect requests from Domination applets. It uses a ServerSocket object to wait for the applet to initiate the connection. When the applet connects, the connection manager reads a GameEvent to determine if the applet is attempting to connect or reconnect. Connection requests will cause the connection manager to issue a new client ID to the applet and then create a DominationServerConnection to manage the connection. The DominationServerConnection will then create a ServerStartupLayer to welcome the player and guide her through the process of starting a game.
A reconnect request will cause the ClientManager to search for an existing DominationServerConnection with the correct client ID. Once found, the connection layer will “adopt” the new connection. If a DominationServerConnection with the correct client ID cannot be found, then the reconnect request fails, and the new connection is closed by the ConnectManager. The final management thread is the ServerMaintenance object. As the name implies, this object controls the server maintenance operation. Every 40 seconds, the ServerMaintenance object instructs the ClientManager to do its maintenance. This involves identifying players who have silently disconnected, and finding and eliminating orphan games. It is important to perform appropriate maintenance within the server because its life span is considerably longer than any of the client applets, which leaves it more susceptible to long-term resource leakage.
New Player Threads When an applet first connects to the DominationServer, the player must join an open game, then start playing. This process takes some negotiations with the server. To guide the negotiation, the applet has a ServerStartupLayer object running within the server, above the DominationSeverConnection object. The ServerStartupLayer object has encoded the logic for registering the player name, creating or joining a Domination game, and requesting that the game begin. Prior to playing a game, a Domination applet will always have two layers within the server: one layer dedicated to managing the network connection and the other to control the game start-up logic. The player may use the Create Game button to generate a new DominationGameLayer. The DominationGameLayer object can be considered the union of the game playing logic and a set of player information objects. Once the new DominationGameLayer is created, however, it does not start running. Instead, players are registered within the object and must indicate their desire to play the game by pressing the “play” button. When the last player uses the “play” button, the DominationGameLayer starts running, and the game starts to play.
Playing Threads When a set of players begin playing a game, the internal layout of the game layers changes dramatically, as illustrated in Figure 16.8. When all the players for a game are ready to play, the game DominationGameLayer is started. The DominationGameLayer’s first action is to “adopt” all the connections for the game players by replacing the ServerStartupLayers as the layer above the connection. The net effect of this is that a single DominationGameLayer object will see all the DominationEvents for all the players in the game.
Figure 16.8 DominationServer game layer configurations. After assuming the player connections, the DominationGameLayer object calculates the initial distribution of armies around the world, then it drops into the game loop. The game loop cycles over the players and instructs the active player/applet to distribute its new armies then perform any desired attacks. This loop continues until a player has won.
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
DOMINATION AT WORK We have seen all the components of both the Domination applet and DominationServer. Now, it is time to see how these entities interact to create a multiplayer game. As an example, we’ll focus on the interaction between the applet and server as the game begins to play. At this point in the game, the DominationGameLayer has been populated with the list of players connection layers, and the start() method has been called to begin the thread running. The flow of the game is clearly expressed by the code within Listing 16.4. At this level, the code follows a linear programming model. There is no evidence of GameEvent processing loops. All processing of the GameEvents is done within the methods that express the interaction during new army deployment (deployArmies()) and attacking (attackCountries()) modes. These two modes represent the only time when the player interacts with the game logic. Listing 16.4 DominationGameLayer run(), initGame(), and playGame(). public void run() { initPlay(); playGame(); } protected void initPlay() { DominationServer.gameStarting( this ); game.initGame(); // Transfer the connections from the ServerStartupLayers // to me, and create the PlayerState for each player. assumeConnections(); // Broadcast the player state. sendPlayerState(); // Initialize the armies with a random player deployment. itineraries(); } protected void playGame() { DominationEvent ge = (DominationEvent) Game.createGameEvent(); PlayerState winner = null; int turn = 0; while ( keepRunning ) { // Is there anyone left playing? if ( playerConnections.size() == 0 ) break;
// Has anyone won yet? if ( (winner=game.findWinner()) != null ) break; // Let someone have a turn. Vector playerStates = game.getPlayerStates(); PlayerState player = (PlayerState) playerStates.elementAt(turn%playerStates.size()); ge.setEvent( DominationEvent.S_PLAYER_TURN ); ge.setInt( 0, player.getClientId() ); writeToPlayers( ge, null ); // Player gets to deploy new armies one at a time. deployArmies( player ); // Player gets to attack. attackCountries( player ); // Next turn... turn++; } if ( winner!=null ) { ge.setEventType( DominationEvent.STATUS_MESSAGE ); ge.setString( 0, "We have a winner: " + winner.getName() ); writeToPlayers( ge, null ); } } The attack mode within the player turn represents the most complex interaction within the Domination game. This mode is executed by the server through a call to attackCountries(), which is shown in Listing 16.5. The attackCountries() method initiates the attack mode by sending a S_START_ATTACK_MODE event to the active player. When the player/applet receives this event, the ClientGameLayer layer calls onStartAttackMode(), which is shown in Listing 16.6. The onStartAttackMode() method codes a mode of operation for the Domination applet. At this point, both the server and applet are in methods that understand the event types that are unique to the attack phase of the game. Once the applet enters onStartAttackMode(), it drives the game. Clicks on the world map are received as C_COUNTRY_SELECT and C_SHIFT_COUNTRY_SELECT events. These events are used to specify the attacking and defending countries. In addition, keyboard events are used to specify the invasion size. As the user changes these parameters, the client first validates the action, updates its state variables, then sends a C_ATTACK_NOTIFY event to the server. This notification message is received by the event loop in attackCountries() and immediately broadcast to all the other players in the game. This provides feedback describing the active player’s actions to the non-active players. When the player signals the attack, the current selections are validated locally, then sent to the server within a C_ATTACK type event. The server calls onAttack() to execute the attack and notify all the players of its progress and outcome. When the attack is complete, the server will return to the event loop within attackCountries(). The interaction between the applet and server continues in this manner until the player signals the end of the attacks. At this point, the applet leaves the event loop sending a C_ATTACK_COMPLETE type event to the server. This message signals the server to exit its event loop. Listing 16.5 DominationGameLayer.attackCountries(). protected void attackCountries( PlayerState player ) { DominationEvent ge = new DominationEvent();
// Tell all players who is starting to attack. ge.setEvent( DominationEvent.S_START_ATTACK_MODE ); ge.setInt( 0, player.getClientId() ); writeToPlayer( ge, player.getClientId() ); // Process attack requests. boolean attacksComplete = false; while ( keepRunning && !attacksComplete ) { DominationEvent e = (DominationEvent) retrieveEvent( ); switch ( e.getEventType() ) { case DominationEvent.C_ATTACK_NOTIFY : // Attacker broadcasting attack data. onAttackNotify( e ); break; case DominationEvent.C_ATTACK : // Perform the attack. onAttack( e ); break; case DominationEvent.C_ATTACK_COMPLETE : // Attacker signals end of turn. attacksComplete = true; break; default : // Unknown message, apply default processing. processGameEvent( e ); } } ge.setEvent( DominationEvent.S_ATTACK_NOTIFY ); writeToPlayers( ge, null ); ge.setEvent( DominationEvent.STATUS_MESSAGE ); ge.setString( 0, "Player " + player.getName() + " concludes attacks.\n" ); writeToPlayers( ge, null ); }
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 16.6 ClientGameLayer. onStartAttackMode(). protected void onStartAttackMode( ) { // Tell the player what to do. Domination.setStatus( "Select source with click, destination with shift-click...\n"+ "1-9 to specify how many armies, A to Attack, and D when you are Done.\n" ); // State variables for attack mode. boolean attacksComplete = false; Country from = null; Country to = null; Country lastFrom = null; Country lastTo = null; int armies = 0; boolean resetArmies = true; while ( keepRunning && !attacksComplete ) { DominationEvent e = (DominationEvent) retrieveEvent( ); boolean message = false; switch ( e.getEventType() ) { case DominationEvent.C_COUNTRY_SELECT : from = onSelectFrom( e.getInt(0) ); to = null; message = true; break; case DominationEvent.C_COUNTRY_SHIFT_SELECT : to = onSelectTo( e.getInt(0), from ); message = true; break; case DominationEvent.C_KEY_PRESSED : { char key = Character.toUpperCase( (char) e.getInt(0) ); if ( key == 'D' ) { attacksComplete = true; } else if ( (key=='A') || (key==' ') || (key=='\n') ) { onAttach( from, to, armies ); resetArmies = true; } else if ( key == '\b' ) { if ( resetArmies ) armies = 0; armies /= 10;
resetArmies = false; message = true; } else if ( (key>='0') && (key<='9') ) if ( resetArmies ) armies = 0; armies = armies*10+(key-'0'); resetArmies = false; message = true; }
{
} break; default : // Allow my upper reaches to see // this game event, but bypass my implementation. super.processGameEvent( e ); } if ( message ) { String s = "From " + (from!=null?from.getName():"--") + " attack " + (to!=null?to.getName():"--") + " with " + armies + " armies\n"; Domination.setStatus( s ); } if ( (from!=lastFrom) || (to!=lastTo) ) { // Update my user interface. Domination.getGameCanvas().setAttack( from ); Domination.getGameCanvas().setDefend( to ); // Tell the rest of the world. DominationEvent selEvent = (DominationEvent) Game.createGameEvent(); selEvent.setEvent( DominationEvent.C_ATTACK_NOTIFY ); selEvent.setInt( 0, (from==null?0:from.getCountryId()) ); selEvent.setInt( 1, (to==null?0:to.getCountryId()) ); selEvent.setInt( 2, armies ); selEvent.setInt( 3, getClientId() ); writeEvent( selEvent ); lastFrom = from; lastTo = to; } } if ( attacksComplete ) { DominationEvent e = (DominationEvent) Game.createGameEvent(); e.setEvent( DominationEvent.C_ATTACK_COMPLETE ); writeEvent( e ); } } From the code examples in Listings 16.4, 16.5, and 16.6, the relationship between the applet and the server becomes clear. The server controls the game state and the game’s flow. While performing this duty, the server frequently delegates authority to the clients. These delegations typically involve player-intensive tasks.
SUMMARY Within this chapter, we have investigated the implementation of the Domination game. This game was created using applets and a server connected in a star connection topology. In the next chapter, we’ll extend the functionality within the Domination game and also enhance the tools within the GameLayer/GameEvent toolkit.
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 17 EXTENDING DOMINATION CHRIS STRANC
D
omination is a game of strategy. Like many strategic games, the timing of the individual moves is not critically important.
They occur when the current player has evaluated the situation and takes action. The other players will probably not notice an additional wait due to network latency. This aspect of the Domination game allows it to be dispersed across a wider network area than many continuous action games. The ability to play a game of Domination over long network connections does, however, pose several design issues that must be addressed. In this chapter, we’ll focus on some of these issues. To help you grasp these concepts more easily, I will group the issues into external and internal design considerations.
EXTERNAL GAME DESIGN The turn-based nature of Domination is not sensitive to the network latency of all the game connections. Left to itself, a Domination applet might well wait till the end of time for a response from the server application. Players, unfortunately, are not quite so patient. While they may be less sensitive to the game delays than the “action addicts” who play arcade games, they are human. And humans demand to be entertained. The user interface of Domination must cater to this need. In particular, the user interface should: • Provide feedback while the application is busy • Provide alternate means of entertainment To a game player, a non-responsive window means death. Death to the game. The game has a responsibility to indicate when it is busy. The more entertaining the feedback, the easier the delay will be accepted. What better example can we have than a class of applications whose primary function is spanning the entire Web to retrieve data. Take a look at you favorite browser. Whenever they touch the network they inform the user by animating the image in their top-right corner. Another approach to appease the waiting player is distraction. By providing alternate means of entertainment, we can keep our players happy while the game does what it must do. The life of the Domination applet is split into two stages. Initially, the player must create/join a game, only then can the game play begin. All too often, the first stage is overlooked and players lose interest while waiting for an opponent to start the game playing. To address this situation, we will allow players in the startup phase of the game to “watch” the progress of a game currently under way. Once a player is involved in a game, she may find the wait for her turn at world domination is unbearable. To soothe such “would be” dictators, we’ll provide the ability for inter-player chatting, emphasizing the human interaction between the opponents.
INTERNAL GAME DESIGN
The knowledge that Domination may be played over long network connections also forces us to look at some of our internal architecture. The length of the network connection has two immediate ramifications on our design: • The user-interface thread can never call a network method • The game must be able to recover from network errors It is all too easy to wire the network into the user interface. Pressing a button on the login dialog box will typically generate a DominationEvent and write it to the server. It seems like a harmless thing to do. Especially when you need the reply from the server to continue. This approach, however, can result in a user interface hangup that gives potential players a reason to search for fun elsewhere. If there is a network error, the initial send may invoke a lengthy reconnect and re-synchronization process. During this time, the user interface thread is blocked, which leaves the user interface totally unresponsive. Not only will it fail to respond to events like button clicks, but even fundamental operations such as repainting damaged areas of the screen will be left undone. The game may be progressing towards successfully restoring the connection, but when the user interface fails to respond, the player will probably choose to abandon the game. Dealing with network errors is also a complex issue. In the previous chapter, I described the foundation of the network error management within the GameLayer/GameEvent framework. In this chapter, we’ll take another step forward in dealing with this issue, and I’ll describe how the framework can guarantee that no events are lost between the client and server, even in the face of corruption and dropped network connections.
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 UNIVERSAL BUSY SIGNAL Browsers do it. Why not games? Domination thinks it’s time to use the universal busy signal. Having animated bitmaps in the upperright corner of your application may be the de facto standard for indicating network activity. Go with the flow, and tell your players when you hit the network. They will understand. They may even thank you for it. Our only dilemma lies in implementing the busy signal. Ideally, it should be a display element, not a piece of application-specific code. The code to provide animated feedback to the user is encapsulated within the StatusCanvas class. The StatusCanvas class provides methods for specifying foreground and background images for the canvas area. You use the two different image types to render different classes of data. The background image is useful for showing the mode of the application. A hand moves across the world during the deployment phase, and large strategic arrows cross the map while the player selects attacking and defending countries. The foreground image is used to indicate transient activities. Network reads and writes will cause little stars to fall from the sky. Figure 17.1 shows how the StatusCanvas image might be built during a period of network activity.
Figure 17.1 The StatusCanvas and its component images. When the status canvas is being painted, the appropriate frame from the background image is rendered, followed by a frame from the foreground image. The foreground image is normally a transparent GIF, which has the effect of superimposing the foreground image over the background image. To prevent flickering, both images are rendered to a buffer, then the buffer is copied directly to the screen. The StatusCanvas class methods are listed in Table 17.1. Table 17.1Summary of StatusCanvas Class methods.
Method
Description
StatusCanvas( Applet a, String defaultImageFile )
Constructor locates the applet and specifies the default image; this image is usually the game logo
void start()
Starts the animation
static void stop()
Stops the animation
static void setBkImage( String filename )
Sets the background image
static void setFgImage( String filename, long duration )
Sets the foreground image
As a convenience, many of the StatusCanvas methods are static, which allows you to access them without passing the StatusCanvas object everywhere. The setBkImage() and setFgImage() methods are used to specify the foreground and background images (which are specified by their file name). As images are loaded, they are cached for later use. Both the setBkImage() and setFgImage() methods accept a null file name. In the case of setBkImage(), a null file name will switch the background image to the default image specified in the constructor. Passing null to setFgImage() will disable the foreground image. The setFgImage() method also accepts a duration argument, which allows the game to request that the image is only displayed for a specific period. The primary reason for developing the StatusCanvas class was to provide feedback to the user when we are communicating to the
network. The link between the canvas and the network activity is enabled by a Notify object deep within the ConnectionLayer class. External objects connect to the ConnectionLayer’s Notify object so that they can receive a flow of notifications describing the activity of the connection. Each activity within the connection is characterized by a unique integer value, as listed in Table 17.2. Table 17.2Summary of ConnectionLayer notification codes.
Activity Identifier
Description
CL_EVENT_READ
An event has been read from the network
CL_EVENT_WRITE_START
An event is being sent to the network
CL_EVENT_WRITE_END
An event has been sent to the network
CL_NETWORK_ERROR
An error has occurred while reading or writing to the network
CL_CONNECTING
The connection is connecting or reconnecting to the server
CL_CONNECT_OK
The connection was successful
CL_CONNECT_FAIL
The connection failed
The Domination applet forms the bridge between the ConnectionLayer and the StatusCanvas. After initializing the network layers, the applet registers itself for ConnectionLayer notifications. The ConnectionLayer then calls Domination.onNotify( int notifyType, Object obj ) to signal network activity. The onNotify() method is shown in Listing 17.1. It examines the notifyType argument and signals the status canvas to display the appropriate images. When we examine the reaction of onNotify() to different notification types, the delineation between foreground and background image use is clear. Notifications describing the state of the network connection result in setting the background image. The passage of events though the connection layer all trigger foreground images. The CL_EVENT_WRITE_START and CL_EVENT_WRITE_END notifications are used to indicate the streaming of a single GameEvent to the network. The start message will cause the network.gif animation to be played. It will continue until an end message is detected and setFgImage( ) is called with a null file name argument.
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
Unlike the start and end notifications for writing GameEvents, the CL_EVENT_READ and CL_NETWORK_ERROR notifications do not arrive in pairs. Instead, the call to setFgImage() is passed a duration for the animation. In the case of a GameEvent read, the call setFgImage( "images/network.gif", 250 ) will display the network.gif image for a period of 250 milliseconds, then automatically remove it. Listing 17.1 The Domination.onNotify() method. public void onNotify( int notifyType, Object obj ) { switch ( notifyType ) { case ConnectionLayer.CL_EVENT_READ : StatusCanvas.setFgImage( "images/network.gif", 250 ); break; case ConnectionLayer.CL_EVENT_WRITE_START : StatusCanvas.setFgImage( "images/network.gif", 0 ); break; case ConnectionLayer.CL_EVENT_WRITE_END : StatusCanvas.setFgImage( null, 0 ); break; case ConnectionLayer.CL_NETWORK_ERROR : StatusCanvas.setFgImage( "images/network_error.gif", 2000 ); break; case ConnectionLayer.CL_CONNECTING : StatusCanvas.setBkImage( "images/connecting.gif" ); break; case ConnectionLayer.CL_CONNECT_OK : StatusCanvas.setBkImage( "images/logo.gif" ); break; case ConnectionLayer.CL_CONNECT_FAIL : StatusCanvas.setBkImage( "images/connect_fail.gif" ); break; } } The StatusCanvas class is a useful class in any application that can operate in distinct modes. Its simple interface and self-contained architecture make it easy to integrate. Its use as a tool to indicate network activity becomes especially critical in games where long network connections are common.
GAME WATCHING You endured the download. You are primed to play the newest multiplayer super-networked applet. You enter your player name, and your world comes grinding to a halt. What does it mean, “Please wait for an opponent.”? What could be more frustrating than waiting for another player before a game can begin. You have no idea if someone will arrive in the next minute, hour, or day! All the while, you are looking at your phone line and thinking of the monthly surcharge from your ISP.
Presenting the players with a solid wall right after they start your applet is probably not a great tactical move. But how can you alleviate some of the anguish of waiting? The answer depends on the game. For Domination, we’ll let our player look over the wall and see the fun that lies beyond. Once a Domination game starts normal play, it cannot accept new players. But we have modified it to allow people waiting at the Select Game dialog box to watch an existing game. As a game watcher, they see all the feedback provided to the normal players. They even get to spy on all the chat messages passing between the players. Figure 17.2 shows Domination in game-watching mode and the modified SelectGameDialog. The player can toggle the dialog box between two different sizes by clicking on the Watch button. When the dialog box is expanded, it will allow the player to select from the list of running games and watch the game action. When the player registers to watch a game, the DominationServer downloads the game’s player list and the status of the countries within the world. The server then adds the watching player to a list of spectators within the DominationGameLayer object. While a player is on this list, they will receive copies of all the messages that are broadcast to the actual players of the game.
Figure 17.2 Watching a Domination game while waiting for your own. The end result is a carbon copy of the game being played. The only difference between this and normal play is the watcher will never get a turn. That thought brings us full circle. Right back to waiting at the Select Game dialog box. When the player opted to watch a game, this dialog box did not go away. It is still patiently watchful for that elusive opponent. When an opponent arrives and initiates game play, the watched game will be overwritten and the new game will start. If a player does not like the game they are watching, they can move to another by selecting it from the list and clicking on the Watch button. The majority of the work in maintaining a game watcher falls to two methods within the DominationGameLayer class. The addWatcher() method is responsible for registering the new game watcher and downloading the player and country data. This is all the initialization required for the applet to render the details of the game state. The second method, writeToPlayers(), writes the GameEvents to a set of players, as shown in Listing 17.2. Listing 17.2 The DominationGameLayer.writeToPlayers() method. public final void writeToPlayers( DominationEvent ge, Vector playerVector ) { if ( playerVector==null ) // Default to the normal player list. playerVector = playerConnections; Enumeration ee = playerVector.elements(); while ( ee.hasMoreElements() ) { ConnectionLayer c = (ConnectionLayer) ee.nextElement(); c.writeEvent( ge ); } // If the message was meant for all players, echo it to the watchers. if ( playerVector==playerConnections ) writeToPlayers( ge, playerWatches );
} The DominationGameLayer maintains three vectors of players associated with the game. The playerConnections vector holds all the players playing the game. The playerUpdates vector holds the set of players that are “watching” the games status from the SelectGameDialog. Finally, we come to the playerWatches Vector, which contains the list of players who are watching the game during its normal course of play. writeToPlayers() is written to accept the player list as an argument because different messages are sent to different groups of players. The last two lines in writeToPlayers() assures that any message directed to the set of normal players will be echoed to the game watchers. People never like waiting. At least we give them an opportunity see Domination in action as they wait to engage in a battle of their own.
INTER-PLAYER CHAT As I’ve indicated before, Domination is a turn-based game; at any time, only one of the players is actively deploying armies or attacking. This means that all the other players have to sit back and watch the game progress until their turn arrives. When you’re playing with a bunch of top-notch strategists who contemplate their every move to the nth degree, not to mention any possible network delays, the wait can become intolerable. Time, once again, to provide that entertainment.
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
While playing a game of Domination, the players can use the Chat button to activate the Chat dialog box. This dialog box allows a player to type a line of text and send it to one or more of their opponents. Figure 17.3 shows the Chat dialog box in action.
Figure 17.3 The Chat dialog box helps players to pass the time (and maybe even form alliances). When a player accepts a line of text by pressing Enter, the game will generate a DominationEvent and send it to the server. In this case, the event will be a C_CHAT_MESSAGE event. The game will store the line of text as the String at index 0 and the client IDs for all the selected recipients in the integer array. When the server receives the C_CHAT_MESSAGE, it scans the list of recipient client IDs. For each recipient it creates and sends an S_CHAT_MESSAGE. This logic is held within the DominationGameLayer.onChatText() method. No information is needed beyond the set of client IDs embedded in the original C_CHAT_MESSAGE. As a result, the entire server-side support for chatting takes a little over 10 lines of code! The server-generated S_CHAT_MESSAGE is received by each of the recipients and passed to the Chat dialog box. The dialog box examines its state and will automatically show itself with the message displayed. If the recipient is not involved in a previous conversation, the originator of the message, and other recipients, will be selected for a reply. The result of these events is a communication channel among the players. The majority of the work was simple AWT code to manage the dialog box itself. All the networking support was accomplished by defining two new GameEvent types and a dozen lines of code within the server. For this cost, we have provided the players with another means of sharing the gaming experience. Previously, they had no alternative to sitting back and waiting until their turn. Now, they can form alliances with other players, discuss strategy, or even just pass the time of day by chatting about the weather on the other side of the continent. Let’s remember, networks bring people together, not games.
DECOUPLING THE USER INTERFACE AND NETWORK The GameLayer/GameEvent framework does an excellent job of decoupling the user interface from the reading of network events. A single thread is responsible for blocking on input from the network. When the input arrives, it is introduced to the stack of GameLayers. The GameEvent may be passed from layer to layer up the stack until it is processed, and possibly could cause an update to the user interface. Let’s take some time to examine the process of writing events to a network connection. Many of the AWT event handlers with Domination gather some pertinent information, package it as a DominationEvent, and send the event to the server for processing. The send to the server is accomplished with a call to GameLayer.writeEvent(). The call to writeEvent() eventually has the DominationEvent object stream itself down the connection. This method of processing user interface events is simple to understand, but it runs the risk of blocking the user interface thread on a network IO operation. Generally speaking, this is a poor design because it allows the user interface to become non-responsive. The server side of Domination also suffers from a GameEvent writing malady. Frequently, the server must broadcast many events to all the players of a game. Given the current situation, the server’s game layer/thread will sequentially write the data to each applet. A
far more efficient solution to writing GameEvents involves moving the burden of pushing bytes down the wire away from the thread that calls GameLayer.writeEvent() to separate threads. This approach will allow the game logic thread to return to its duties earlier, which results in getting the information to the client faster. Figure 17.4 demonstrates a comparison of the direct and decoupled GameEvent writing within the DominationServer application.
Figure 17.4 Direct and decoupled writing within the DominationServer. Ideally, we would like to provide the decoupled writing of GameEvents without introducing more threads into the application. The ConnectionLayer object is a logical choice for this new functionality. Because it is a GameLayer, the ConnectionLayer object is derived from Thread, which means it has its own execution context. Conceptually, the ConnectionLayer class is the interface to the connection, and so it is the correct architectural element to manage writing GameEvents. All calls to GameLayer.writeEvent() are already routed through the ConnectionLayer.writeEventConnection() method so it is also the most convenient location. All we need to do is modify the behavior of the writeEventConnection() 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
The writeEventConnection() method is used to tell the GameEvent to stream its contents to the DataOutputStream associated with the connection socket. The new version will take the GameEvent and store it in a writeEventQueue queue, then notify the ConnectionLayer thread object. When the ConnectionLayer object is notified, it will retrieve the queued GameEvent and send it on its way. This new behavior is captured in modified version of the methods shown in Listing 17.3. Listing 17.3 Implementation of decoupled GameEvent writing. protected synchronized boolean writeEventConnection( GameEvent e ) { // While we have not actually written the event to the connection one // is well on the way. lastWriteEventTime = System.currentTimeMillis(); writeEventQueue.append( e ); notify(); // Tell the ConnectionLayer thread it has something to // process. return true; } public void runReadWrite() { while ( keepRunning ) { GameEvent e = retrieveEvent( ); // Retrieve a pending GameEvent. if ( e==null ) { // Check if we can process an out bound message. if ( writeEventQueue.size()>0 ) 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.registeredId<=regIdRecvStream) ) return; // Remove any acknowledged messages. removeCached( rged.idRecvStream );
// Is this the next registered event? if ( (rged.registeredId==regIdRecvStream+1) ) regIdRecvStream = rged.registeredId; // Are we missing any registered messages? if ( rged.idSendStream>regIdRecvStream ) { // 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
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
A BRIEF NETWORK REVIEW At this point it might be helpful to review the current situation within the MazeWars game. The network connectivity has transitioned from its initial star topology with the server to an interconnect topology among the game sessions, as shown in Figure 18.4.
Figure 18.4 MazeWars application connectivity. Unlike Domination, the MazeWars connectivity changes as sessions join and leave a shared game. When the game is first created the only connection is between the original session and the server. This is connection 1 in Figure 18.4. When another session starts, it links to the server as connection 2. When the session joins the game, it creates connection 3 to the original session. The third session follows a similar pattern, creating connection 4 to the server to drive the Welcome dialog box, then creating connections 5 and 6 to communicate with the other sessions in the shared game. If the original session was to close, all connections to it (1, 3, and 5) would be shut down. The remaining participants in the game would continue to play as usual. It is this dynamic connection nature that allows a networked MazeWars game to exist far beyond the life span of any single session.
SENDING THE ACTOR DATA We have seen how MazeWars constructs the set of connections used to transfer information among the various sessions in a game. It is now time to focus on the content that passes through these conduits. The transmission of actor information across the network is triggered by the actions of the BroadcastThread object. This object was created via a call to initBroadcastThread(). The run() method for this class calls sendMazeActorData() then sleeps until BROADCAST_DELAY (300) milliseconds has elapsed. This process is repeated until the end of the game. The sendMazeActorData() method, shown in Listing 18.3, controls the dynamic state transfer mechanism. Listing 18.3 The BroadcastThread.sendMazeActorData() method. public final void sendMazeActorData() { if ( ConnectionLayer.getConnectionLayerCount() < 2 ) return; // Don't bother if we are not connected to anyone... MazeWarsEvent e = new MazeWarsEvent( MazeWarsEvent.CC_MAZE_ACTOR_STATE ); fillMASEvent( e );
writeEvent( e ); } The first check, performed by sendMazeActorData(), tests to see if there are external sessions connected. The method checks the number of ConnectionLayer objects registered within the application. A single connection represents the connection to the MazeWarsServer application; all other connections represent interfaces to external sessions. The process of writing the actors state to the other sessions is accomplished by generating a CC_MAZE_ACTOR_STATE event, filling it with the actors state information, then writing it to all the external sessions. The method fillMASEvent(), shown in Listing 18.4, turns out to be the heart of the actor state transfer logic. Listing 18.4 The BroadcastThread.fillMASEvent() method. private final synchronized void fillMASEvent( MazeWarsEvent e ) { Hashtable remoteClassesSeen = new Hashtable( 30 ); Enumeration ee = gameState.getMazeActorElements(); int intCount = 0; int stringCount = 0; e.setInt(intCount++, MazeWars.getSessionId() ); while ( ee.hasMoreElements() ) { // Scan the list of maze actors. MazeActor actor = (MazeActor) ee.nextElement(); if ( actor.isNetworkBound() ) { // Do we send this one across? String className = actor.getClass().getName(); if ( !remoteClassesSeen.containsKey(className) ) sendActorClass(className, actor ); // Add the actor to the data within the event... Cell c = actor.getCell(); e.setInt(intCount++, actor.getActorId() ); e.setInt(intCount++, c.row); e.setInt(intCount++, c.col); e.setString(stringCount++, className); e.setString(stringCount++,actor.getName() ); } } } In principal, the fillMASEvent() method is responsible for traversing the list of actors within the game and concatenating their dynamic information to the end of the CC_MAZE_ACTOR_STATE event. This logic is prominent within the listing. However, there are two significant digressions. The first is the test: if ( actor.isNetworkBound() ) isNetworkBound() is a new abstract method added to the MazeActor class. It allows us to determine if the actor should be transferred to external sessions. Most MazeActor classes are destined to travel the network. We expect classes like the ConsoleActor, Shark, and Hunter to be transferred to other sessions; however, some of the other classes are less obvious. The Bullet, Bomb, and Flame classes are AutoActors used to represent the killing effects of the Gun, BombLauncher, and FlameThrower weapon classes. When a FlameThrower is fired, it creates a set of Flame actors to show the progress of the flame through the maze. The Flame class is network bound, so the flames are automatically transferred to the external sessions and rendered. This duplicates the progress of the flame blast on the remote maze with no additional programming effort. An example of a class that is not transferred across the network is the Mystery class. You may recall from Chapter 14 that Mystery objects are placed into the maze to provide a supply of Weapons for the ConsoleActor/game player. The Mystery objects are not capable of transferring Weapons to actors that originate in external sessions, so to prevent confusion, they are not duplicated within the external sessions.
The other digression within the fillMASEvent() method is indicated in the following lines: if ( !remoteClassesSeen. containsKey( className ) ) sendActorClass( className, actor )
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
To understand their purpose, we must investigate the nature of the information we are sending across the network. The CC_MAZE_ACTOR_STATE event is populated with the actor ID, the location within the maze, a class name, and the actor’s name. With this information, the external sessions must be able to both display the actor and allow the local actors to interact with it. Both of these activities need additional information. To draw a foreign actor, the session must know what image file to render. To allow the local MazeActors to react to the foreign actors correctly, the MazeActor.isKillable() property must be known. This property is key to the operation of the getSightingDistance() and getSightingActor() methods. You may recall that these methods allow an actor to look around the maze for a killable actor. If we were to arbitrarily assign all foreign actors as killable, then local hunters would feel compelled to attack the flames from a flame thrower. Making foreign actors non-killable would have an equally futile effect. They would be totally ignored by all the local actors. It would be a “You’re here, but we don’t want to play with you” situation. So it becomes clear, we need some additional information to describe the “type” of the actor to external sessions. There are two ways of realizing this “type” information. First, we could use the class name that was sent with the actor information to create a local actor of the correct class. This local actor would then be used as a surrogate for the foreign actor. However, this approach has some drawbacks. The game logic must now treat objects of the same class differently based on whether they are local actors or foreign surrogates. MazeWars also supports a dynamic loading facility, which allows the player to load a custom AutoActor into their game. But the chances of all the external sessions having access to the same custom actor’s CLASS file are slim. The custom actor was created by a Java programmer who aspired to MazeWars’ greatness by using their programming talents to create the new and lethal AutoActor class. As such, the CLASS file probably only exists on the programmer’s local computer. MazeWars solves the problem of the additional class-specific information by sending a class template to describe the actor. The class template contains the required class information, which allows the external session to render and interact correctly with the foreign actor. The call to sendActorClass() is responsible for ensuring all the external sessions have been sent the template information for the specified class. The logic within fillMASEvent() and sendActorClass() ensures that class templates are only sent once per class to each external session. The class information is sent within a CC_MAZE_ACTOR_CLASS event. Now you know how the information describing the local actors within a MazeWars session is packaged up as neat little bundles of information and sent to the external sessions. Next, we’ll look at how this information is received.
INTEGRATING EXTERNAL GAME INFORMATION A MazeWars session will receive a continuous stream of MazeWarsEvents from the other sessions within a shared game. All these events are read from the network and passed to the appropriate ConnectionLayer object. All the connection layers will pass these events up to a single MazeWarsGameLayer object. It is this object’s duty to integrate all the external game information into the local MazeWars game. It performs this with the help of the RemoteActor class. All foreign actors are represented within the local game by instances of the RemoteActor class. These actors are initialized with the class template information and position data for a foreign actor. They use the class template information to load and display the correct actor image and return the correct isKillable() value. The RemoteActors are processed by the game logic in the same manner as any local actor. The MazeWarsGameState class has no special understanding of this class. The logic of adding, killing, and removing actors remains unchanged. RemoteActors live and die at the whim of the MazeWarsGameLayer, particularly the onMazeActorState() method, which is shown in Listing 18.5. This method is called to process the CC_MAZE_ACTOR_STATE event that is generated by the BroadcastThread object in foreign sessions.
Listing 18.5 The MazeWarsGameLayer.onMazeActorState() method. protected synchronized void onMazeActorState( MazeWarsEvent e ) { // Create an array of the RemoteActors for working with. ConnectionLayer cl = null; Enumeration en = RemoteActor.getRemoteActorElements(); int remoteCount = RemoteActor.getRemoteActorCount(); RemoteActor[] remotes = new RemoteActor[ remoteCount ]; for( int i=0 ; en.hasMoreElements() ; i++ ) remotes[i] = (RemoteActor) en.nextElement(); // Process the actors that have arrived. int foreignCount = (e.getIntCount()-1) / 3; int sessionId = e.getInt(0); Cell c = new Cell(); for ( int i=0 ; i
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 onMazeActorState() method performs three distinct operations: It creates new RemoteActors, updates the position of existing actors, and removes dead actors. The method starts this process by generating an array of all the RemoteActors within the game. This array will be used to track which actors are moved and removed. The inbound CC_MAZE_ACTOR_STATE event contains information describing all of the local actors in the sending session. The first logic loop processes each of these actors sequentially. It scans the array of RemoteActors trying to find a match based on the session and actor IDs. If a match is found, the RemoteActor has its position updated and its entry in the array nulled. If no match is found, the foreign actor must be new to this session, so a new RemoteActor is created, initialized, and introduced to the game. After processing all the actor information within the inbound event, onMazeActorState() turns its attention back to the RemoteActor array. It scans the array looking for any remaining remote actors from the external session. Because any remote actors that existed in the external session should have been nulled in the array, all remaining actors must have been killed in their home session. As these unfortunate creatures are located, they are removed from the local game. Given this information, we now know how foreign actors move around the maze. Now we can get to the good part—killing these remote maze forms.
KILLING IN A NETWORKED MAZE The process of killing has not changed from the standalone version of MazeWars. The actors roam around killing each other with calls to ActorGameBridge.killRange() or by firing Weapons, which use the WeaponGameBridge.killActors() method. The game logic remains blissfully unaware that the RemoteActors are just proxies for actors in a different session. So, what makes it all work? An actor is notified of its demise by a call to MazeActor.onKilled(). Normally, most actors just roll over and play dead by calling ActorGameBridge.removeMazeActor(). The RemoteActor, does not accept its fate so easily. Listing 18.6 shows the death throws of a RemoteActor. Listing 18.6 Ending the existence of a RemoteActor. public void onKilled( String killString ) // Disappear from the maze. gameState.getCanvas().removeMCI( MazeWarsCanvas.CL_ACTORS, getCell(), getMazeCellImage() );
{
// Tell the remote's home base it has been killed... MazeWarsEvent mwe = new MazeWarsEvent( MazeWarsEvent.CC_MAZE_ACTOR_KILLED ); mwe.setInt(0, homeSessionId ); mwe.setInt(1, homeActorId ); mwe.setString( 0, killString ); connectionLayer.writeEvent( mwe ); }
When the RemoteActor is killed, its first action is to disappear from the maze. At this stage, the actor is not quite dead, it’s in a deep coma. The game playing logic has marked the actor as dead, and it lies in limbo. This limbo is resolved when the actor calls either removeMazeActor() to get buried or reviveMazeActor() to get resurrected. To determine its fate, the RemoteActor sends a CC_MAZE_ACTOR_KILLED event back to the session where the real actor resides. When the CC_MAZE_ACTOR_KILLED event arrives at the actor’s home session, the MazeWarsGameLayer object locates the real actor and kills it. It then checks if the actor decided to revive itself. If it did, a CC_MAZE_ ACTOR_ALIVE event is sent back to revive the RemoteActor as well. If the real actor actually died then no special process is needed. The next time a CC_MAZE_ACTOR_STATE message is sent, the dead actor will be missing, which signals the removal of its proxy. Why all this work just to kill a remote actor? You might be tempted to send the real actor a death event, then simply remove the remote actor. If the real actor is revived, the next CC_MAZE_ACTOR_STATE event will regenerate it. The problem with this approach lies in the network. There will be a delay between the time the death message is sent and received. During this time, a CC_MAZE_ACTOR_STATE may arrive at the session where the killing occurred. This event would not reflect the demise of the actor. Instead, the actor would re-appear and start to wander away from the killing zone, only to be struck dead a few moments later! When I shoot things, I like their complete and immediate death. One final note on RemoteActors and the killing process: RemoteActors do not kill anything; the calls to killing methods always occur in the session where the real actor exists.
IS IT LIVE OR IS IT A REMOTEACTOR? You can now see how we integrate foreign actors into the MazeWars game. As you play MazeWars, you may notice that the remote actors are not as well behaved as the local ones. They move with jumpy motions and seem to have an advantage in killing you. The jumpy motion is due to the delay times in the BroadcastThread and network transmission. While actors can move a single cell per TICK_TIME (75 milliseconds), the delay time between network broadcasts is normally BROADCAST_DELAY (300) milliseconds. This means an actor may have moved up to four cells between broadcasts. The simple solution to this dilemma is to reduce the delay between broadcasts. The closer the broadcast delay is to the clock’s tick time, the smoother the remote actor motions will be. Unfortunately, this will force all the sessions to process more network data. We are left with a trade-off between smooth motion and CPU cycles. MazeWars allows the player to modify the broadcast delay time with the Page Up (faster response), Page Down (slower response), and Home (default setting) keys. The change in broadcast delay is mirrored by all the sessions playing the game. This allows you to experience the trade-off first hand. The Bouncer class of actor provides the perfect specimen for examining the effect of the broadcast delay. The bouncers move at a constant speed of one cell per tick. They also travel in straight lines, changing directions only when they hit a wall. If you play two MazeWars sessions and watch a set of foreign Bouncers move around the maze, you can see how their motion is smoothed by reducing the broadcast delay, but you can also see the CPU usage crawl higher and higher. As you continue to decrease the delay, there comes a point when the CPU is almost 100 percent utilized. Then, something goes wrong. The actors move cell by cell, but they tend to surge over the face of the maze. If this isn’t bad enough, the sessions becomes de-synchronized.
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 games no longer have enough time to write and process the CC_MAZE_ACTOR_STATE events. Instead, the reading and writing queues within the ConnectionLayer start buffering these messages, processing them in bulk. This is what causes the actors to surge across the screen. It is a good indication that the delay rate is too short. The second distinguishing characteristic of remote actors is their uncanny ability to kill you. If you stand still and wait for the approach of a remote actor they will invariably outwit you. The reason for this lies in the latency between the games and the advantage it bestows on the moving actor. The effect of the latency between sessions is best viewed in the context of an example. Figure 18.5 shows two sessions in the same shared game. The ConsoleActor, at the left of each image, has decided to ambush a Hunter. The top row of images shows the ConsoleActor’s session. The bottom shows the session that hosts the Hunter actor.
Figure 18.5 The six ticks to doomsday. The two sessions experience a two “tick” latency. That is, it takes two ticks for motions in one session to be reflected in the other. At tick one, the ConsoleActor has just arrived at his chosen ambush point. The player may even relax because, according his view, the Hunter is still four cells away. The picture is quite different within the Hunter’s session; he is two cells closer to the ambush position than the ConsoleActor thinks. In addition, the ConsoleActor is still traveling toward the wall. In the top session, the ConsoleActor patiently waits through ticks two to five. At tick five, the Hunter enters the ambush zone and the ConsoleActor fires. In theory, the ConsoleActor now has one tick to move from the path of the Hunter’s bullet. The bottom session paints a totally different picture of the ambush. In this session, it is the ConsoleActor whose motions are delayed by two ticks. By tick three, the ConsoleActor’s position is accurately reflected in the Hunter’s session. Tick three also marks the arrival of the Hunter at the ambush zone. There is a clear line of vision between the two actors, and so, in tick four, the Hunter shoots at the ConsoleActor. Tick five marks the progress of the Hunter’s bullet toward killing the ConsoleActor. Then finally, we get to tick six. At this point the Hunter’s bullet kills the ConsoleActor. The Hunter’s home session will generate a CC_MAZE_ACTOR_KILLED event to inform the ConsoleActor. This event may take a little time to reach the ConsoleActor, but it is the tag of death. The ambush did not go as planned. There is a little consolation that the ConsoleActor’s bullet will also find its mark and kill the Hunter. Still, the element of surprise was meant to work better than this! If you spend some time, you can learn to play a game using latency to your benefit. Still, in a perfect world, we should not have to think so hard while playing a game. Given this example, it is easy to see how latency can introduce complication and confusion in the management of multiplayer games. The existence of latency allows the multiple sessions to have de-synchronized views of the overall game.
SUMMARY In this chapter, we have taken the standalone version of MazeWars and integrated it with a network communication layer and a game server. The result is a new game that allows actors from many sessions to run around and wreak havoc on each other. We discovered how a game can “mix and match” connection topologies to get the features it needs. Part of this discovery was the simplicity of implementing an interconnect topology by embedding a ConnectManager in the application. Probably the most amazing aspect of the development was how little change was required to the original game logic to support foreign actors. The use of RemoteActors to act as proxies for actors enabled the game logic to remain intact, despite a radical enhancement to the game. Finally, we examined how RemoteActors moved about the maze and killed their opponents. This examination led us to the discussion of the multiplayer evils: latency and de-synchronization. In the next chapter, we’ll return to these issues to see how we can improve the situation.
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 19 NETWORK MAZEWARS REFINED CHRIS STRANC
I
n Chapter 18, we worked to bring MazeWars to the networked world. We focused on such fundamental problems as implementing
efficient connectivity and integrating information from external sessions into the local game logic. As the chapter drew to a close, I described two bad habits our maze friends developed in their travel across the Net: non-smooth motion and de-synchronizated actions. In this chapter, we are going back to the maze to correct those misbehaviors.
LIFE IN THE NETWORK LANE The problems with remote actor behavior is a direct result of the delays in the transfer of game state information between sessions. I’d like to take some time out to develop a better understanding of where these delays come from and how we can address them. The delays combine to become the time elapse that occurs between the time the actor in one session moves until the time the actor’s image in a foreign session is updated. This time can be broken down into various “buckets.” The first bucket is the period that elapses between the actor’s movement and the time that information is transferred to the network. The second bucket is a big long delay, called network travel time. The delay in transferring actor information to the network has a quantizing effect on the player information. If the actor moves every 75 milliseconds and the actor information is broadcast every 150 milliseconds, then the actor’s movements appear in two-cell bursts. Figure 19.1 demonstrates this effect. The figure shows an actor moving from left to right, in successive ticks. The left animation represents the actual actor motion in its own session. The middle animation shows the effect of transferring the actor position every two ticks.
Figure 19.1 Quantized actor motion. The delay inherent in the network transfer offsets the actor’s state information. In Figure 19.1, the right animation shows the results on the actor’s image after a two-tick network delay is imposed. Comparing the left and right animations provides a graphic example of the de-synchronized game states between two sessions. It is this situation that we’ll try to correct. Besides a delay, the network can cause other changes in the flow of data between sessions. The formatting and writing of an actor state event usually occurs at a consistent interval. By the time the information is processed by the other session, the events may arrive together, with extended idle time in between.
SMOOTHING THE FLOW
You may recall that the BroadcastThread.fillMASEvent() method is responsible for creating a summary of the actors within a session. This summary is packaged within a MazeWarsEvent. This event is then sent to each of the external sessions. The process of gathering the information is represented by the shaded bars in the center animation of Figure 19.1 (ticks 2 and 4, and 6 and 8). There is no actor information transmitted in non-shaded ticks, which results in the actor moving in jumping motions in the middle and right animations. We have many options to correcting this situation. The first path we’ll explore is making our RemoteActors a little proactive. We shall use Newton’s first law of MazeWars: “An actor in motion will stay in motion.” LOCALLY ANIMATING REMOTE ACTORS Given values for the direction and speed of an actor, we should be able to extrapolate its motion in an external session. By performing this extrapolation, we will be able to animate the RemoteActors. This technique will allow us to maintain remote actor motion in the time between CC_MAZE_ACTOR_STATE events. To accomplish this, we need to retrieve the actor’s speed information and provide means of animating the actors. The speed information is gathered by the BroadcastThread.fillMASEvent() method and appended to the actor information in the CC_MAZE_ACTOR_STATE event. To provide a means of animating the actor, we turn to our old friend the AutoActor class. By deriving RemoteActor from the AutoActor class, we can assure that our remote actors have an onTick() method called once every system clock tick. In conjunction with the onTick() method, we use the AutoActor speed control methods to perform the remote actor animation. When an actor state event arrives, we set the direction and speed of the remote actor to the supplied values. Within the RemoteActor.onTick() method, all we need to do is call the moveAtSpeed() method to move the remote actor at the same speed and direction as its master. Listing 19.1 shows the RemoteActor.updateLocDirSpeed() method. As you might expect, the method updates the direction and speed of the RemoteActor. The logic that governs whether the actor’s position will be updated is dependent on the remote actor’s current cell, the previous cell, and the new cell. There are three alternatives: • If the new cell is the same as the current cell, no action is taken. • If the new cell is not the previous cell, then the actor is moved. • If the new cell is the same as the previous cell, then the decision to move the actor is based on the time the actor has been out of sync. After two passes with the new cell pointing at the remote actor’s previous cell, the remote actor’s position is updated. The logic governing the acceptable location for the remote actor is critical to both the actor’s smooth motion and the accuracy of its position. If we are too restrictive and force the actor to always move to the network-specified cell, we would experience actors that would occasionally move one cell forward, then back, then forward again. This frantic motion is the result of the local animation logic conflicting with the network position data. We place a limit on the duration a remote actor can remain out of step with the network data to ensure the long term (greater than two tick) accuracy. A remote actor that is moving at a slow speed and then stops, might well stop one cell beyond its master actor; it’s being proactive you know. Because the master actor has stopped one cell behind, it is actually stationary in the remote actor’s previous cell location. In this situation, the timing mentioned in alternative three will force the remote actor to move back to the right 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
Listing 19.1 The RemoteActor.updateLocDirSpeed() method. public final void updateLocDirSpeed( Cell c, int dir, int speed ) { Cell currentCell = getCell(); int oldDir = getDirection(); setDirection( dir ); setSpeed( speed ); if ( !c.equals( currentCell ) ) { boolean moveActor = false;
// The current cell is wrong.
if ( c.equals( getAGB().getPrevCell() ) ) { if ( goneTooFar<3 ) { // Let us stay here, the master actor // may catch up. goneTooFar++; } else { // Time to move back. moveActor = true; } } else { // Not previous cell, so move. moveActor = true; } if ( moveActor ) { lastMoveTick = tickCounter; gameState.moveMazeActor( this, c ); goneTooFar = 0; } } } Performing local animation with loose coupling to the master actors position provides a level of desensitization to the frequency of actor state events and irregularities in the inter-session transmission time. In effect, we are allowing the remote actor a two-cell window for its valid position. So what have we accomplished with this additional complexity? Figure 19.2 shows the new motion of the actor. The jumping motions have been eliminated. Because the actors are using the normal moveAtSpeed() method, if a RemoteActor walks into a wall, it will be stopped by the game logic. The remote actor would then wait patiently for the next event from the network to indicate its new course and speed. Achieving smooth motion is critical to hiding the networked nature of remote actors from the game player. Of course, it is possible for the new RemoteActors to get surprised. If the master actor decides to change direction, then any ticks that pass before the new actor state arrives may lead the remote actor astray.
Figure 19.2 Path of an animated RemoteActor. You can see the effect of locally animating the remote actors by starting a MazeWars session and creating a game. Start a second
session, then, when the Welcome dialog box displays, check the Animate Remotes check box and Join the existing game.
MazeActor Speed Data One of the pre-conditions for supporting local animation of remote actors was the availability of the master actor’s current speed. This data is readily available from the speed control methods in the AutoActor class. However, AutoActors are only half of the story. The ConsoleActor is probably the most important actor in the maze. The ConsoleActor’s motions are defined by the player’s key presses, not by speed-control methods. So, how can we support local animation of the ConsoleActor in external sessions? The basic approach taken was to monitor the movements of the ConsoleActor and attempt to determine its speed based on historical information. Sound complicated? It isn’t. To perform the task, we simply create a new ActorGameBridge class, called the RemoteDataAGB class. The ActorGameBridge class, which we discussed in Chapter 14, acts as the bridge between the actor and the game state and logic. All requests for actor motion are passed through the ActorGameBridge.moveToNextCell() method. We’ll look here for the solution to our missing speed information. The RemoteDataAGB.moveToNextCell() method, presented in Listing 19.2, is called whenever the ConsoleActor requests a move as a result of a key press. The request to move is satisfied by a call to super.moveToNextCell(). After this call, the method determines if the actor did indeed move. If the actor moved, the method checks if the direction is consistent with the actor’s last motion, and if this motion occurred at the projectedMoveTime. If the direction and times are consistent, the actor’s projectedSpeed is assumed to be correct and is stored in the speed member. This speed member is reported as the ConsoleActor’s speed and is suitable for sending across the network. The management of the projectedMoveTime member requires a little explanation. When the actor changes speed or direction, the moveToNextCell() method will execute the block of code responsible for generating the next projectedMoveTime. This code compares the time of the last motion, prevTickTime, with the current time, tick. The difference in these values is the number of ticks per move. It is stored as the projectedSpeed. The projectedMoveTime is set to the current time, tick, plus the number of ticks before the next expected motion, projectedSpeed. If the next motion actually occurs at projectedMoveTime the speed has been constant across two motions, and that is good enough to set the speed member. Listing 19.2 RemoteDataAGB.moveToNextCell(). protected boolean moveToNextCell() { long prevTickTime = lastMoveTick; long tick = getTickCount(); boolean moved = super.moveToNextCell(); Cell c = null; if ( moved && !prevCell.equals(c=getCell()) ) { prevCell = c; // Did they move as we predicted earlier? if ( (getDirection()!=prevDirection) || (tick!=projectedMoveTime) ) { // No, project the next time. prevDirection = getDirection(); projectedSpeed = tick-prevTickTime; projectedMoveTime = tick+projectedSpeed; speed = 0; } else { // Yes, record their speed as a known value. projectedMoveTime = tick+projectedSpeed; speed = projectedSpeed; } } else { testExpired(); } return moved; } The MazeWars class is responsible for associating each actor with an ActorGameBridge. To support the local animation of actors,
the MazeWars.createAGB() method creates a RemoteDataAGB object for all non-AutoActor-derived actors. The ConsoleActor gets a RemoteDataAGB bridge that monitors all its motions and determines if the player ever achieves a constant speed. In practice, this happens quite frequently; when the player keeps a motion key depressed, the actor moves at a constant velocity of one cell per tick.
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
REMOVING THE BROADCAST DELAY Another method of smoothing the motion of remote actor is to send the actor state information more frequently. If we could send the actor state once for each clock tick then everything should look nice and smooth. At least in theory. In practice, as we increase the send rate, the CPU utilization increases to the point where the normal game playing thread and the networking threads can cause each other significant delays. The increased transfer rate affects both sides of the network. The BroadcastThread may generate actor state events and write them to the network once every TICK_TIME milliseconds. Instead of transferring these events directly to the network, they may become queued within the ConnectionLayer. The ConnectionLayer class is coded to process events that have been read from the network before writing outbound events. So if we have a stream of inbound events, it is possible to cause outbound events to be buffered together until there is a break in the inbound flow. The outbound events will then be streamed to the network. Similar buffering can occur between the layers on the receiving side of the network. The result of all this activity is that we may receive events describing all the movements of each actor, but the timing of these events will be inconsistent. In addition, the user interface has suffered because it must compete for CPU time to render the game. Now we have achieved a sluggish user interface and remote actors that surge across the screen. Marvelous. Thankfully there is a solution. By reducing the priority of the BroadcastThread, we can ensure that it will not compete with the game-playing thread. The process of sending actor information is performed when the primary threads are blocked or idling. We must still deal with the problem of events being grouped in the writing and reading buffers of the ConnectionLayers. To handle this situation, we added functionality to the GameLayer and ConnectionLayer classes. The GameLayer.getNewestEvent() method will scan all the pending events for a specified type of event. It will retrieve the event received last and discard all other events of the specified type. The ConnectionLayer.removePendingWrites() method will remove all events of a specified type from the writeEventQueue in the ConnectionLayer. To prevent outdated information from being sent to the network, the BroadcastThread now uses removePendingWrites() to purge any CC_MAZE_ACTOR_STATE events prior to writing a new actor state event. On a similar note, when the MazeWarsGameLayer receives a CC_MAZE_ ACTOR_STATE event, the first thing it does is check if there are any more recent events in the queue for it by calling GameLayer.getNewestEvent(). Under normal situations, the increased flow of game state will allow us to animate actors accurately. If the load becomes too great, and events begin to get buffered, we discard old data and examine only the latest information. In this scenario, our previous work with locally animating actor becomes critical. When the events become buffered, we actually experience both bursts of information and idle periods. The local animation keeps our actors moving smoothly during the idle times. When a burst of data arrives, all but the newest event is discarded, and the RemoteActor is updated with the new value. The optimal broadcast delay is also affected by the responsiveness of the connection between the sessions. Connections on a Local Area Network (LAN) typically have travel times below 200 milliseconds. This can accommodate our optimal transfer rate of TICK_TIME (75) milliseconds. The use of synchronous sockets connecting two sessions, each on a 28.8 PPP connection can reach 1000+ milliseconds! The excessive travel time is due to the nature of the physical connection. The data must travel along one phone line to an ISP, then down a second line to the other player. After this, the data acknowledgment must perform the reverse journey. Practical experience suggests that a minimum broadcast delay of one-third the average travel time is appropriate. Once you exceed this limit, the additional traffic can cause more grief than benefit.
The Min Broadcast Delay check box on the MazeWars Welcome dialog box will cause all the sessions in a shared game to use a low priority BroadcastThread with a delay of TICK_TIME milliseconds.
INTER-SESSION LATENCY We have explored some techniques to improve the responsiveness in sending and processing actor state information. Now we’ll cast our attentions to the second great hurdle of networked games. The network travel time is responsible for offsetting the states of session within a shared game. As Java programmers, we can’t do much to reduce the inherent network delays. Java networking is based on Sockets libraries and TCP/IP connectivity, which provides good performance on a LAN. However, network connections using PPP or SLIP over phone lines are sure to provide longer network latency. The network travel time is responsible for offsetting the states of sessions within a shared game. Figures 19.1 and 19.2 both demonstrate the effect of a two-tick network latency. The time it takes data to travel across a socket is completely beyond our control. Or is it? As the game starts, the player interacts with the Welcome dialog box to join a game. If we could provide feedback describing the network delay for the prospective opponents, then the new player might select a “better” game to play. I have modified the Welcome dialog box for this purpose. Players can now request the network latency times when selecting a game by pressing the NetTimes button. Figure 19.3 shows the modified interface. When the player latency time is determined, it is displayed in the status line at the bottom of the dialog box.
Figure 19.3 Informing the player of network delays while joining a game. LIVING WITH LATENCY Once the player has started playing a shared game, the latency between the sessions begins to occur, producing de-synchronization. The game logic and user interface will operate using remote data that is obsolete as it arrives. The problem of de-synchronized games is very serious because it creates a discrepancy between what the game player sees and the events that occur within the distributed game. The player is unable to account for the fact that the images he is seeing are in fact delayed, and the external actors have progressed beyond their reported positions. The player cannot project the correct location for the remote actors. And, why should he? That is our job.
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
Determining Connection Latency The first step in living with latency is to see how bad the situation is. Latency is quite easy to calculate. All you need to do is send an event across the connection, have the other side echo it back, and when it arrives, calculate the elapsed time. This basic functionality is quite useful, so I added it to the GameLayer/GameEvent framework. Each ConnectionLayer object has a flag to determine if it should track the connection latency. When this flag is set, the HeartBeat thread modifies its behavior. Normally, the HeartBeat thread wakes up periodically and, if the associated ConnectionLayer has been idle, it will generate a HEART_BEAT event. When the ConnectionLayers latency flag is set, the HeartBeat logic will call the ConnectionLayer.testConnectionLatency() method at regular intervals. This method will send a REFLECT_REQUEST event across the connection. The event will fulfill the normal function of the HEART_BEAT event. Both sides of the connection will see an assured flow of events. In addition, the event will be packaged with a RegGameEventData object that will support the maintenance of the registered message caches. The REFLECT_REQUEST event type has some special properties. The Connection object at the other side of the network will read the event and call ConnectionLayer.storeNetworkEvent(). This method has been modified to test for a REFLECT_REQUEST event type. When one is detected, the REFLECT_REPLY event is created and sent back across the network. This logic was implemented within storeNetworkEvent() for two reasons. First, we wanted to measure the network latency, not including the time needed to store, retrieve, and process the event within the ConnectionLayer. Second, the GLE framework deals only with GameEvents objects. In order to capture the trip time, we needed to store the time the event was sent. The only space we had to send this information was the registeredId in the RegGameEventData object that traveled with the event. This data was transferred to a new RegGameEventData for the return trip. For the same reasons, we also catch the REFLECT_REPLY event within storeNetworkEvent(). When a REFLECT_REPLY event is seen, the ConnectionLayer.onReflectReply() is called to store the accumulated travel time. This data is appended to a rolling array of LATENCY_LIST_SIZE (5) latency values. The getConnectionLatency() method reports the average value of this array as the connection latency. Within MazeWars, we track the latency of all the inter-session connections. We use this information in the next extension to our animated RemoteActor. USING LATENCY DATA Given an approximate indication of the latency in a network connection, we can attempt to correct for the time lag between the two sessions. Figure 19.4 shows the familiar example of an actor moving from left to right in successive ticks. The example depicts a twotick network latency as the frames shift down while crossing the network. The animation on the right of the figure depicts the motion we will achieve by correcting the actor motion with latency data.
Figure 19.4 Correcting motion for latency. The actor starts moving in frame 2. This motion is transferred to the network and is first seen at the foreign session in frame 4. When this information arrives, we can calculate that (given a speed of one tick per cell and a network delay of two ticks) the actor is
probably not still positioned at the second cell, but rather at the forth cell across the frame. In successive frames, we continue to project the actor forward until tick 8. At this point, we attempt to project the actor forward two cells, but find the wall in the way. Given this situation, we leave the actor in the cell adjacent to the wall. The result of using the latency information is impressive. Now, despite the network delay, both the local and foreign session display very similar movements. When you factor in that this effect is realized at both sides of a network connection, you can realize a game that actually “plays” how it looks. No longer will you attempt to ambush an actor only to find they have seen you two ticks earlier. The game is automatically projecting your opponents probable position, so they achieve no advantage by their motion. Players can activate the latency compensation by checking the “Latency Compensation” check box in the Welcome dialog box. The code to implement this algorithm is presented in Listing 19.3. The updateLocDirSpeedDelay() method accepts two additional arguments over the previous RemoteActor movement method. The latency argument is retrieved from the ConnectionLayer when it is time to process the CC_MAZE_ACTOR_STATE event. The ticksTillNextMove argument indicates when the master actor would move next. It has been added to the information describing each actor within a CC_MAZE_ACTOR_STATE. We need this information to correctly synchronize the tick when both the master actor and its remote proxy will move again.
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 19.3 The RemoteActor.updateLocDirSpeedDelay() method. public final void updateLocDirSpeedDelay( Cell c, int dir, int speed, long latency, int ticksTillNextMove ) { if ( (speed!=0) && (dir!=Maze.MD_STOPPED) && (latency>0) ) { int ticksElapsed = ((int)latency-MazeWars.TICK_TIME/2)/ MazeWars.TICK_TIME; Cell next = new Cell( c ); while ( ticksElapsed>ticksTillNextMove ) { // Calculate the next cell. next.row = c.row + Maze.motionArray[ 2*dir ]; next.col = c.col + Maze.motionArray[ 2*dir+1 ]; if ( getCellType( next ) == Maze.MCT_OPEN ) { // Valid move. c = next; ticksElapsed -= ticksTillNextMove; ticksTillNextMove = speed; } else { // Not a valid move. dir = Maze.MD_STOPPED; ticksElapsed = 0; } } } // Update the new improved direction and speed. updateLocDirSpeed( c, dir, speed ); } The updateLocDirSpeedDelay() method starts by determining if it can attempt to project the actor’s real position. To do this projection, the method needs values for the actor speed, direction, and latency. If this information is available, the method calculates the number of ticks that have probably elapsed since this information was accurate. We expect this information to be only approximate and are careful to underestimate. If we were to overestimate, then the actor would advance prematurely, only to be dragged back by another CC_MAZE_ACTOR_STATE event. This has the visual effect of altering the actor’s direction of travel for an instant, and that looks wrong. If we underestimate the latency and are proved wrong by the following move CC_MAZE_ACTOR_STATE will move the actor forward again. Because the direction of travel is consistent, this does not jar the players senses. After deciding the number of latent ticks, we start trying to move our actor. At each iteration the decision to move the actor forward one cell is based on comparing whether ticksElapsed is greater than ticksTillNextMove. The original value of ticksTillNextMove is provided by the master actor data within the actor state message. After the first remote actor motion, ticksTillNextMove is set to the actor’s speed. With each motion, the ticksElapsed value is decreased by ticksTillNextMove ticks, until we have accounted for all our latent time. As we attempt to move an actor forward, we check to make sure the actor’s new position is an open cell. We stop the process of projecting the actor’s position if it hits a wall. The result of the latency compensation is quite good. Look back at Figure 19.4. Notice that the actor position is cast accurately after tick 4. We have restricted the error in the remote actor’s movement to ticks 2 and 3, the time when the actor’s initial motion travels across the network. Of course, this approach is not perfect. If the actor does not progress until it hits a solid object, it may overshoot its final position. We also encounter latency delays after an actor hits a wall and stops. The remote must wait for the next actor state event to determine its new direction and speed.
We must recognize the limits of the algorithm. If an actor does not tend to travel in straight paths, or the latency is too high, the results may prove to be unacceptable. We could guard against these possibilities by reducing the projection in these cases. That way, we would hope to project a view of the world that is closer than the actor state event, while minimizing the chance of overestimating the position.
SUMMARY We examined several techniques for improving the realism of remote actors. The techniques ranged from sending more information to using the supplied information more intelligently by projecting the probable actor location. These techniques are applicable to a wide range of network-based action games. Nothing short of removing all latency between shared game sessions could actually correct the problem of living with desynchronized game states. Of course, because this is not possible, all we can do is strive for a realistic game interaction. In short, multiplayer network gaming is not an exact science. It is an art of compromise, trying to keep two or more players happy at the same 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
CHAPTER 20 SQUEEZING THE LAST DROP: JAVA OPTIMIZATION STEVE SIMKIN
H
ave you ever given up on an applet that looked promising because you got tired of waiting for it to download? Thought so.
Now that you’re the developer, and your applets will be competing with the attention span of casual surfers, you’ll want to make sure that your creations get served up promptly. This chapter can help. You’ll learn about some coding techniques that you can use to make your programs smaller and faster. You’ll also learn ways to package your CLASS files to speed their delivery across the wire.
TIP: If You’re Just A Cock-Eyed Optimizer For the last word on optimizing Java programs, see Jonathan Hardwick’s Java Optimization pages (http://www.cs.cmu.edu/ ~jch/java/optimization.html). These pages contain an impressive collection of words of wisdom, practical suggestions, and links to other performance-related sites. Some of the material in this chapter was adopted from Hardwick’s work.
SHOULD I OR SHOULDN’T I? Before we jump into the nitty-gritty of performance and optimization, I must issue a word of warning. Think very carefully before tinkering with your program to improve performance. If your applet is cleanly designed and clearly coded (and it works!), you should be very reluctant to rewrite any of it for the sake of size or speed. Many pressured hours have been spent by countless programmers trying to correct bugs that were introduced by “optimizing” code. These side-effect kinds of bugs tend to be very subtle, and finding them is made equally difficult by the fact that the very change that caused the bug almost certainly made the original code harder to read. Sometimes, a performance enhancement obscures the structure of the original code entirely. Consider yourself warned. Having said that, real reasons do exist for optimization. The most likely one is that your applet is taking too darned long to download over the Net. The applet works, the design is coherent and logical, the Java classes express the design clearly, and yet, you feel that you just have to do something or nobody will have the patience to wait around to sample your brilliant work. Luckily, there are techniques you can use to speed the delivery of your program, as well as some general coding and compiling practices that can help it run faster, without introducing bugs or making it hard to maintain.
DELIVERY Although many factors can influence the length of time it takes to serve up your applet, you have direct control over two: the size of the files you send and their number. In most cases, your Internet host has to make a new TCP connection for every file to be transmitted. For a large applet with many CLASS files, this can result in substantial surfer boredom waiting for the host machines to negotiate all those connections (“123.123.123.123 contacted. Waiting for response. . .”). To address this problem, the major combatants in the battle of the browsers each recognize a file format that allows you to stuff all of your CLASS files into a single container file, which gets sent as a unit to the client machine. The browser then pulls the CLASS files out of the container and loads them. This measure alone can speed delivery considerably, without you touching a single line of code.
There is a problem with this simple solution, however. Browser wars being what they are, Microsoft and Netscape don’t recognize the same container file type. As a consequence, you have to decide which browsers you want to cater to. And you’ll need to continue to keep all of your CLASS files on the Web server, in order not to alienate users whose browsers can’t receive and disassemble packed files. If you happen to be deploying your applets within a corporate intranet, you’re in luck. The corporation is likely to have a standard browser, which simplifies your decision regarding the file format to support. It also guarantees that your applet will reach its clients in good time. Standardizing on one of the popular browsers has another advantage for performance. It means that your code will be passed through a JIT (Just-In-Time) compiler before execution. JIT compilation translates byte code to machine code before running it, making Java execution speed comparable to native speed. NETSCAPE Netscape Navigator knows how to download standard, uncompressed 32-bit zipped files. Just use a tool such as WinZip (available from http://www .winzip.com) to zip your CLASS, audio and image files into a single ZIP file. Place the ZIP file in the directory alongside your CLASS files. Then use the following HTML tag to tell Navigator to take the ZIP file: <APPLET ARCHIVE="TicTacToe.zip" CODE="TicTacToeApplet.class" WIDTH=680 height=473> You can learn more about Navigator’s use of ZIP files at http://home.netscape.com/eng/mozilla/3.0/relnotes/unix-3.0.html.
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
MICROSOFT Microsoft’s Internet Explorer (starting with version 3.0) knows how to download a file format called CAB. CAB, short for CABinet, is a format that Microsoft has used internally for a while. They are now promoting it as a standard for applet delivery on the Internet. To this end, they have made available a set of tools for creating and inspecting CAB files, and will soon release a specification to allow other browsers to understand CABs, too. You can read all about it and get a copy of the tools from http://www.microsoft.com/ workshop/java/cab-f.htm. CAB files give their users a double advantage. In addition to gathering the CLASS, audio, and image files into a single file, the CAB format also compresses them, reducing download time even further. To make your applet available in CAB format, use the Microsoft CABinet Development Kit tool to create the CAB file, place it in the directory with your CLASS files, and add the following tag to your HTML page: <APPLET CODE="sample.class" WIDTH=100 HEIGHT=100> CAB files have one additional feature that you may want to consider. If you want, you can make your CAB file available as a class library that the user can install on her computer. The intention is that frequently used classes should be resident locally to further speed loading time. Although using this feature runs counter to the Java philosophy of not bloating the installed software base on the user’s hard drive, it may solve some practical problems. It’s your call. OTHER VENDORS Not to be outdone, Sun Microsystems has its own Java archive format, called JAR. Like CAB files, JAR files allow the developer to bundle CLASS, image, and sound files into a single file format that supports compression. In addition, the JAR format allows digital signing of individual components of the JAR archive, allowing the browser to authenticate incoming files. Release 1.1 of the JDK supports JAR files. If you’re interested in JAR files, you can read all about them at http://www.javasoft.com/security/codesign/jarformat.html. If you choose to ZIP your applet, your users can load it more quickly by installing a program called CONNECT!Quick client, available from http://www.connectcorp.com/Quick.html. This product claims to boost the speed of downloading regular ZIP files, but it really comes into its own when the server where the applet resides is running CONNECT!Quick server.
CODING FOR PERFORMANCE As I mentioned earlier, you can adopt certain coding practices that will improve your programs’ performance without breaking their logic or obscuring their structure. These performance-improving code practices involve introducing derived variables in order to eliminate redundant calculations. For example, consider the following snippet: int sum = 0, intA, intB, intArray[]; for (int i = 0; i < intArray.length; i++) { sum += intArray[i] * intA * intB; }
The multiplication of intA and intB during each iteration of the loop is invariant, meaning that because the values of intA and intB don’t vary, neither will their product. Therefore, it is unnecessary and can be reduced to single multiplication by moving it outside the for loop, as follows: int sum = 0, intA, intB, intArray[]; int product = intA * intB; for (int i = 0; i < intArray.length; i++) { sum += intArray[i] * product; } Similarly, repeated identical method calls carry an unnecessary performance price. One way of reducing this price is to call the method once and assign its result to a variable. However, if the method is final, private, or static you can speed performance in another way. Java compilers include an optimization option to copy methods with these specifiers into the body of the calling methods, a technique known as inlining. If you’re using the JDK, compile using the command javac -O. If you’re using another compiler, check your documentation for a similar option. To get the most out of the optimization option, revisit your class methods to decide which can be declared final. Remember that, by definition, a final class cannot be overridden, so if your design includes the possibility of extending the class and overriding this method, it’s not a candidate. Neither is a method that accesses private variables, because an inlined version of that method will fail byte code verification. Small methods should certainly be made final, while the cost in size of inlining large methods may outweigh the gain in speed. The decision to inline methods exemplifies the type of trade-offs involved in the optimization effort. Inlining gains you speed by copying the byte code of appropriate methods wherever they are called, effectively making them part of the calling methods. This performance boost comes at the expense of size, of course. Luckily, javac -O reduces the size of CLASS files by stripping the line number information used for debugging and stack traces. In most cases, the resulting reduction in file size more than outweighs the increase in file size caused by inlining. Thus, the overall effect of using the optimization option will be both to speed execution and to reduce size, unless you have a large number of methods that end up being inlined. A word of caution: Don’t use javac -O until you’re finished testing your code. Until then, you’ll need those line numbers for debugging.
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 TECHNIQUES AND A WARNING You can use the techniques we’ve seen so far without a second thought. They will speed the delivery of your CLASS files considerably and will result in faster byte code (and possibly smaller files). Before I show you other optimization techniques, I must issue a few words of warning. First, when thinking about optimization, you must decide what you’re optimizing for. As we saw in the previous section, there is frequently a trade-off to be made between optimization for speed and for size. While in that case, the two effects of javac -O combined to improve both speed and size, most of the techniques we will discuss from now on require you to choose one at the expense of the other. In order to make an intelligent decision about trade-offs, you have to decide what to optimize in the first place. A broad campaign to optimize an entire large program will have two results: It will make your code incomprehensible and impossible to maintain, and it will cause you to spend much time and effort optimizing code that has no real impact on your application’s performance. Much effort has been wasted—and damage inflicted on innocent software—by programmers who made assumptions about where and what to optimize. Don’t fall into that trap. The JDK supplies a couple of tools you can use to gather statistics about which parts of your program could really do with a little help. In order to concentrate on the parts of your program most in need of optimization, you need to know where your program is spending most of its time. After all, a method that takes up just one percent of execution time will have negligible impact on overall performance, no matter how well or poorly it is written. To help you figure out which methods really matter, the JDK includes a profiling tool that keeps track of the number of times any method calls any other method, and how much clock time the program spent executing each method. The profiler can very quickly let you know which parts of your program have the most direct impact on its overall performance. To use the profiler on a Java application, run it with the usual java command using the -prof argument. To use it on an applet, start the applet by entering java -prof java.applet.AppletViewer followed by the HTML file name. In either case, the profiler generates a file called java.prof, containing the profiler’s findings. For example, Listing 20.1 shows some of the contents of the java.prof file that was generated by the program I wrote to test the TicTacToe implementation I described in Chapter 13. I created the file by entering java -prof testAI on the command line. Listing 20.1 Output from the Java profiler. count 228480 36330 36330 36329 36329 29761 29761 28560 7772
callee TicTacToeGameMoveManager.evaluateRo java/lang/Object.()V GameState.()V TicTacToeGameMove.(II)V java/util/Vector.ensureCapacity(I)V java/util/Vector.elementAt(I)Ljava/ TicTacToeGameMoveManager.applyMove( TicTacToeGameMoveManager.evaluate(L java/util/Vector.(II)V
caller TicTacToeGameMoveManager.evaluate(L GameState.()V TicTacToeGameState.()V TicTacToeGameMoveManager.getPossibl java/util/Vector.addElement(Ljava/l MinMax.findMax(LGameState;I)LGameMo MinMax.findMax(LGameState;I)LGameMo MinMax.findMax(LGameState;I)LGameMo java/util/Vector.(I)V
time 4239 28 126 299 304 245 3890 6103 88
Listing 20.1 shows that TicTacToeGameManager.evaluateRow is called far more than any other method in the program. Together with TicTacToeGameManager.evaluate, it also accounts for a large percentage of the program’s running time. So, any effort to speed up the TicTacToeGame should probably concentrate on these two methods.
The profiler enables you to measure the results of your optimization efforts. This is very important. As we’ve said several times, performance-oriented changes to code often blur code structure, making maintenance more difficult. To justify this cost, there must be a considerable, measurable gain in performance. A slight gain may well cost more than it is worth. The time reported in java.prof is based on time-of-day. This means that it measures the amount of clock time your program is spending in each method. Therefore, distracting your CPU by running other programs in parallel with your Java program will result in exaggerated time measures and invalidate comparisons among implementations. To get accurate results, run each implementation on an idle machine, preferably taking the average of two or three trials. The hard-code performance fiend looking for optimizing opportunities will also be interested in the Java disassembler. The disassembler (described at http://java.sun.com/products/JDK/tools/win32/javap.html) has a number of options, including one to display byte code, listed alongside the associated variable and method names. To use this option, run javap -C followed by the Java class name. Figure 20.1 shows the disassembler’s output for TicTacToeGameManager.
Figure 20.1 Output from the Java disassembler. The labeled byte code is useful for comparing the relative size of alternative logic formulations. If trimming every byte is important to you—or if you want to see for yourself how your compiler implements your code—you’ll be spending time with the disassembler.
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
OPTIMIZING FOR SIZE To keep the size of your programs down, take advantage of the core APIs distributed with the JDK. In addition to relieving you of the need to reinvent the wheel, using the core APIs allows you to make use of Java classes that already reside on the client machine. They consequently reduce the size of the byte code that must be transmitted from the server over a network. Another technique for reducing size is to make sure that common functionality is implemented in one place only. I remember learning this as a principle of good design, but it often clashes with performance considerations (remember how we recommended inlining functions to reduce speed?). So consider the trade-offs, do some testing, and make a choice. OPTIMIZING FOR SPEED Avoid using synchronized methods. A common trap for unsuspecting programmers is the StringBuffer class. Many of its methods are synchronized, which causes a bottleneck when calling simultaneously on StringBuffers in different threads. During I/O and network-streaming operations, use buffered classes. Pushing individual bytes around is extremely time consuming. You can make a stream buffered by passing it to the appropriate constructor. For example, given an OutputStream called out, you can make it buffered (with a default 512-byte buffer size) just by passing it to the BufferedOutputStream constructor as follows: BufferedOutputStream bufOut = new BufferedOutputStream(out); Implementing this technique demands careful planning, but it can result in major improvements in a program’s speed. If your program repeatedly executes a line of code that includes a constructor, consider changing the code to create a single object that is reused on each iteration. Remember that every object no longer in use will eventually be garbage collected, an expensive operation. Accumulating many objects will eventually result in major cleanup operations, stalling your program. So instead of calling that constructor, reinitialize the variables of an existing object, and away you go. You can read an excellent article about this technique in the September issue of Javaworld (http://www.javaworld.com/jw-09-1996/jw-09-indepth.html). In heavily graphical applets, make sure that you aren’t doing any unnecessary screen painting. Too many screen refreshes (or refreshing too much of the screen) can make response intolerably sluggish. To learn how to manage dirty rectangles, and other tips for improving response in graphical apps, see Chapter 8.
HOW ARE YOU DOING? On a slightly different note, you may be interested in your particular setup’s Java performance. You can put your system through its paces at Pendragon’s Java Performance Report (http://www.webfayre.com/pendragon/jpr). This site will run a standardized applet on your system and report the results. You can use it to help decide where to cast your vote in the battle of the browsers. Or just to see the elements of a comprehensive Java checkup. Figure 20.2 shows the Pendagron Java performance test in action.
Figure 20.2 My system goes up against the CaffeineMark 2.01 Benchmark.
SUMMARY We’ve seen that optimization is an endeavor that should be undertaken with great hesitation. We’ve discussed the pitfalls of optimization and the trade-offs it involves. But if you must optimize, I’ve suggested generally applicable techniques that won’t hurt your code. Some of these will speed delivery of your class files to the client, while others require you to change the Java code itself. I also discussed the tools that can help you decide which parts of your program should be candidates for optimization.
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 21 FRED CAVIT AYDIN, STEVE DENG, DOUG IERARDI, CRAIG KAWAHARA, SUSANTO KOLIM, TA-WEI LI, AND FERRY PERMADI
F
red is a prototype for a networked, multiplayer game, played in the style of Doom and implemented entirely in Java. When we
began the project in January of 1996, a few Java-based games were already appearing on the Web—although these were mostly single-player arcade games and multiplayer board games. Our goal was to push this a bit further and, at the same time, explore the limits of the then-current Java APIs. Fred, however, was intended to be an experiment, and many of the issues that we struggled with are still unresolved, such as how you get true realtime, 3D graphics from within Java, or how you get sufficient responsiveness for a Doom-like “shoot ‘em up” game over the Internet. In this chapter, we’ll provide an overview of Fred (version 0.2 beta), its overall architecture, some of the design decisions, and the implementation details. In particular, we’ll focus on the following topics. • Graphics—We chose to model Fred on Wolfenstein 3D, an ancestor of Doom that put stricter limitations on its “2 ½” dimensional geometry, but included the familiar first-person point of view and similar gameplay (namely, shoot anything that moves). An essential part of the rendering engines of both of these games is texture mapping: the ability to make a polygon look like part of brick wall by mapping an appropriate texture onto it. Our experiments included several attempts to get some form of texture mapping working under Java. However, all of these failed to deliver adequate performance. Nevertheless, the rendering engine still has the basic structure of a Doom-like game engine, with a few additional shortcuts to speed up drawing under the Java AWT. • Network Latency—We designed Fred to be a networked multiplayer game. An initial CGI script would lead you to an active server where you would retrieve the client applet and join an on-going game with up to 16 other players. This scenario poses several problems, which we’ll discuss in detail in this chapter. • Bandwidth—Once the entire applet is downloaded, the amount of data transferred between the client and server during gameplay is very small; a 14.4 modem is sufficient. • Latency—No one wants to play a shooter-style game in which there’s significant latency—where you press a key and only see the effect on your display several seconds later, or where the images of other players show where they were several seconds earier. A shooter-style game must be responsive. However, no communication across the Internet provides realtime guarantees. There is always some latency, often significant. To deal with this situation, the design attempts to make the client appear as responsive as possible, while applying latency-hiding techniques at the server. • Consistency—Consistency is a problem that plagues many distributed systems. In our game, we need to ensure that the state of the game at each player’s client is the same. • Security—We need to ensure that the correct client software—not a modified version of the program—is being run. We will implement a security mechanism to support this. However, as an added complication, we also want to support a limited form of modification by end-users. We want to allow end-users to extend Fred by defining new Player class objects that alter the characteristics and behaviors of players, objects, and monsters. We will use Java’s dynamic loading of classes to upload new Player extension classes into a running game. The full version of Fred uses approximately 10,000 lines of Java code, implementing 57 classes. Fred can be played at http://langvin. usc.edu/Fred.
GAMEPLAY
As in Doom and Wolfenstein 3D, the gameplay is quite simple: You wander about an alien, quasi-3D environment and shoot anything that moves. Of course, that oversimplifies the paradigm slightly. In a multiplayer game—especially one with a chat facility—ample opportunity exists for building alliances with other players and developing richer strategies. To join a game, the player runs an applet via a browser. The applet first prompts for the player’s name and password, which in this version of the game are used only for the chat facility and for maintaining statistics such as the high-scores list. After entering the game, the player is presented with a 3D viewport, a 2D visual map, and a chat window. Through the viewport, the player can see objects and other players and obstacles within her field of view (60 degrees), as shown in Figure 21.1.
Figure 21.1 Fred in action. The chat window has two functions: to receive announcements from the server and to talk to, and collaborate with, other players. Messages can be broadcast to all players or directed to a single player. A list of all current players displays to the right of the chat window. Each player is identified by his or her unique login name and a unique color. The visual map, shown in Figure 21.2, pops up in its own resizeable frame and gives an overview of the playing field. Players can use this map to pan over the playing field or track the current player. Selecting a player’s icon in this window will identify him by highlighting his entry in the chat window’s menu of players.
Figure 21.2 The map provides an overview of the playing field.
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 ARCHITECTURE Fred is composed of two components: the client and the server. The client is an applet, and the server consists of two processes that run as Java applications on the host machine. The two processes are the Fred server and the TimeServer.
THE FRED SERVER The server mediates communication for the chat facility and broadcasts player activity. Figure 21.3 shows the class diagram for the messages that are sent by the server. The server does not enforce strict synchronization of player activities; in other words, we permit the various players’ views of the world to differ by a small amount at any time. The server itself maintains a “globally consistent” view of the playing field. When it is necessary to reconcile these conflicting views—when one player shoots at another, for example—the server invokes a module (called the Arbiter) to judge the situation, based upon the individual claims of the players, its own global view of the game, and its knowledge of network latencies. These latencies are estimated by a second server process (the TimeServer) that implements a simple variant of the network time protocol (xntp) to synchronize virtual clocks among the server and its clients.
Figure 21.3 Message class diagram in Booch notation. The server itself was also designed to handle just one level or block of a larger playing field. Using this strategy, a client would be sent to the appropriate server as he or she progressed through the game, mediated by a proxy on the server initially contacted. (This feature is not fully implemented in the current version of Fred. Instead, there is merely a “force field” surrounding the accessible playing field.)
The Map The map, which is implemented by the Map class, stores the global state of the world and all of the current players. All access to a map is synchronized. The map at the server end conceptually represents a globally consistent view of the game. Each player currently enrolled in the game has a proxy on the server that can update the state of the map. At the client end, there is a proxy for the server that tries to synchronize the local and global maps by communicating with the server. Meanwhile, the local map is continually updated by the player’s own actions at the keyboard. Figure 21.4 shows the high-level class diagram for Fred. The diagram uses the Booch notation. The clouds are classes. The lines connecting the classes show class relationships. The lines that end in an arrow show inheritance; the lines that end in a circle show has-a relationships (see Chapter 3 for further definitions of these concepts). For more information on using Booch diagrams see Object-Oriented Analysis and Design With Applications by Grady Booch published in 1994 by Benjamin/Cummings.
Figure 21.4 High-level class diagram for Fred. The map can be displayed via a global bird’s-eye view provided by the visual map, or from the point of view of a single player through the 3D viewport. Our initial experiments tested the limits of texture mapping—in the manner of Wolfenstein 3D and Doom—and so the engine is largely driven by a raycaster of the sort used in those games. (More on raycasters later on.) However, because we were unable to achieve suitable performance with pure Java code, we later turned to a polygon-based rendering engine. Much of the original raycaster survived, however, and was used to perform clipping to further optimize the performance of the renderer. THE PLAYER The Player class is an abstract class that is extended by each character and monster implemented. In the current version, these characters are limited to a robot and Doom-style personae for the players. However, the game was designed so that new classes extending the Player class could be designed and dynamically loaded by the users themselves. Hence, each Player class implements a fixed Player interface, with a minimal set of additional semantic and security checks done by the trusted 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
THE RENDERING ENGINE Raycasting is a simpler version of raytracing, which is used in computer graphics to draw real-world objects. HOW DOES RAYCASTING WORK? In their classic Fundamentals of Computer Graphics, Foley and Van Dam explain that raytracing “determines the visibility of surfaces by tracing imaginary rays of light from the viewer’s eye to the object in the scene.” This technique tries to simulate the physical behavior of light rays as they are cast and reflected from objects to a viewer’s eye. For example, light from the sun or other light source bounces off a red ball and reaches our eyes. That’s why we can see the ball. The light rays that arrive on our eyes have different brightnesses. The surface of the ball that is facing the sun is brighter than the rest of the surface. By tracing the paths taken by these light rays, one can create a photorealistic rendering of a scene, with accurate shadows, textures, reflection, and translucence. However, it is impossible to trace all of the light rays emanating from a light source. So instead of tracing the light rays from the light source to the objects, the procedure used in raytracing is reversed: We trace the light from the viewer to the target object to the light source. Although this technique sounds promising, it has one drawback that computer game programmers cannot tolerate. To create such realistic images, a lot of killer math is used. The computations often take a long time. And without specialized hardware, they can be slow. Raycasting, on the other hand, is a stripped-down version of raytracing. In this section, we’ll look at a rendering engine that uses a very restricted form of raycasting found in games like Wolfenstein 3D, after which Fred’s engine is loosely modeled. Let us assume that every wall in the environment has flat surfaces that are perpendicular to the floor, and they all are of the same height. The arrangement of walls on a level of the game can then be represented on a two-dimensional map, showing the position and length of each wall. The job of the raycaster will be to render this two-dimensional map into a three-dimensional scene. (Games of this sort are sometimes called “2 ½ dimensional.”) A series of rays is cast from the viewer’s eye—one for each vertical scanline in the final image. So, for example, in Fred, where the 3D viewport is 320 × 200 pixels in size, 320 rays would be fired from the viewer’s eye, one along each of the vertical scanlines. Each ray is traced through the environment until it meets an object. When it intersects an object (a wall, for example) at some point A, the distance between the source and the intersection point is calculated. All points that are vertically above or below point A form a strip of the wall and, because of our assumptions, all have the same distance to the viewer. This distance can then be used to calculate the apparent height of the wall along this vertical scanline. The entire sequence of strips can then be patched together to render the entire portion of the environment that is visible to the viewer. In short, raycasting tries to map 2D data into a 3D world. Figure 21.5 shows the two types of worlds.
Figure 21.5 2D and 3D raycasting. IMPLEMENTING THE ENGINE As previously mentioned, the input to the raycasting engine is 2D data. This 2D data is the map of the world or the current level of the game. In Fred, the geometry of this world is highly restricted. In addition to the restrictions mentioned earlier, we impose an additional constraint: that the environment is made up of uniformly sized (1 × 1 × 1) cubes, and cubes are laid out along a rectilinear grid of bounded size. Figure 21.6 shows our cubed world. Thus, the geometry of the world can be described by a two-dimensional boolean array, where each entry of the array is true if (and only if) the corresponding location in the world (called a cell) contains a
cube. Each cell represents an n by n unit square in the world. The larger the map, the longer it will take to draw the visible world. In Fred, our map is represented by a 64 × 64 array. (In the actual implementation, certain cells can contain static objects—objects which cannot move. Various walls are also assigned colors. This data is also stored into the array noted above.) Each cell represents a 64 × 64 square unit region of “space” (floor). Each wall is also assumed to be 64 units in height.
Figure 21.6 A world of cubes.
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
CASTING A RAY Before we render the world, there are several attributes we have to define: • Point of view (POV) is a point on the map where the ray originates. • Field of view (FOV), shown in Figure 21.7, is the angle between the first cast ray and the last one. It is suggested the best FOV is between 60 and 64 degrees. We decided to use 60 degree.
Figure 21.7 The angle between the first ray cast and the last ray cast is called the FOV. • Viewing angle (VA) or direction of view (DOV) is the angle that is right in the middle of the FOV. VA corresponds to the direction our eyes are looking. In Fred, we sweep the ray from right to left (counterclockwise). So if FOV = 60 degrees, VA = 0 degrees, the starting and ending angle will be –30 and 30 degrees. • Projection plane is a plane where we draw all the vertical strips. How many rays should we cast? Although the real answer is unlimited, we are constrained by the size of our viewing plane (window). We restricted the number of rays to be the same as the number of vertical strips in the width of the viewing plane. We decided our viewing plane size is 320 × 200. The width 320 means that only 320 vertical strips maximum can be rendered. If we cast (320+a) rays, there are a rays that will not be drawn or they might overlap, so this wastes time. If we cast (320–i) rays, there will be i “blank” vertical strips on the screen. The size of the angle between each subsequent ray is then (60 degrees / 320) = 0.1875 degrees. We know that our map consists of square cells. The cells have only two types of sides, which we call either north-south or east-west. We also know that those two sides are the surfaces of the wall. Recall that in raycasting, we should calculate the distance between the POV and the end of the ray that touches a wall. How can we calculate the distance? Get out your old high school trigonometry book to find the answer. Or, if you’d rather, take a look at Figure 21.8 (it’s a whole lot easier than digging through the closet!).
Figure 21.8 Checking horizontal intersections. We can apply the same formula to check for vertical intersections. In this case, xa is 64 and ya is 64 * Tan(angle). After we get the intersection point, the next step is to calculate the distance. There are two ways to do that, as shown in Figure 21.9.
Figure 21.9 Two ways of finding distance. Of course, as game programmers, we will always choose the faster solution. As many game programming books point out, the square root is a function you’d generally prefer not to have to compute unless it is absolutely necessary. Of course, for the second formula to be useful, we need to reduce the cost of computing those expensive trigonometric functions. (You may also have noticed that the tangent function was used in the intersection calculation, as well.) The trick here is we used precalculated figures. As mentioned earlier, our ray sweeps the world in a counterclockwise direction with a 0.1875 degree increment. The sine, cosine, and tangent functions are tabulated for all degree values at 0.1875 degree increments and are stored in three arrays. Our final step in the raycasting engine is to find the wall height on the screen. This is pretty straightforward because the relation of the intersection point with the height of the wall is inversely proportional to its height. Let’s say that the distance is d, and we choose a constant k. Then the wall height is just (1/d).k. If the distance doubles, then the wall height halves. And so forth. Figure 21.10 summarizes wall viewing.
Figure 21.10 Projecting a wall. HOW TO DRAW A STATIC OR MOVING OBJECT Fred would not be interesting if there are no bad guys that we can kill or static objects that litter the world, like the jade panther. But the overriding question is how can we render these bad guys without sacrificing game performance? The standard trick is to use bitmapped images, usually called sprites. Each character is “photographed” several times from different angles. The number of snapshots taken depends on the shape and texture of the object itself. If it has a very symmetrical shape (like a ball), only two or three images may be required. In Fred, the robot and the panther’s pictures are taken from eight different angles, as shown in Figure 21.11.
Figure 21.11 Eight views of the walking robot sprite. To determine which image to display, we have to know our viewing angle (VA) and the VA of that object. From these two VAs, we know which way we and the object are facing. If, for instance, we are facing each other, then we use the image in the sequence that provides a head-on view of the robot. To make the calculations easier, all objects are assumed to sit within a bounding box, shown in Figure 21.12, whose size is d × w × h. The d × w surface is the surface that touches and is parallel to the floor. We assumed that d is always equal to w. The point in the middle of this d × w square is the intersection point of the ray and this object. This point is treated like the intersection point of the wall and the ray. The only difference is that there might be more than one intersection point on the wall but, using this convention, there can be only one on the object. The distance can then be used to calculate the height of the image on the screen by applying the same formula we used to calculate the height of a wall.
Figure 21.12 A 3D bounding box.
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
GRAPHICS OPTIMIZATIONS Developers of 3D action games often face some conflicting design requirements: • Creating a high-impact, realistic, 3D environment. This requirement can be achieved by using a combination of expensive hardware and more sophisticated, but often time-consuming or resource-hungry, algorithms. • Responsive control and good visual feedback. This requirement reinforces the demand for expensive hardware but limits the use of time-consuming algorithms. • No fancy hardware. For consumer games, developers would prefer to use commodity hardware while respecting the available system resources. This requirement simply nullifies the use of expensive hardware. When developing Fred, we were under the constraint of these requirements plus one more self-imposed restriction: that the entire game must be written in Java, without the use of native methods. Java—at least before the arrival of just-in-time (JIT) compilers—is slow. And even with JIT compilers, performance may still lag behind that of compiled C or C++ code by a factor of 3 to 5, or more. Is Java fast enough for 3D action games? This is one of the key questions that motivated the “Fred exercise.” Our initial raycasting engine was fast enough to suggest that it may be feasible to implement a Doom-like game in Java. Our goal, however, was not to duplicate Doom or Wolfenstein 3D, but to re-create the impression and the visual impact of these games in Java on the Web. WHEN TEXTURE MAPPING ISN’T AN OPTION We always wanted to implement texture mapping in Fred. But the performance price for it is too high. Texture mapping on a surface is essentially an image resampling problem in 2D (uv) space. In our case, the problem involves extracting a vertical scanline from the texture map and painting it using a sprite-scaling technique. This technique requires some form of array manipulation. Unfortunately, we find that array operations are very expensive in Java, probably because of the overhead of range checking on every array access. The emergence of improved JIT compilers may alleviate this handicap, but until then, texture mapping is out of the picture. This left us with the polygon drawing routines of the AWT, which are significantly faster because: • They don’t require pixel by pixel manipulation. • The methods themselves are implemented largely in native code within the Java Virtual Machine. We added a dark, grainy image for the floor, an image for the sky, and some fractal mountains as a backdrop on the horizon. As the finishing touch, we enhanced the three-dimensional feel of the environment by shading walls according to their position with respect to a single light source, and through depth cueing (walls that are further from the viewer are darker than those that are closer up).
Speeding Up Raycasting Without texture mapping, there is no reason to draw the wall vertically, line by line—each cell wall can be painted with one call to fillPolygon. This is a simple modification: As each ray scans cross the 2D map, we build a polygon list using cell boundaries detected by the rays. The polygons are not painted immediately; we later merge the polygon list with the visible sprites and draw them from back to front, using the well-known Painter’s algorithm. The information gathered during the scan can also be used to clip polygons to their visible portions, reducing the amount of overdrawing.
Note that because we are only interested in cell boundaries, it is not necessary to scan the 2D map sequentially. So, what does that leave us? A binary search algorithm immediately comes to mind. We, in fact, can employ an exponential jump method that shares characteristics of a binary search algorithm. The idea is that instead of casting a ray at the increment of one pixel at a time across the screen, we double the increment step, starting from 1, as long as the ray is hitting the same cell. When the ray overshoots—that is, when a new cell is detected—we immediately back track to the last known position where a ray would hit the current cell. A new ray is re-sent at half of the previous increment from the known position. If it still overshoots, we try again using an even smaller increment until the ray returns to the current cell. From then on, we continue to cast rays, but the increment step is reduced by half each time until it reaches back to 1. At this point, the boundary of the current cell is reached. Among the various methods we tried, the exponential jump method performed best on the geometries permitted in Fred levels. Experiments showed that it reduces the number of rays cast by over 80 percent on average and is generally about 6 to 10 times faster than sequential raycasting. Nevertheless, even with this improvement, Fred is still very sluggish.
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
Progressive Rendering Can we increase performance at the expense of image quality and accuracy? The answer is yes. Game developers have often done this to achieve the impression of high performance. These techniques include reducing pixel depth or display resolution, field interlacing, aliasing, and so forth. The key, however, is to degrade the image in a way that is likely to be undetectable or unnoticeable to the user. We know that as the player is moving or turning, he might not perceive as much detail as he would when standing still. A moving player still expects a certain level of visual feedback, synchronized to the commands entered at the keyboard or mouse; a resting player, however, may pay more attention to image quality than frame rate. Our goal is to maintain the game’s responsiveness by appropriately reducing the accuracy of a rendered scene. We need two mechanisms to implement this trick: an indicator that measures system responsiveness and a software control knob that determines when to make the trade-off of speed versus accuracy in the rendering engine. For the former, we use a counter that measures the number of moves the player has made since the last screen update, where a move is defined as either a turn or a step. We want to keep the value of this counter to a minimum. Implementing a control knob in the raycaster may seem complicated, but it is simply an extension to the exponential jump algorithm we discussed a moment ago. As we mentioned in the previous section, the “jump” starts and terminates at 1 to guarantee that no pixel is missed during the scan. Now consider using a minimum jump step k, where k>=1. The effect is that the cell boundary search will terminate quicker, but its accuracy suffers, with a maximum error of k–1 pixels. Some artifacts of setting k greater than 1 are noticeable. For example, when looking down a long corridor, a rectangular strip may appear at the far end of the right-hand side wall, and the wall may seem broken at the near end of the left-hand side wall where it joins a perpendicular face. Another artifact is more subtle: As the player moves, some wall boundaries may appear to shift position from frame to frame. Now we have a nice control knob, k. When k is 1, the rendered image is accurate and artifact-free, but the frame rate is lower. When k is bigger, the raycaster only generates an approximation of the desired image, but at a much higher rate. We can close the feedback loop by connecting k to the player’s current motion. We call this technique “progressive rendering.” The implementation itself is actually fairly simple. Each time the raycaster is activated, it checks the value of the move counter. This value indicates the number of steps the player has moved since the last screen update. A higher number suggests that the raycaster has lagged behind and a higher frame rate is desired. This triggers an increase in k, which in turn speeds up the raycaster at the expense of lower image quality. As the player slows down, the image quality will be improved as a result of a smaller value of k. Sounds complicated? Not really. The code that implements progressive rendering is only a few lines long, as shown in Listing 21.1. Listing 21.1 The Render method. public void Render(Graphics g) { int k; if(Progressive == true) { /* get number of moves player has made since last call to Render() */ k = player.getNumberOfMoves()+1; /* prevent error from getting bigger than the user defined MAXJUMP value */
if(k > MAXJUMP) k = MAXJUMP; } else k = MINJUMP; RayCaster(g,player.getViewpoint(), k); } This simple hack greatly enhances Fred’s playability and responsiveness on a wide range of hardware, from 486s to SGI workstations. Because the artifacts appear only when the player is moving, they do not stay on the screen long enough to be bothersome. Even if the player notices them, the image would have been improved by the time the player slows down and takes a closer look. The progressive rendering technique also has another nice property. Because of its closed feedback loop, image quality is dynamically calibrated to an optimal setting. On a slower machine, such as a 486DX2/66 MHz, the player may occasionally notice some artifacts when she is moving around. But the game will always maintain a level of responsiveness similar to that of a much more powerful machine, such as an SGI Indigo, where artifacts are almost non-existent.
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
Delayed Rendering Of Sprites Another optimization trick used to cope with the asynchronous update problem in Java is called delayed rendering of sprites. As you know, a sprite is a small bitmap that represents either a character or an object in the game. A sprite is considered to be visible if it is within the player’s FOV and is not completely obscured by a wall. Depending on its distance from the viewer, the sprite image is scaled before it’s painted on the screen. The AWT API offers a method called drawImage to do the job. However, there is a caveat. The drawImage method only promises to scale and paint the image. It does not guarantee when the update will be completed and provides no deadlines. The drawImage method is non-blocking. Its returned value indicates whether the image has been drawn. If drawImage returns false, the pending image will be updated asynchronously via one or more calls to the observer’s imageUpdate interface. This behavior is problematic in a realtime graphics environment where the timing and synchronization of various screen elements (such as several sprites that comprise a single frame of an animation) are critical. We derived a method to circumvent this asynchronous update problem. Through trial and error we noticed that only the initial call to drawImage with new width and height values requires an asynchronous update; any subsequent calls using the same width and height values are completed without delay. We exploit this fact to maintain a minimally acceptable frame rate by caching certain previously scaled sprite images. Here is the idea: If drawImage returns false, we will not wait for it to finish; instead, we swap in a previously rendered sprite in place of the current one provided that the size discrepancy is below a preset threshold. However, if the discrepancy is too large, the graphics engine would momentarily suspend itself until the pending image is finished. (This is done by blocking on a semaphore that is later released by an associated ImageObserver.) To place the substitute image, we simply call drawImage again using the old width and height values knowing that this time the image will be painted immediately. As soon as the pending image is finished, it will become the new substitute candidate even thought it may have lagged behind several frames. The delayed rendering technique works quite well in preserving the game’s responsiveness. However, the artifacts may be noticeable to a player on a slow machine when moving very fast through a level. OPTIMIZING FRED’S NEXT VERSION We have presented three optimization techniques implemented in Fred’s graphics engine. This is by no mean the end of the story. There is always room for improvement. The next version of Fred includes the following optimizations.
Environment Caching Visible elements are likely to remain in the player’s FOV for multiple frames. We can exploit this spatial locality of visible objects by maintaining an environment cache for the calculation of subsequent frames. In other words, we use information gathered during the rendering of one frame to approximate the geometry of the next frame, speeding up its rendering. The environment cache simply contains a list of cell boundaries in polar space with the viewer at the center. There are two ways to accelerate rendering in this context:
• Use the cache to provide hints to the raycaster for the next cell boundaries. • Directly project the cell boundaries to screen space using the current orientation and location of the viewer. The raycaster will only be used periodically to eliminate propagation errors and to discover new features in previously occluded space.
Raycasting Vs. 3D Pipeline If the geometry database is small enough, the traditional rendering techniques, including 3D transformation, clipping, and screen projection pipelining are an efficient alternative to raycasting. Only a handful of polygons (about 20) are visible at any one time in our environment. These polygons can be represented by a sequence of points each in polar coordinate (distance, view_angle) form. At each frame, these points are reprojected on to screen space, sorted and clipped, and then painted using the Painter’s algorithm. We expect this 3D projection method to outperform raycasting, as long as we effectively maintain an environment cache.
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
Advanced Map Representation Raycasting is one way to update the cache; another way is to use a Binary Space Partition (BSP) tree. We can use a BSP tree to speed up the ray-tracing process. Currently, Fred’s world is represented by a 2D ASCII array. The time taken to cast a ray is proportional to the number of cells crossed by the ray. For an n × n array, a ray crosses a number of cells proportional to n. The worst case is when a ray goes along the diagonal from one corner of the array to the other. (Using Pythagoras’ Theorem which gives us [sqrt]2n cells crossed). In practice, this is not too bad because inter-wall distances are typically small relative to the size of the map. But if the map contains long hallways or large empty spaces, some of the rays may have to traverse many cells before they terminate at some wall boundaries. BSP trees reduce the number of cells crossed by a ray by directly linking objects together. Instead of the algorithm walking across cells to find an object that a ray might hit, the algorithm directly knows the objects that are in the path of a ray. The speed of the algorithm is no longer proportional to the number of cells and is much faster. A BSP tree presentation is much more complicated to code and significantly changes our current 2D array-based raycasting algorithm. See the side bar on BSP trees for more information.
Java Perks: Binary Space Partition (BSP) Trees BSP trees are used to record how objects in a world are linked together. In a cell-based system, objects are placed in a position in the world at cell (x, y) say. However, this is not the case with BSP trees. A BSP tree partitions the objects in a world into groups depending on where the objects are placed relative to each other. This is called space partitioning. A BSP tree divides the world recursively into groups of two. To create a BSP tree, a reference object is chosen within the world. All the objects are then divided into two groups: those that are behind the reference object and those that are in front of the reference object. A reference object is then chosen in each of the two halves and those halves are themselves split into two halves based on objects behind and in front of the reference object in that half. This process continues until only one object is in each half. We now have a binary tree which places each of the objects as either behind or in front of another object. The neat part comes when the BSP tree is used. If you imagine a player standing in front of a wall. We want to draw what the player can see. So, what do we draw? We draw the wall and all the things behind it. If we draw them in reverse order—the things behind first—then we don’t even have to worry how big the wall is, the things behind it will be seen. As you might guess, I’ve ignored many complications caused by BSP tree such as the complication of having an object that is both in front of and behind another object, but I hope that you can see that BSP trees allow us to very quickly calculate the objects that a player can see. For more information on BSP trees see the BSP tree FAQ at http://www.qualia . com/bspfaq.
Another approach is to use a multiresolution representation of the map. You can imagine it as a pyramid of our 2D map, with increasing resolution from top to bottom, doubling its dimension at every level. The raycaster can be easily adapted to accommodate this scheme: We simply “elevate” the ray to a higher-level map if it travels far enough from the player. Not only will this method accelerate the ray’s traversal, it will also create the effect of varying levels of detail.
Texture Mapping And Other Graphics Features
At the time of this writing, both Microsoft and Netscape have included a JIT compiler in their respective browsers for Windows 95 and Windows NT. Further optimizations in JIT compilers, and in the Java Virtual Machines themselves, may some day resurrect the implementation of texture mapping in Fred. Nevertheless, in the near future, the most likely path to a fast game engine probably comes through the use of native code. In the 3D gaming world, tight loops are often recoded in assembly language and fixed point is often used in place of floating point—at least that’s how we did it in the pre-Pentium days. While specially crafted APIs for 3D gaming, with accompanying native methods, will probably not appear in the near future, the promised 3D graphics APIs are likely to provide a robust, cross-platform solution to many of these problems. These APIs will provide access to underlying graphics hardware, when available, while maintaining the same level of compatibility on non-accelerated platforms.
CONSISTENCY AND LATENCY Latency is an inherent property of the communication system. The further messages must travel, the higher the latency. Modern computer networking is certainly no exception to this rule. Moreover, the routing of packets and the processing of messages (fragmentation, calculation checksums, and so forth) along these networks can only increase this latency. One major challenge in creating a networked realtime multiplayer game is how to deal with this inherent latency of the network. Multiplayer PC games often achieve good performance by relying on direct modem-to-modem connections, where latencies are much lower than across the Internet. Multiplayer games on workstations are generally playable over LANs, where, again, the latencies are rather low. Currently, however, the most successful multiplayer games on the Internet (Netrek, for example) have been those that don’t demand the sort of realtime responsiveness of a Doom clone. Conceptually, Fred tries to maintain a single shared map—a shared data structure that resides on the server—among several networked clients. Each client is has its own copy of this map, which it is constantly updating. The problem is this: How do we keep each of these local copies consistent with the single shared copy on the server—and with every other player’s local copy? The answer is synchronization.
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
TO SYNCHRONIZE OR NOT TO SYNCHRONIZE We can deal with this situation in several ways. We can enforce complete synchronization among all the maps. In this solution, none of the players sees any change in the world until that change has been broadcast to and acknowledged by all of the other players. Let’s see how this strategy works in terms of the implementation. Every copy of the map is considered to be locked by a single “key” held by the server. When a player wishes to record an update to the map, she requests the key from the server. When the key is available, she unlocks the map and modifies its contents. The changes are then broadcast to all players. Once the change is acknowledged by all players, the key is made available to the next player who wishes to modify the map. In case you’re wondering, all of this handshaking is really necessary. Players A and B should not be able to occupy the same place at the same time. This means that they should not be permitted to move into the same place at the same time. If they were to try doing this, then the system needs some mechanism to serialize the players’ moves—to delay B’s move until A’s move has been recorded and displayed, for example. Of course, this means that when a player moves forward in the game, that move is sent to every player. And the actual update on the moving player’s display won’t occur until all other players receive and acknowledge the change. The resulting lag, however, is a function of the latencies observed by all players in the game. For a fast-paced game, this solution currently works well only over a modem or fast LAN, where the latencies are low. A second solution is not to synchronize at all. In this case, when a player moves, the screen is updated immediately and the change is sent to all other players. Because there is some network latency, all of the players will run somewhat out of sync with each other and have a local view of the world that is somewhat inconsistent with each of the other players. Of course, this strategy leads to inconsistencies that are often noticeable during game play. For example, player A may shoot at and hit player B in his own local view of the world. Yet, in player B’s view of the world, he has already moved out of view of player A. Whether we record this as a hit or a miss, the decision would appear to be unfair to one of the players. REACHING A COMPROMISE While developing Fred, we discussed extensively how to deal with these latency and consistency issues. Since Fred was written in Java and Java is the Internet programming language, we wanted to have Fred perform reasonably well over the Internet. Thus, we decided on a compromise between these two models. The objective of the network communication design is to achieve fairness without extensive synchronization among the players (synchronization would negatively impact on the performance of Fred over the Internet where latencies are high for most connections). The philosophy of the consistency handling in Fred is to assume no synchronization among the players’ states. Each player maintains his own state or picture of the world (player health, position, weapons, etc.), which may be somewhat out of sync with everyone else’s picture of the world. This state is displayed on the screen in both the visual map and the 3D viewport, which leads to a good response time during game play because the player’s actions can be immediately translated into a visual effect on the player’s display. Of course, because we cannot guarantee complete consistency without complete synchronization, we must settle for something weaker—some form of “fairness.” To attain this, Fred maintains a global reference state at the server, in case a conflict should arise among the players. For example, consider the scenario we described above: Player A shoots at Player B and believes there is a hit. Player B, in his own view of the world, thinks that no hit occurred. In this case, we would like to resolve the issue by having both players refer to the global reference configuration. However, there is still at least one drawback to this approach.
Without synchronization, the player with lower latency has a big advantage over the player with higher latency, because the lower latency makes the near player’s local state more closely synchronized to the global reference, which is the basis of all conflict resolution. A nearer player can shoot more precisely than a further player, which is unfair to the player with higher latency.
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 solution to this problem might be to place the global reference state in a place where it has the equal latency to both A and B. However, it is not practical to migrate the host in this way. Instead, we emulate this solution by taking latency, among other factors, into account when resolving such conflicts. A module within the server, called the Arbiter, handles these conflicts on a case-by-case basis. The Arbiter takes a report of the incident from each of the players involved and provides a resolution based on those facts, together with latencies, the distance between the players, their speed, and the presence of obstacles in the environment. Actual latencies are computed by a second server (the TimeServer) that synchronizes virtual clocks among all of the players. To implement the Arbiter, we decided to use fuzzy logic as the engine for conflict resolution. For example, when one player shoots at another, the final value of the conflict resolution is the damage to the target. So, we define a fuzzy set called Damage. All of the factors that can influence the assignment of damage are defined as fuzzy variables such as Latency, Distance, WeaponPower, and WeaponRange. From this set and these variables, we derive several rules to infer damage from all the factors. For example: • If latency is high, the damage is low • If latency is medium, the damage is medium • If latency is low, the damage is high These three rules attempt to emulate a placement of the global state at the center of all players, with respect to the true observed latencies, by giving greater freedom to the player with higher latency. To implement all of this in Java, we need to establish a communication path for all clients to broadcast their change-of-state information. Although peer-to-peer connections would probably have been more efficient, the decision to implement the client as a Java applet made this approach impossible. Because a Java applet can only open a connection back to the server that it comes from, Java security constraints required that we employ a central server running on the same host as the Web server. The central server broadcasts all updates from each of the clients to all of the other player’s clients. The logical place to maintain the global reference state is, of course, in the central server. While forwarding state change messages among the players, the server also updates its global state. FURTHER OBSERVATIONS In developing Fred, we ran across a problem in Sun’s reference implementation that had a significant impact on network latencies, one that we suspect is related to the implementation of user-level threads within the Java Virtual Machine itself. In the Fred server, each client has a proxy (running as a thread) that blocks on the socket reserved for acquiring player updates. However, it seems that these threads do not wake up and run immediately when the data becomes available. There is an additional, measurable delay of about 100 milliseconds associated with waking up the Java thread blocking on a socket. Table 21.1 shows the performance results of experiments we conducted over PPP, Ethernet, and FDDI networks, contrasting the performance of comparable Java and C clients and servers on a LAN. Table 21.1Network performance results.
Server
Network
Round Trip Delay
Java
FDDI
101 milliseconds
Java
Ethernet
123 milliseconds
Java
PPP (28.8 bps)
230 milliseconds
C
FDDI
1 milliseconds
C
Ethernet
20 milliseconds
C
PPP (28.8 bps)
140 milliseconds
Since the implementation of Fred, several new APIs that we can use to simplify its networking component have appeared. Remote Method Invocation (RMI) provides a mechanism for invoking methods on remote objects. In order to create the same effect as RMI, we had to define a Client class in the applet to hide all of the network communication from the rest of the applet classes. In the server side, each player has a proxy object to which each player’s client object talks. The client has methods, such as move and shoot, invoked by the other classes in the applet. (In effect, the client is remotely invoking methods on the global map housed at the server.) When the client receives such a message, it creates a packet suitable to be transferred over the network and forwards it to the proxy on the server side. When the proxy receives a message, it extracts parameters from the message and invokes the corresponding method on the server object. With RMI, the code for this communication can be significantly simplified, eliminating both the Client and Proxy classes from applet and server side, respectively. The object serialization APIs could also have been used effectively in this implementation. Object serialization provides a consistent way of transforming objects to and from a sequence of bytes. In the current implementation, the messages exchanged between client and proxy were defined as different Message subclasses, which required us to write our own code to convert the messages into network packets and to restore them from packets when they are received. Not difficult to code, but certainly laborious. It would be more helpful to use object serialization to automate the translation of these message objects into a form suitable for transportation across the network.
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
DYNAMIC LOADING IN JAVA One of the most exciting features of Java is its ability to create programs that have the ability to decide at runtime which classes to use. This ability, known as dynamic loading, provides a programmer with a great deal of flexibility when designing a program. The actual implementation of these classes does not need to be known and can even be changed, so long as the interface to the class is not affected. In fact, Java does not require that the classes to be loaded reside on the user’s local computer; Java can load classes across a network from a remote computer. This feature can be accessed through some creative use of the Class class. Take, for example, the following line of code: RealClass newClass = (RealClass) Class.forName("RealClass").newInstance(); There are three things going on in this one line of code: • The static method forName of the Class class is called to load a class named RealClass. • The forName method returns an instance of the Class class. • The newInstance method is called to create an entirely new instance of the Class class. The object returned by the newInstance method call is then typecast into a RealClass object. The newly created RealClass object is then assigned to the newClass variable. This variable can be used as if it were any other instance of the RealClass class. The only information needed to load the RealClass class is its name contained in a String constant. The class that is actually loaded does not need to be a RealClass class but could actually be any descendant of this class. Even if the user is prompted to enter the name of the class to be loaded, this line of code will execute correctly so long as RealClass is used as a parent class at some point in time. Game designers can take advantage of dynamic loading in a number of ways. The Fred game engine uses it to allow users a large amount of customization, primarily in two areas. The first allows the user to decide which type of rendering engine to use. There are currently two different types of rendering engines available: an experimental raycasting engine or a polygon-based engine. Depending on which engine the user selects, the appropriate class will be transferred to his computer and the action will commence. This approach not only allows the user to express his preference, but also provides a game designer with a convenient way to upgrade portions of the game engine. The second, and more compelling use of dynamic loading within Fred happens with the actual players. The classes that are used to control all aspects of the various players, including the computer-controlled robot players, can be changed at runtime by the user. The user even has the option to create his or her own Player classes, with customized attributes, and enter them into the game. Other users will then be presented with a brand new player that they have never competed against. In fact, a user with sufficient programming skills could even design his own robot player that could enter the game and begin playing, all without the assistance of the actual user. The Fred engine does not exhaust the possible uses of dynamic loading. A game engine that is designed correctly could actually be customizable to the point where two users could be playing games that look extremely different, but actually be playing against each other. Both the game designer and the game user reap the benefits of dynamic loading. The game designer has the ability to redesign or reimplement portions of a game without having to redistribute the entire game itself. The game user is able to customize the game,
with an infinite amount of game variation and replayability.
SECURITY ISSUES Security is an important issue in most networked applications. Many of these issues are handled by the design of Java itself, and by the limitations placed upon Java applets. However, networked games have other problems to deal with. Security violations in networked games are certainly not as serious as the ones that might occur in financial applications, for example, but they have their own consequences and should influence game design.
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 most important of these violations occurs when the design of a game permits one or more players to gain an advantage over others using factors from “outside” the game itself. For example, in the popular and long-lived networked game Netrek, such a player is known as a “blessed Borg.” A player becomes a blessed Borg if she gains extra playing advantages over the other players. For example, a player could achieve this status by cracking the code downloaded from a server and inserting special “cheats” to gain a playing advantage. This sort of unfair advantage depends upon the nature, design, and implementation of the game. In a “shoot ‘em up” game, like Fred, who wouldn’t want to have unlimited health and powerful weapons? Or be able to shoot or move faster or more accurately than any other player? To derail such tampering, Netrek employs a signature scheme based on public-key cryptography. Clients are assigned unique public and private key pairs. Before joining a game, the server sends the client a random string. The client encrypts it with its private key and sends it back to the server. The server then uses the public key of the client to decrypt it and compare to the original string. Such a scheme is useful for player authentication. Almost all the multiuser network applications take security precautions. The most common is the classical user name/password combination. Fred utilizes a password-protected interface in its initial login screen to protect the integrity of its high-score database. However, in Fred we had to take additional precautions because the game was designed to allow users to install custom player classes. Because of this flexibility, classical security and player authentication is not enough. This simple but very useful addition of uploading custom player classes can have unpredictable results. It’s easy for an adversary to modify the methods in such a class to change the behavior of a typical player. We attempt to solve this by imposing our own security checks on player classes at the server end. Just as Java code itself is required to pass certain security checks, each Player class—in addition to implementing a specified interface—must also adhere to predefined semantic constraints on its actions. For example, all health and damage estimates are checked and corrected at the server. Player movements and hits must be “reasonable,” as well. So all actions of a player are examined and compared to the behaviors of a typical player. For example, a typical player cannot shoot in all directions within a short amount of time and cannot move a large distance from one place to another within a short amount of time. These are some of the behaviors the server can easily detect with the help of the TimeServer (using synchronized virtual clocks to timestamp all actions).
SUMMARY AND ACKNOWLEDGMENTS In this chapter, we’ve dissected Fred so that you can see first-hand the challenges that such a networked game imposes and the obstacles we’ve had to overcome in perfecting our creation. We’ve discussed issues surrounding security, latency, consistency, and graphics rendering. We’ve even included some optimization techniques that we implemented in the current version of Fred, and several other techniques that we’ll be implementing in future versions. The version of Fred discussed in this chapter was implemented by Cavit Aydin, Steven Deng, Doug Ierardi, Craig Kawahara, Susanto Kolim, Ta-Wei Li, and Jack Tsou at the University of Southern California during the Spring of 1996. Of course, many others contributed their time and efforts to the cause. We’d like to acknowledge the assistance of Sharill Ibrahim (Boss Films) and Scott Wang (The Shoah Foundation and Disney Online), whose help was greatly appreciated during various stages in the planning and implementation of Fred. And although the Internet provided an enormous source for many of the images and sprites, Susanto Kolim artfully generated others with the use of the Persistence of Vision raytracer (POV-ray). We’d like to express our thanks also to Ferry Permadi who created the fractal horizon. If you have any questions, comments, or suggestions, we’d love to hear from you. Drop us a line at [email protected].
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
Table of Contents
APPENDIX A A JAVA TUTORIAL STEVE SIMKIN
N
eil chuckled when we agreed that I would write this chapter. How could I possibly teach anyone to program in Java in just 30
pages? The answer is, of course, that I can’t. Anyone interested in learning Java thoroughly should get a copy of Java Programming EXplorer, by Neil, Alex Leslie, and myself. That said, readers with a programming background can pick up some basics from this chapter, especially in the areas most relevant to games programmers. If the material here gets you started, and you feel ready to progress without investing in another book, you can refer to Javasoft’s Java documentation site at http://java.sun.com/doc/index.html. Before jumping into the features of the Java programming language itself, I’ll spend a couple of pages describing the qualities that make Java such a great language for programming Internet applications. Then, we’ll write the simplest possible Java program, the infamous “Hello, World.” This little program will show us the importance of classes in Java programming. It will also give us a chance to learn to use the javac compiler and the java line command, used to run Java applications. With “Hello, World” under our belts, we’ll be ready for a whirlwind tour of Java syntax. Somehow, in a few pages, you’ll be taught about Java data types, logic flow, and method implementation. You’ll get to use your new knowledge of Java syntax in the next section of the chapter, a quickie tutorial on applet writing. You probably know that applets are the small programs that can be embedded in HTML pages. In other words, the Internet games you will learn to write in this book are going to be applets, so you’ll want to pay close attention to the applet lesson. After that, I’ll show you how to put a pretty face on your applet with the User Interface components that come with the Java Developer’s Kit. I’ll also show you how to make your applet interactive by responding to user events, such as mouse clicks. With so much to cover, we’d better get going…
WHAT MAKES JAVA SO SPECIAL? Nothing captures the essence of Java as elegantly and concisely as the following statement from Sun Microsystems’ white paper, The Java Language: An Overview: Java: A simple, object-oriented, network-savvy, interpreted, robust, secure, architecture neutral, portable, high-performance, multithreaded, dynamic language. From the point of view of Internet game programming, the most important buzzwords from the white paper statement are networksavvy, architecture neutral, and multithreaded. So we’ll look at these a little more closely. Network-savvy: As we discussed in Chapter 1, Java’s distributed nature allows applets to flow freely across the Net, from the server machines where they reside to the client machines where they run. For multiplayer games, Java’s network package offers easy communication methods between applets and server programs on the host. Because every player can communicate with the host, the host can let them communicate with each other. Finally, the fact that the client program is always loaded from a single, central repository guarantees that all players are always using the latest release, so the programmer is spared compatibility considerations. Architecture neutral: The Internet’s beauty is its openness. This has always been true for email and Usenet newsgroups. It should
be true for games, as well. Java goes a long way towards realizing this goal. Java programs are compiled into a format called bytecode. Bytecode is understood by the Java Virtual Machine (JVM), which is a program that translates bytecode into the machine code that can be understood by real-world, physical computers. Figure A.1 shows a Java program as it makes its way from source code to bytecode to machine code. Any platform for which a JVM has been written can run any Java program. Most popular platforms already have implementations of the JVM, and more are being added all the time. For the games programmer, the message is, “This game room is open to everyone!”
Figure A.1 Entering, compiling, and running a Java program. Multithreaded: In the first chapter, we looked at a game called Europa. One of Europa’s features is its multiple windows. There’s a master window that displays a list of everyone logged on to the Europa server, a graphical picture of games in progress and their players, and a chat area. In addition, there’s a window in which the Europa game is actually played. This game window has its own optional chat area. All these display areas are constantly refreshed with information entered by the player or received over the network. To coordinate all this activity, Europa uses a number of threads. Threads are almost like independent programs (they are sometimes called lightweight processes), each with its own task. Unlike other popular programming languages, Java has built-in multithreading capabilities. The programmer can easily start threads, kill or suspend them, and have them talk to each other. Java takes care of apportioning system resources so that every thread gets a fair turn. This allows Java programs to do complicated work while keeping the programming of each individual thread relatively simple. Before we leave our general discussion and start writing code, a few more comments are in order. You’ll soon see that Java shares many keywords and language elements with C++. This was a deliberate decision by James Gosling, the inventor of Java. He wanted his new language to be simple for programmers to learn, so they could start working quickly. But there are a few fundamental differences between Java and C++ that must be understood from the outset. First, there are no pointers in Java. Java objects are accessed by reference. The reference variable can’t be used for anything other than to refer to its object. There is no such thing as a generic pointer, certainly no pointer arithmetic, and only limited opportunities for casting objects to other types. A second major difference between Java and C++ is the lack of memory management. The JVM runs a system thread that is responsible for detecting objects that are no longer accessible and freeing the memory they occupy. This garbage collection thread runs in the background and steps in to do its thing whenever the CPU has a little time off from its other responsibilities. A few other differences between Java and C++ have the effect of enforcing object orientation and simplifying inheritance hierarchies. The goal of these Java differences is to enhance software quality and security. Pointer arithmetic is one of the major source of bugs in C++ programs. It is also used heavily by the authors of virus programs to access memory addresses that should be off-limits. Similarly, by taking memory management out of programmers’ hands, Gosling simply removed a second potential source of programming errors.
NOW YOU’RE READY FOR HELLO, WORLD After all that talk, it’s time to do some coding. Listing A.1 shows how to write everybody’s first program, “Hello, World”, in Java. Listing A.1 HelloWorld.java. class HelloWorld { public static void main(String args[]) { System.out.println("Hello, World"); } } We can learn some important points about Java from HelloWorld.java. First, the entire program is enclosed in a single class. In keeping with its pure object orientation, the only unit of expression in Java is the class. All data must reside within a class, and all operations are performed by class methods. The HelloWorld class has a single method called main. main is a public method, meaning that any other class can call it. It is also a static method, meaning that it is associated with the class definition, rather than with an instance of the class. In other words, I don’t have to create a HelloWorld object in order to invoke HelloWorld’s main method. The word void preceding the method name indicates that main doesn’t return a value to the program that calls it. When we specify the type of value that a method returns to its calling program, we are assigning a type to the method (in this case, void). All Java applications (standalone programs that can be run from the console command line) must have a public static void main
method. The method needs one more thing: There must be parentheses following the method name that contains “String args[]”. These words let us know that the main method accepts a series of words (known as an array of String objects) from the program that calls it. We’ll see in a moment how to pass these Strings to main, when we learn how to run Java applications. In Java, unlike C++, you can’t use a simple printf or cout function call to display the “Hello, World” message. Let’s analyze the code that actually does display the greeting in order to see why not. Here’s the code: System.out.println("Hello, World"); Remember when I said that the only unit of expression in Java is the class? It follows that in order to display a message on the system console, I must call a class method. Because the only method in the HelloWorld class is main, we’ll have to look elsewhere. The first element in the display statement, System, is the name of a Java class that provides system operations to other programs. System has a publicly accessible variable called out, an object representing the standard output stream. For standalone Java applications, anything written to out is displayed on the system console. Notice that to refer to out, you write System.out. That is, the name of the parent object, then the name of the child object, separated by a period. System.out offers a number of output methods, including println. As you can see from the sample code, System.out.println accepts a single argument, the String object whose content is to be displayed on the console. Why a “String object,” and not simply a “string” or a “character array?” In Java, character constants enclosed in quotation marks are full-fledged objects, with all the attributes and behavior that implies. We’ll talk about the implications of this fact in a few pages, when we discuss data types. I think we’ve squeezed HelloWorld dry. Time to see what it does. Before we can run HelloWorld, though, we have to compile it. Assuming that you’ve installed the JDK and set up your environment according to the download instructions, compiling your program is simple. Just open a console window (a DOS prompt if you’re in a Windows environment), go to the directory where you saved your source code, and at the prompt type: > javac HelloWorld.java If you didn’t get any error messages, your compilation was clean. If you now list the directory contents, you’ll see a new file called HelloWorld.class. This is the bytecode version of HelloWorld.java. Now you’re ready to run your program. Go back to the system prompt and type: > java HelloWorld Notice that when running the javac compiler, you have to enter the source file’s extension (.java); however, when running the program itself using the java command, you do not enter the class file’s extension (.class). If you typed everything correctly, you should see the “Hello, World” greeting displayed on your console. Figure A.2 captures the process of running and compiling HelloWorld.
Figure A.2 Compiling and running a Java application. Before we take a closer look at the elements of the Java programming language, let’s make one little enhancement to HelloWorld. I promised to show you how to pass String arguments to your application. To do this, I’ve modified HelloWorld to accept a single String argument from the console command line and echo it straight back to the console. Listing A.2 shows the new program, which I call EchoArg.java: Listing A.2 EchoArg.java. class EchoArg {
public static void main(String args[]) { System.out.println(args[0]); } } Compile EchoArg.java, and run it as follows: > java EchoArg hi You should see the word “hi” echoed back to the console. Of course, EchoArg’s applicability is pretty limited; it echoes only the first argument passed to it and crashes if you don’t pass anything. Still, it demonstrates that each word on the command line following the name of the Java class gets passed to the application as a member of the String object array argument to the application’s main method. If you’re still with me, then you’re ready for the real stuff.
JAVA SYNTAX The next few pages are my attempt to capture the essence of the Java programming language. It certainly doesn’t tell you everything, but it should let you know what the topics are. For those with a programming background, there’s enough information here about each topic to get you started. When you want to know more, consult the Javasoft site I mentioned at the beginning of the chapter. WHAT’S NOT HERE? Some important topics are simply beyond the scope of a chapter this short. I will not even try to introduce you to the basics of objectoriented programming. This chapter assumes that you are familiar with at least the terms class, object, and method. If not, take a look at Chapter 6 of the Java Programming EXplorer. Similarly, in this section, I will not be touching on inheritance as it’s implemented in Java. This includes how subclasses extend their parent classes, the notion of abstract classes, and the interface mechanism, which replaces the C++ notion of multiple inheritance. When we look at applet programming, we’ll see examples of these things, but they will get just enough treatment to clarify the program in which they occur. I will also not be covering exception handling in Java. Java’s exception handling mechanism is significant, but requires a longer treatment than I could give it here. The examples of exception handling in the sample programs throughout this book should give you the idea. Again, if you want to learn more, check out Chapter 5 of the Java Programming EXplorer. NAMING RULES FOR JAVA IDENTIFIERS In Java, anything that can be named is called an identifier. Identifier names can start with any letter, a dollar sign ($), or an underscore (_). The remaining characters can be any of these, as well as digits (0 through 9). COMMENTS There are three ways to write comments in Java. A double slash (//) comments out anything to its right on the line where it appears. A slash-asterisk (/*) followed by an asterisk-slash (*/) makes anything between them into a comment. This type of comment can stretch over several lines. A slash double-asterisk (/**) followed by an asterisk-slash (*/) is a special type of comment known as a documentation comment. When a documentation comment immediately precedes a declaration (of a class, interface, constructor, or member method or variable), it can be recognized and used by automated documentation generators, such as the javadoc tool that comes with the JDK. SIMPLE DATA TYPES In Java, there are two kinds of data types: simple (or primitive) and composite. As you might guess, simple types are those that can’t be broken down into their components, while composite types are composed of combinations of simple types. Java has four kinds of simple data types: integer, character, floating-point, and boolean. For each one, I’ll give the range of permissible values, the most common notation for constants, and the type’s operators.
Integer Types
There are four integer types in Java. Table A.1 lists them, along with their minimum and maximum values. Table A.1Java Integer Types.
Type
Minimum Value
Maximum Value
byte
–128
127
short
–32768
32767
int
–2147483648
2147483647
long
–9223372036854775808
9223372036854775808
Notice that all the integer types are signed. Unlike C, Java has no way to declare these types unsigned. Integer constants are usually written in their standard decimal format. For example, to declare an int variable and assign a value to it, you would use: int MyInt = 11; By default, integer constants have a type of int, unless their value is beyond int’s limits, in which case they are of type long. Table A.2 shows the operators for the Java integer types. Table A.2Integer Operators.
Operator
Description Unary negation
~
Bitwise complement
++
Pre-or-post unary increment
—
Pre-or-post unary decrement
*, /, %
Multiplication, division, modulus
+, -
Addition, subtraction
<<, >>
Left shift, sign-propagating right shift
>>>
Zero-fill right shift
<, <=
Less than, less than or equal
>, >=
Greater than, greater than or equal
==, !=
Equal, not equal
&, ^, |
Bitwise AND, bitwise XOR, bitwise OR
=
Assignment
Java also recognizes Op = operators, meaning binary operators which combine with equals signs. For example, we can add 2 to the current value of MyInt as follows: MyInt += 2; This notation can be used with any binary operator, on operands of any data type.
The Char Type
Java has a special type called char which is used to represent character values. char is actually an unsigned integer that can hold values ranging from zero to 65535. Why so many? Because Java was designed to work with a character set called Unicode. Unicode is an emerging standard that associates the complete character sets of as many scripts as possible, both living and historical, with 16bit character values. All ASCII characters have the same value in Unicode and ASCII, so most people reading this book won’t feel any difference. But for those who will work with other character sets, the char type was built to accommodate all 34,168 Unicode characters that have been assigned so far, plus all that could potentially be assigned. Character constants are written enclosed in single quotation marks. Since char is an integer type, we can perform arithmetic on char variables. For example, we could write a loop that walks through each lowercase character as follows: for (char c = 'a'; c <= 'z'; c++) {. . .} Table A.3 lists non-printable characters that must be written as escape sequences by preceding them with a backslash. Table A.3Characters Requiring Escape Sequences.
Value
Escape Sequence
Newline
\n
Horizontal Tab
\t
Back Space
\b
Carriage Return
\r
Form Feed
\f
Backslash
\\
Single Quote
\'
Double Quote
\"
Octal Bit Pattern
\ddd
Unicode Character
\udddd
Floating-Point Types There are two floating-point types in Java. Table A.4 lists them, along with their minimum and maximum values. Table A.4Java Floating-Point Types.
Type
Maximum Absolute Value of Mantissa
Minimum Value of Exponent
Maximum Value of Exponent
float
2^24
–149
104
double
2^53
–1045
1000
Like integer constants, you can enter floating-point constants in the same format you would use in normal writing: 3.7 or -65.89. You can also enter them in scientific notation: 602.71E3. Floating-point constants have an implied type of double. All of the operators available to integer values are available to floating-point values, except for the bit-level operators.
The Boolean Type
In Java, the results of all conditional evaluations take a type called boolean. Expressions and variables of this type can have a value of either true or false. Boolean variables can be used wherever boolean expressions can be used. Thus, given a boolean variable called done, the following statements are equivalent: if (done == true) {. . .} if (done) {. . .} Boolean expressions take a negation operator: !. They can be combined using AND (&&) and OR (||) operators as follows (don’t try this at home; this code is for demonstration purposes only): char c = 'c'; if ((c >= 'a' && !(c > 'z')) || (!(c < 'A') && (c <= 'Z'))) that character value is a letter
// tests
WRAPPER CLASSES Each of the simple data types has an associated wrapper class, which wraps it in an object. This technique accomplishes two goals. First, it prevents direct access to the data item itself, giving the programmer more control over who can modify the item’s value. More importantly, the wrapper classes that come with the Java Developer’s Kit provide the most common services needed for creating, inspecting, and manipulating items of that type. For example, the four wrapper classes for the numeric types—Integer, Long, Float, and Double—offer transformation of values among ints, longs, floats, and doubles. In addition, each numeric wrapper class implements conversion between its type and String objects. Finally, the Integer and Long classes have methods—parseInt() and parseLong() respectively—that are useful for parsing user input. Table A.5 summarizes the numeric wrapper classes and their most useful methods. In the space available, I am only able to give you an idea of what’s available from these classes by listing their method names, and not all their combinations of arguments and return values. For details of how to invoke these methods, see the Java API documentation. Table A.5The Numeric Wrapper Classes and Their Most Common Methods.
Methods
Classes
Description
intValue(), longValue(), floatValue(), doubleValue()
Integer, Long, Float, Double
Returns the value of the object’s data item converted to a simple data item
toString()
Integer, Long, Float, Double
Returns the value of the object’s data item in string representation, enclosed in a String object
valueOf()
Integer, Long, Float, Double
Converts a String object with a numeric value to a numeric object
parseInt(), parseLong()
Integer, Long
Converts a String object to a simple numeric data item after testing for valid numeric value
The Boolean wrapper class has a toString() method as well, which returns either true or false. Use of this method will ensure that two String objects representing boolean values never fail to match because of case discrepancies. There is also a Character wrapper class, which provides a number of useful methods for inspecting character values, as well as for performing common conversions. Table A.6 lists the Character class’s methods. Table A.6Character Class Methods.
Method
Description
digit()
Converts char value to an int
forDigit()
Converts int value to a char
isDigit()
Tests that char value is between ‘0’ and ‘9’
isLowerCase()
Tests that char value is between ‘a’ and ‘z’
isSpace()
Tests that char value is a space, tab, or newline
isUpperCase()
Tests that char value is between ‘A’ and ‘Z’
toLowerCase()
Converts char value to corresponding lowercase value
toUpperCase()
Converts char value to corresponding uppercase value
charValue()
Returns Character object value as simple char item
toString
Returns Character object value as String object
STRING AND STRINGBUFFER OBJECTS Java has no simple data type for representing character strings. It has no direct equivalent of the C language’s null-terminated character array. Instead, it provides two classes that bundle up character arrays with the methods needed to process those arrays as strings. These classes are called String and StringBuffer. The String class is used for constant strings, while StringBuffer is used for dynamic strings.
The String Class The simplest way to create a String object is as a constant character string enclosed in double quotation marks. The following code snippet shows how to create a String constant, declare a String variable, and assign the constant’s value to it: String s = "This is a String object."; The String class offers numerous methods for creating, inspecting, and manipulating String objects. I’ll show you a few that I use frequently, but to see the full range of String methods, refer to the API documentation. I commonly inspect String objects to find out either where a specified character value occurs within the String, or which character value occupies a specific position within the String. For example, I might want to extract the character value following the first occurrence of a hyphen (“-”) within the string. To do this, I could use the following code: String s; char c; int i = s.indexOf('-'); if (i > 0) c = s.charAt(i + 1); There are numerous ways to copy the value of a String object, or a subset of its value, depending on the desired destination. Here are a few examples of copying String object values: String source = "This is the source String."; String dest1(source); // creates a new String object whose value // equals the value of source String dest2(source.substring(8,9)); // creates a new String whose value // is "is" (index 8-9 of source) byte[] ba = new byte[10]; source.getBytes(8, 17, ba, 0); // copies ten bytes starting from index // 8 of source, to ba, starting at index 0 The String class has a concatenation operator, +, making concatenation of String variables and constants extremely easy. You can even combine the concatenation and assignment operators to append String objects, as in the following example: String s = "prefix"; s += " suffix"; // new value is "prefix suffix"
The StringBuffer Class The StringBuffer class offers lots of methods for appending, inserting, and modifying the value of StringBuffer objects. Once again, I’ll just give a few examples: StringBuffer sb = new StringBuffer("initial value"); sb.append(" with a tail"); // new value "initial value with a tail" sb.insert(8, "changed "); // new value "initial changed value with a tail" sb.setCharAt(8, ' '); // new value "initial changed value with a tail" ARRAYS Arrays in Java are first class objects. They are created with the new keyword. They have an integer variable, length, indicating the size of the array, which the array object stores separately from the array data itself. There is also a special method, arraycopy, for copying values between arrays of the same type. FLOW CONTROL The flow control techniques in Java are very similar to those in C. One difference is that conditional expressions must be of boolean type. In other words, you can’t test a non-boolean variable, with the implied casting of zero and null to false, and non-zero, non-null values to true. Thus, you would use the following code to initialize a String and substitute in a command line argument if one was entered: String recipient = "World"; if (args.length > 0) recipient = args[0]; For a simple two-way choice, you have a couple of options. You can use if/else notation, or the conditional operator, which is composed of a question mark and colon (“? :”). The conditional operator takes three expressions. The first must be boolean, while the second and third must be of the same type as each other. The boolean expression is evaluated. If it equals true, the conditional expression returns its second expression. If the boolean expression equals false, the conditional expression returns its third expression. Thus, the following two snippets are equivalent: if (args.length > 0) recipient = args[0]; else recipient = "World"; recipient = args.length > 0 ? args[0] : "World"; If the choice among multiple alternate paths depends upon the value of a single variable, you can use the switch statement, which is the same in Java as in C. Listing A.3 outlines the use of the switch statement. Listing A.3 The switch statement. int i; switch(i) { case(0): statement; case(1): { statement; statement; break; } default: statement; }
For controlling simple repetition, Java uses the same while and do/while statements as C. The while statement tests the truth of its controlling expression before executing the body of the loop. Listing A.4 shows the structure of the while statement. Listing A.4 The while statement. boolean done = false; int rc = statement returning an int; if (rc == -1) done = true; while (!done) { body of loop; } The do/while statement is used when the body of the loop sets the value to be tested in the while condition. This requires the body to be executed at least once. Listing A.5 shows the guts of the do/while loop. Listing A.5 The do/while loop. Boolean done = false; do { statement; if (. . .) done = true; } while (!done); For controlling more complex repetition, Java uses a for statement. The for loop has two parts: a head and a body. The head specifies the three elements of loop control: initializing variables, testing the termination condition, and incrementing variables between iterations. The for loop is most commonly used to step through arrays, but with a little ingenuity can be used to control almost anything. Listing A.6 shows a for loop whose body operates on each successive command line argument. Listing A.6 The for loop. For (int i = 0; i < args.length; i++) { statement that references args[i]; } The last flow control statement I’ll show you is the return statement. return is used to return control to the method that called the current method. Optionally, it can also return a value to the calling method. This value’s type must match the current method’s declared type. The following snippet might appear in a method that returns an int value: if (OperationSuccessful) return 1; else return -1; CLASSES AND METHODS At the beginning of this tutorial, I emphasized that the only unit of expression in Java is the class. Classes are composed of data elements and methods. Methods are the user-defined operations that can be performed on a class (or on objects of that class). In other words, all the code that does anything is implemented as methods. Methods are defined with a series of modifiers, followed by the method name, followed by the list of arguments accepted by the method (enclosed in parentheses), followed by the body of code that implements the method (enclosed in curly brackets). Remember the definition of the main method in HelloWorld.java: public static void main(String[] args) {. . .} Let’s look at the method modifiers, going from right to left. In HelloWorld.java, we assigned a type of void to main. A method can be assigned any data type recognized by the Java program. These include the Java language simple data types, classes defined in the
JDK, as well as any classes defined in the Java program itself. A method’s type precedes its name in the method’s definition. Thus, the interface for a method that returns an int, such as Integer.parseInt, is defined as follows: public static int parseInt(String s); A method call can be placed anywhere that any expression of the same type would be placed. Thus, I can declare an int variable, and assign it a value as follows: String s; int i = parseInt(s); Variables can be declared either inside or outside of methods. Variables that are declared inside a method are local to the method and can only be referenced within it. Variables declared outside of any method can be referenced from anywhere in the class. Earlier, I remarked that main is declared as a static method, meaning that it belongs to the class, rather to any particular instance. A method declared without the static modifier belongs to an instance. Similarly, class variables can be declared as either static or instance variables. The main method’s first modifier, public, indicates the access that the HelloWorld class allows to its main method. Not only methods can be given access modifiers—classes and variables can, too. Table A.7 lists the Java access modifiers and their meanings. Table A.7The Java Access Modifiers.
Access Name
Applies To
Description
public
classes, methods, variables
Can be accessed from anywhere by anyone
private
methods, variables
Can be accessed only from within the class in which the method or variable is declared
protected
methods, variables
Can be accessed from within the same class or its subclasses, even if the subclass is declared in a different package from its superclass
(unspecified)
classes, methods, variables
Can be accessed only from within the same package in which the class is declared
PACKAGES The programs in the JDK are grouped into packages. Packages are a means of organizing large groups of programs by project, by the department and organization where the code originated, or by any other useful classification. For example, the wrapper classes we discussed earlier are defined in a package called java.lang. This tells the Java interpreter where to look for their definition. When the interpreter encounters a reference to one of the wrapper classes, it looks for a directory path java\lang (or java/lang on Unix), starting from one of the directories named in the classpath environment variable. You can refer to classes that belong to packages in three ways. First, you can call them by their fully qualified package names. Second, you can place an import statement at the beginning of the program naming the class you intend to reference in the program. Third, you can place an import statement at the beginning of the program with a wildcard in place of the classname you intend to reference. This last method allows you to reference any class in the imported package by classname only. Thus, the following three snippets are equivalent: int i = java.lang.Integer.parseInt(s);
// fully qualified class name
import java.lang.Integer; int i = Integer.parseInt(s);
// specific class imported
import java.lang.*; int i = Integer.parseInt(s);
// entire package imported
In reality, java.lang is implicitly imported by the Java compiler. You can reference any class in the java.lang package without either qualifying its name or importing its package.
APPLET PROGRAMMING Armed with our knowledge of Java syntax, we’re ready to try programming an applet. After all, you’re reading this book to start writing multiplayer games for the Internet. It’s applets, not applications, that you’re here to learn. To teach you the basics of applet programming, I’ve distilled Neil’s applet tutorial from the Java Programming EXplorer. We’ll start by writing an extremely simple applet, and then work through the steps of embedding it in an HTML page and running it under a World Wide Web browser. After that, we’ll look more closely at programming applets themselves. We’ll discuss the responsibilities of an applet. We’ll see that the browser has expectations of an applet, and that much of applet programming consists of deciding how to fulfill those expectations. Finally, I’ll introduce an “applet idiom,” which you can use as a template for your own applets. The applet idiom will also serve as a brief introduction to thread programming in Java. As always, our first foray into applet programming will be to write a HelloWorld program. We’ll actually write a series of them, so we’ll number them as we go. Listing A.7 is about the simplest applet you could possibly write. Listing A.7 HelloWorldApplet01.java. import java.awt.*; import java.applet.*; public class HelloWorldApplet01 extends Applet { public void paint(Graphics g) { g.drawString("Hello, World", 160, 70); } } As with the Java applications we wrote earlier, you can compile this applet by going to the directory where its source file is stored and entering the following at the command-line prompt: > javac HelloWorldApplet01.java To run this applet under a Web browser, we need to embed it in an HTML page, which will instruct the browser in how to run the applet. Listing A.8 shows the HTML code needed to run HelloWorldApplet01. Listing A.8 HelloWorldApplet01.html. This HTML file consists of a single <APPLET> tag. The tag has three parameters: a CODE field, which tells the browser which Java class file to run, and WIDTH and HEIGHT fields, which tell the browser the size of the screen area to allocate to the applet. You can open HelloWorldApplet01.html in Netscape to view the applet, or you can use appletviewer, the lightweight browser that comes with the JDK. Appletviewer is so lightweight, in fact, that all it knows how to do is scan HTML files for the <APPLET> tag, opening each applet in its own window. For debugging applets, you will find appletviewer much more convenient to use than any of the full-function browsers. To run appletviewer, go to the directory where the class and HTML files are and enter: > appletviewer HelloWorldApplet01.html Figure A.3 shows the HelloWorldApplet01 applet running under appletviewer.
Figure A.3 HelloWorldApplet01 running under appletviewer. WHAT’S IN THE APPLET? After entering an applet by rote, compiling it, and using a browser to run it, you might want to know how the whole business works. You could start by comparing the application and applet versions of HelloWorld. The most obvious difference is that in place of a main method, HelloWorldApplet01.java has paint. There is no main because an applet is not a program running in its own right, in control of itself. It is a collection of methods that are invoked by the browser program. The browser decides which applet methods to call and when. When the browser encounters the <APPLET> tag in an HTML page, it allocates screen space based on the values of the width and height arguments, and calls the applet’s paint method, which the applet uses to fill the allocated space. The browser passes one argument, a Graphics object, to paint. Graphics objects offer methods that facilitate drawing into the applet’s screen area. Throughout this book, you’ll see examples of using the Graphics object to paint and draw. You’ll be studying its API documentation closely. In our case, we used Graphics’ drawString method to draw the HelloWorld greeting. In addition to the text of the string to be drawn, drawString takes two arguments, indicating where to draw the string. The string will be drawn starting at position 160, 70 (counting from the upper-left corner) on the 400 × 150 grid allocated by the browser for the applet. The class definition for HelloWorldApplet01 says that it “extends” Applet. This refers to the Applet class, which is defined in the JDK. The Applet class is the parent of all other applets. It provides a default implementation of every method that the browser might call. If the applet programmer writes her own implementation of these methods, the browser will take those in place of the default behavior. When a class bases itself on another class, taking its behavior as a starting point, it is said to inherit from its parent class. When it selectively replaces elements of default behavior, it is said to override the default behavior. The extends keyword indicates that HelloWorldApplet01 inherits from Applet. It overrides Applet’s paint method. LET’S BE FLEXIBLE An applet that draws a single, fixed string is of limited value. We can enhance it by letting the HTML page administrator set the value of the string to be drawn. To do this, we need to make a few changes to our applet, and to its HTML code. Listing A.9 shows our new, improved applet. Listing A.9 HelloWorldApplet02.java. import java.awt.*; import java.applet.*; public class HelloWorldApplet02 extends Applet { String string; int xpos; int ypos; public void init() { string = getParameter("GREETING"); xpos = 160; ypos = 70; } public void paint(Graphics g) { g.drawString(string, xpos, ypos); } } HelloWorldApplet02 does two things we haven’t seen before. First, it provides an init method. When the browser loads an applet, the first method it calls is the applet’s init method. Remember that the Applet class implements a default init method. But by
supplying your own and overriding the default, you can take care of initialization tasks. Because the browser calls the paint method repeatedly, whenever it (the browser) judges appropriate, paint is not the place for code that should be performed only once. Like paint, init must be declared public so that the browser has access to it. To get the greeting text, init calls the getParameter method, passing it the name of the parameter whose value it wants. But where does the parameter come from? The HTML page that described the applet. You can pass information from an HTML page to an applet by using the tag. The tag is placed in the HTML file between the <APPLET> and tags. It has two fields: NAME names the parameter to be passed, and VALUE supplies its value. The values for both fields are enclosed within quotation marks. You can have as many tags as you want, as long as their NAME fields are distinct. Listing A.10 shows the HTML file for HelloWorldApplet02, with its tag. Listing A.10 HelloWorldApplet02.html. WE WANT SOME ACTION The first two versions of our HelloWorld applet are, admittedly, pretty boring. Let’s take a giant step forward and add action to our applet. Listing A.11 is a version of HelloWorld, in which the text scrolls right to left across the screen. Listing A.11 HelloWorldApplet03.java. import java.awt.*; import java.applet.*; public class HelloWorldApplet03 extends Applet implements Runnable { Thread workThread = null; String string; int xpos; int ypos; int leftedgepos; int pause; public void paint(Graphics g) { if (--xpos < leftedgepos) xpos = size().width; g.drawString(string, xpos, ypos); } public void init() { string = getParameter("GREETING"); xpos = 160; ypos = 70; leftedgepos = -100; pause = 100; } public void start() { workThread = new Thread(this); workThread.start(); } public void run() { while (true) { repaint();
try { Thread.sleep(pause); } catch (InterruptedException e) {} } } public void stop() { if (workThread != null) { workThread.stop(); } } } Lots to talk about here. We’ll begin with the start and stop methods. We know that the browser program controls the applet, starting and redrawing it as it sees fit. It also activates and deactivates the applet at will—for example, when the applet is scrolled in and out of the visible page. To do this, the browser calls start and stop. Now for the hard stuff. What’s all this talk of threads scattered through the program? For that matter, what is a thread? We briefly touched on threads earlier in this appendix. Now, I’d like to expand our discussion. A thread (also known as a lightweight process) is a piece of code that runs independently within an application. Until now, we haven’t needed to create one because our earlier applets were static. They drew their String and returned control to the browser. But this latest version does something. It loops, continuously redrawing the screen area as the text scrolls right to left. Left to itself, the applet would take control from the browser and never surrender it, blocking other applets from running, and preventing the user from interacting with the browser. By running the applet in its own thread, we allow everyone a crack at the CPU. To have the applet run in its own thread, we need to do a few things: • The applet must implement the Runnable interface. The Java keyword interface is a contract obligating a class to implement methods conforming to signatures defined in the interface code. In our case, Runnable requires any program implementing it to define a method called run. Any time a thread is started, it calls its own run method. • Create and start the thread. This happens in the applet’s start method. There the applet declares and creates a Thread variable. It then calls the Thread’s start method, which will call run. • Implement the run method. To draw the applet’s screen area, we call the repaint method. The repaint method clears the screen area and then calls paint to refresh it. After repainting, run calls a method called sleep, which pauses the applet in order to slow the text scrolling. • Implement the paint method. Here we reposition the String before drawing it to the screen. NEIL’S APPLET IDIOM In his applet tutorial in the EXplorer book, Neil introduced a piece of base code that is so useful that I must share it with you here. He calls it his applet idiom. It generalizes our last program, providing a template that you can use to jump-start your own applets. You can see the applet idiom in Listing A.12. Most of it will look familiar to you. I’ll explain the stuff that doesn’t. Listing A.12 AppletIdiom.java. import java.awt.*; import java.applet.*; public class AppletIdiom extends Applet implements Runnable { Thread workThread = null; int sleeptime = 100; public void init() { // get parameters and set up variables }
public void start() { workThread = new Thread(this); workThread.start(); } public void run() { while (true) { // do some work e.g. repaint // then sleep for a useful amount of time try {Thread.sleep(sleeptime);} catch (InterruptedException e){ } } } public void stop() { if (workThread != null) { workThread.stop(); } } public String getParam(String p, String d) { String s = getParameter(p); return s == null ? d : s; } public int getParam(String p, int d) { try { d = Integer.parseInt(getParameter(p)); } catch (NumberFormatException e) {} return d; } } The only unfamiliar parts of the applet idiom should be the two methods called getParam. One takes two String arguments and returns a String, while the other takes a String and an int, and returns an int. You should call these methods from the rest of the program instead of getParameter. They allow you to supply a default value for the parameter in the event that the HTML file has no matching tag. In addition, for integer parameters, getParam takes care of the conversion from String representation to int. It does this by using the parseInt method from the Integer wrapper class.
USER INTERFACE PROGRAMMING Now that you know how to create a visual space in Java, you probably want to start filling it with stuff. In the next few pages, I’ll outline the basics of user interface programming using the JDK. It really will be just an outline of the basics. However, you’ll have enough to get going as long as you come away from this section knowing that there are four steps to Java UI programming: 1. Create a container for your interface. As its name implies, a container is where you put all the other visual elements of your program. An Applet object is a container used in applet programs. In Java applications, the container is a Frame object, which opens an empty window. We’ll create a Frame in the next example. 2. Decide what components you want in your container. The JDK offers a full range of visual components. We’ll see a few of them in this appendix. 3. Decide how you want your components laid out. Java offers several layout management services. Again, you’ll only see a couple of them in this appendix. 4. Decide which user events you want to handle. Java implements default behavior for every user event associated with every component, so you just have to override the events you want to intercept. GoodbyeButton.java, shown in Listing A.13, illustrates these four steps. Listing A.13 GoodbyeButton.java.
import java.awt.*; class GoodbyeButtonFrame extends Frame { public GoodbyeButtonFrame() { super("Goodbye Button"); setLayout(new FlowLayout(FlowLayout.CENTER, 50, 25)); add(new Button("Goodbye")); resize(200, 100); show(); } public boolean action(Event e, Object o) { if (e.target instanceof Button) { String s = (String) o; if (o.equals("Goodbye")) System.exit(0); } return true; } } public class GoodbyeButton { public static void main(String args[]) { Frame f = new GoodbyeButtonFrame(); } } This program is an application. The only thing that happens in the main method is to create a GoodbyeButtonFrame variable. So main simply creates a container, which knows how to fill itself. Let’s look inside that container. GoodbyeButtonFrame’s constructor consists of five lines of code. We’ll analyze each of them. First super("Goodbye Button"); calls the constructor for GoodbyeButtonFrame’s superclass (Frame), passing it a String to use as the window’s title. The next statement setLayout(new FlowLayout(FlowLayout.CENTER, 50, 25)); takes care of Step 2 from our UI programming checklist. It tells Java which set of layout services it wants to apply to the current Frame. Each set of layout services is administered by a LayoutManager object. Study the LayoutManager classes well. They greatly simplify the complicated tasks of placement and resizing. In our example, the FlowLayout class implements a simple, left-toright placement of elements in the window. If there’s no room for the new element in the current row, FlowLayout bumps it to the next row. The three arguments to the FlowLayout constructor tell it how to align the elements it places within the row and how much space to leave around each element. Next, we have: add(new Button("Goodbye")); The add method places a Button object, with “Goodbye” written on it, in the Frame container, accomplishing Step 3 from the checklist. Notice that we don’t have to declare a Button variable. We can just pass the Button, fresh from its constructor, straight to the Frame. Moving right along, we control the size of the GoodbyeButtonFrame with the line resize(200, 100); and show();
makes the window visible. Figure A.4 shows the results.
Figure A.4 The GoodbyeButton window. We’ve only covered Steps 1 through 3 of the user interface checklist. Step 4 demands a method of its own. Take a look at the following snippet: public boolean action(Event e, Object o) { if (e.target instanceof Button) { String s = (String) o; if (o.equals("Goodbye")) System.exit(0); } return true; } All event handling code in Java follows this pattern. The action method accepts two arguments, an Event object and an Object object (Object is the superclass of all superclasses). By testing the values of these two objects, you can determine what the user did and respond. In our example, we test the identity of the Event object’s target attribute, using the instanceof keyword. If the target turns out to be a Button, we perform a second test. For Button targets, the Object argument is a String whose value equals the Button’s text. So if we get a match, we end the program calling the System.exit(0) method. Event handling methods return a boolean value. Java uses this value to decide whether to run its own default behavior for whatever event has occurred. By returning true, the method is saying, “I’ve handled this one. You don’t need to.” Were the method to decide that it’s not equipped to handle an event, or that it wants the default behavior to run after application code, it would return false, giving a green light to the default behavior. In our example, we want to forestall any behavior other than the call to exit, so we always return true. Some event handlers are more specialized than the generic action method. For example, mouse events generate mouseUp, mouseDrag, and mouseDown events. Listing A.14 shows a very simple applet for drawing lines. Listing A.14 LineDrawer.java. import java.applet.*; import java.awt.*; public class LineDrawer extends Applet { private int startX = 0; private int startY = 0; public boolean mouseDown(Event e, int x, int y) { startX = x; startY = y; return true; } public boolean mouseUp(Event e, int x, int y) { Graphics g = getGraphics(); g.drawLine(startX, startY, x, y); return true; } }
In addition to the Event argument, the mouseUp and mouseDown methods accept the coordinates where the event took place. LineDrawer.java saves this information on mouseDown, using it to call the Graphics object’s drawLine method on mouseUp. In Figure A.5, you can see a masterpiece I created using the LineDrawer applet.
Figure A.5 A LineDrawer applet creation. Keyboard events have their own set of event methods: keyDown and keyUp. Listing A.15 shows a keyDown method used in conjunction with a subclass of TextField, the JDK component used for single-line text entry. In NumTextApplet, we use keyDown to restrict entry to numeric values. Listing A.15 NumTextApplet.java. import java.awt.*; import java.applet.*; class NumTextField extends TextField { NumTextField() { super(10); } public boolean keyDown(Event e, int key) { if (Character.isDigit((char) key)) return false; return true; } } public class NumTextApplet extends Applet { private NumTextField ntf; public void init() { add(new Label("Numbers only: ")); ntf = new NumTextField(); add(ntf); } } The keyboard event handlers take, as their second argument, an int whose value maps to a key on the keyboard. By casting that value to a char, we can examine it using the Character wrapper class’s access methods. Voilà, a simple numerics-only test. Too simple, as it turns out. If you actually try this applet, you’ll see that not only does it screen out alphabetic characters, it rejects all the Home, End, and cursor arrow keys as well! As they say, I’ll leave the solution to this problem as an exercise for the reader. In fact, at this point the rest of your Java programming career becomes an exercise for the reader, because this is where I step off. Here’s what we’ve learned: • The qualities that make Java so well-suited to Internet game programming, specifically that it is network-savvy, architecture neutral, and multithreaded. • The primitive Java data types, logic flow structures, and method declaration conventions. • The scaffolding of Java applets. • Java’s approach to user interface component composition and layout. • Java’s approach to handling user events. • The JDK’s tools for compiling and running Java applications and applets.
HOMEWORK As you start coding in Java, you’ll quickly want to expand your knowledge to topics such as exception handling and data structures. Follow that up with a full command of all the graphical components offered by the JDK. Add a little network programming and the world will beat a path to your Web site!
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
APPENDIX B SOME USEFUL RESOURCES STEVE SIMKIN
T
his appendix has two sections. The first section lists Java-related resources, general programming tools, Internet clients for
various protocols, and archives of software, image, and sound files. The second section is for fun. It lists lots of Internet games and gaming resources of all types. Such a list could never be comprehensive, but this one will keep you busy for a long, long time.
JAVA RESOURCES I can’t begin a section on Java resources without mentioning the ultimate source of all things Java. Before you go anywhere else, stop by Javasoft’s Java home page at http://www.javasoft.com. This is where you came to get the JDK. This site also has the Java language specification and JDK API documentation. As I write this appendix, Sun is beginning to release a series of APIs for specific domains, such as database access, commerce, and encryption. You can find descriptions of them all here. You’ll also find white papers and FAQ sheets on Java-related topics. There’s also a page for press releases. Recently, Sun initiated a moderated discussion forum on a crucial topic for the future of Java: Sun’s Java Beans versus Microsoft’s ActiveX. Follow this one closely. APPLET REPOSITORIES It’s always helpful to have some guidance when you are looking for good applets. The sources I’ve listed here should help you find the best applets in cyberspace and answer any Java-related questions that might be brewing in your head. Gamelan: Earthweb’s Java Directory http://www.gamelan.com Be sure to make this a priority stop on your Java tour of the Web. Your first visit to Gamelan may make you gasp at the breadth of Java activity that has so quickly filled the Web. Gamelan categorizes applets and recommends some favorite ones. It can also keep you abreast of developments in the larger Java world. Java Applet Rating Service http://www.jars.com Mission of the Java Applet Rating Services, or JARS for short, is much narrower than Gamelan’s. It is almost purely a rating service, with a little bit of discussion thrown in. But what a fantastic rating service! Of course, I have to say that because Neil is one of their judges. The JARS folks do a great job of staying on top of who is doing the most interesting things in Java. Draw inspiration from their top one percent and five percent. They also keep a repository of source code for you to take home. Toronto Java Users Group http://www.jug.org
I must put in a plug for my local Java Users Group. The Toronto JUG site offers a class archive, a class swapping arrangement, and some incredibly enthusiastic volunteers, dedicated to spreading the good news about Java. If you have specific Java questions, try addressing them to [email protected]. You’ll likely get a reliable answer from Paul, Geoff, Greg, or one of the other helpful souls at JUG. JAVA NEWS With Java developments coming so frequently, it’s good to know that there are people devoted to keeping up with the news and sharing it with you. I find the following ‘zines and newsletters unfailingly interesting. I only wish I had time to read them cover to cover! JavaWorld http://www.javaworld.com From the people at IDG, who also bring you NetscapeWorld and SunWorld Online, comes JavaWorld, a full-blown, magazine-style monthly. Here you’ll find articles on the nuts and bolts of Java programming at all levels, a section called “Under the Hood” that demystifies Java internals, reviews of Java-related books and products, and handy tips. Digital Espresso http://www.io.org/~mentor/jnIndex.html This weekly newsletter is full of news items, summaries of important threads in Java discussion groups, bugs and warnings reports, and product reviews. I frequently start my Internet sessions with a five-minute glance at Digital Espresso. It’s a quick, light read, but I always come away with worthwhile information. Figure B.1 shows a typical Digital Espresso page.
Figure B.1 A typical issue of Digital Espresso. Java Language Newsgroup news://comp.lang.java.programmer Check out this site to find out what’s on the mind of your fellow Java programmers. I don’t follow it very often any more. The same beginners’ questions tend to recur constantly. But if you are interested in those questions or you’re looking for a place to post your own, comp.lang.java could be the place for you. INTERACTIVE DEVELOPMENT ENVIRONMENTS IDEs are probably the single most important area of Java software development. If we are to develop industrial-scale applications in Java, we will need industrial-strength IDEs in which to do it. A good IDE has three qualities: a comfortable, easily configurable editor; a way to launch compilers and programs from inside the environment; and extensive debugging capabilities. None of the environments listed here provide everything the developer needs, but they are certainly a good start. Symantec Java Central http://cafe.symantec.com/javacentral/ index.html Borland Internet Tools Home Page http://www.borland.com/internet
These companies sell the two major commercial IDEs on the market. Symantec’s Café and Borland’s Latte are both based on successful IDEs for C++ development. They both provide the basic editing, compilation, and debugging services needed for interactive development. In addition, they have facilities for project management. Both Borland and Symantec have their own compilers, which run much faster than the javac compiler that comes with the JDK (which was written in Java). Jfactory http://www2.roguewave.com/products/jfactory/jfactory.html Microsoft Visual J++ http://www.microsoft.com/visualj As I write, these environments are available in beta versions. I haven’t seen them, but I’ve heard good things about both of them. They should be worth a try. Java WebIDE http://www.chamisplace.com/prog/javaide This free IDE looks promising for small scale development. It puts an attractive graphical interface on the JDK tools to facilitate editing and compilation in a simple, color-coded environment. Kalimantan http://www.real-time.com/java/kalimantan/index.html So far, the Kalimantan suite (also free) consists of a couple of tools that provide a direct visual interface to the JDK tools. One is an inspector for object member variables, while the second is a debugger. WORLD WIDE WEB BROWSERS I’m including references to the two leading Web browsers in the Java Resources section for a very simple reason. Applets’ performance on the Web is largely a function of the browser used to run them. The relative merits of these browsers is a major topic of discussion in every Java forum these days. Even the Europa game’s mailing list is frequently taken up with Netscape versus Explorer debate, well beyond any immediate relevance to playing Europa. Netscape Navigator http://www.netscape.com/comprod/mirror Internet Explorer http://www.microsoft.com/ie/ie.htm Both of these browsers are free to individuals, and easy to download and install. They both get you up and surfing quickly. There are heavy ideological and symbolic overtones to the competition between them. Rather than take a stand, I’ll step aside and refer you to the plentiful material in the Java and Internet news resources mentioned elsewhere in this appendix.
GENERAL PROGRAMMING RESOURCES The tools in this section are useful to any programmer, regardless of language. Some of them, like the Programmer’s File Editor, can boost your productivity. Others, such as Pretty Good Privacy, should simply be in the repertoire of things you know about. Yet others are archives you can turn to for software, images, sounds, and other files. PROGRAMMING TOOLS Let’s begin with my choice for the best sites for programming tools.
Programmer’s File Editor Home Page http://www.lancs.ac.uk/people/cpaap/pfe You can think of this editor as a lightweight IDE. Not only is it a highly customizable text editor, but you can launch compiles and programs from it, as well. It can open multiple files simultaneously and lets you define template files (such as the applet idiom from Appendix A), which can be imported into program files. Windows NT Unix Utilities ftp://ftp.cc.utexas.edu/microlib/nt/gnu Anyone coming to a Windows/DOS environment from Unix inevitably feels the lack of utilities such as grep and tar. Those wonderful folks from GNU have filled the gap with software available from this site. As a minimum, download and install the following files: tar.exe, win32gnu.dll, and gnu-bin.tar.Z. SOFTWARE ARCHIVES Of course, our tour of resources wouldn’t be complete without some mention of the best sites with the hottest software. It’s a good thing that most ISPs provide a flat rate for online time because you’re sure to find yourself wandering around the following sites for hours. OAK Software Repository http://www.acs.oakland.edu/oak.html The OAK Software Repository is a public service of Oakland University’s Office of Computer and Information Services. I’m not going to recommend that you download anything specific from OAK. I just wanted to make sure you knew about it. Drop in and take a look around. Use the search engine so you can fully appreciate the size and richness of this repository. If there’s a useful piece of freeware or shareware you need, chances are you can find it on OAK. The Cygnus Support FTP Server ftp://ftp.cygnus.com This is the master GNU FTP site. GNU’s slogan is: “We make free software affordable.” Like OAK, there is a wealth of software here for the taking. The difference is that everything here comes from Cygnus Support, a company dedicated to the creation of truly fine software. To see all that a free IDE can be, download and install the GNU C/C++ environment. For instructions, see the readme. html file at ftp://ftp.cygnus.com/pub/gnu-win32/latest or at http://www.cygnus.com/misc/gnu-win32. IMAGE AND SOUND ARCHIVES As you write games, you’ll find that you develop a constant need for images and sounds. Personally, I’m not up to creating these files for myself. Luckily, other people just love to create them, and still others love to collect them. Here are a few places you can go for pictures and sounds to use in your animations. GIF Animation on the WWW http://member.aol.com/royalef/gifanim.htm This site is a model of treating a subject thoroughly and comprehensively. You can use it as a handy source of royalty-free GIF animations. But on one of your visits to this site, stay a while and do some reading. You’ll find a fascinating exposition of the history, theory, and practice of GIF animation. Figure B.2 shows the welcome page from GIF Animation on the WWW.
Figure B.2 Welcome to the WWW world of GIF animation. Rob’s Multimedia Lab Au Directory http://www.acm.uiuc.edu/rml/Au Like the GIF animation site, you can use Rob’s Media Lab as a great file archive, there for the plundering. Rob has collected loads of sound effects, quotes, cartoons, and computer noises for you to use. When you’re done gawking at all Rob has to offer, go up one level in the directory structure (http://www.acm.uiuc.edu/rml) to find a whole world dedicated to every aspect of multimedia development. Washington University’s Public Archive ftp://wuarchive.wustl.edu/multimedia You’ll find lots of images at this site, stored in the /images directory. While you’re here, you might want to hop over to the / languages and /system directories for a look at their collection of compilers and operating system utilities. About the Cards http://www.waste.org/~oxymoron/cards/aboutcards.html By merit of the meticulous, painstaking effort it must have taken to create this site, I am awarding it a special mention. Here you will find a collection of playing cards, laboriously and meticulously created in GIF format, free for the taking. The anonymous creator of this site brags that unlike the cards in Microsoft Solitaire, the symbols on these cards are perfectly symmetrical and distributed. This is one of those sites that impresses you with the talent and generosity of the Web community. Especially when you have to write a chapter about programming card games in Java! Figure B.3 shows a few samples of Oxymoron’s work.
Figure B.3 From Oxymoron’s card collection. ENCRYPTION Many strategy games involve simultaneous moves by more than one player. As I mentioned in Chapter 1, adapting games of this type to Internet play can be a challenge. One solution to this problem is for each player to publish his or her turns in an encrypted form, and then reveal the key that solves the encryption once all players have made their turns. Visit the sites I list here to learn how you can use encryption in your own games. PGP Home Page http://www.pgp.com You should be able to fulfill all your encryption needs here. Unfortunately, I have not had good luck connecting to this site. If you get stuck, be sure to check out the following site. Hassop Cottage PGP Page http://www.netlink.co.uk/users/hassop/pgp.html
I’ve heard talk of a Java/PGP implementation, or at least an API. To follow the progress of this effort, take a look at Hal Finney’s PGP-compatible mail client at http://www.portal.com/~hfinney/java/pgpmail/PGPMailer.html.
INTERNET RESOURCES If you’re going to be gaming on the Net, you’ll need client programs. If you’re going to be writing Net games, you may want to keep abreast of Internet news and issues. I’ve listed a couple of useful sites in each category. Happy playing, happy reading. WINDOWS CLIENTS If you’re a Unix or Mac user, you’ve got a Telnet client built in. I guess you do in Windows, too, but the client I recommend here is just so much better than the standard one that I really want you to know about it. As for IRC, you can find a list of clients, plus everything else you could possibly want to know, in the Internet Relay Chat FAQ at http://www.kei.com/irc.html. I also got lots of results by searching for “unix irc client” and “mac irc client” on a couple of Web search engines. EWAN, Winsock Terminal Emulator http://www.lysator.liu.se/~zander/ewan.html A good tool gives a feeling of satisfaction to its user, quite apart from whatever the tool is used to accomplish. The EWAN Telnet client gave me that feeling. It has a simple, attractive interface. It allows you to open multiple windows and save bookmarks to your favorite sites. I use EWAN to access those MUDs that don’t require dedicated MUD clients. Home Page of the IRC Chat Client mIRC http://www.mirc.co.uk For ease of use and simplicity of interface, mIRC is a good complement to EWAN. It didn’t give me those goosebumps, though. Probably because I don’t spend anywhere near as much time chatting on IRC as I do in Telnet sessions. Maybe it does have something to do with what the tool helps you accomplish. INTERNET NEWS AND ISSUES As with Java, changes on the Internet happen at near light speed. The references I’ve listed here should keep you up-to-date with all the happenings in our cyberworld. c|net Online http://www.cnet.com For intelligent articles on topics of concern to denizens of the Net, c|net is the place. Last time I looked, there was a solid contribution to the growing body of “battle of the browsers” literature, which I referred to earlier. c|net has its own shareware archives, a RealAudio radio station, and, yes, game reviews. Dov Wisebrod’s Internet Law Resources http://www.io.org/~sherlock On the serious side, you may want to read about what you can and can’t do on the Net and how legislators are trying to change the rules. Dov thought up the Digital Doomsday Clock, a countdown to the end of freedom of speech on the Internet. He has a lot to say about government and the Internet, and the issues he addresses have direct implications for all of us.
GAMING RESOURCES We’ve been serious long enough. Time to have some fun. In the following section, you’ll find discussions of games and lists of links to games. But mostly, you’ll just find games. Enjoy.
SITES ABOUT GAMES If you want to learn before you play, check out the sites in this section. You’re bound to find some information of interest. WorldVillage http://www.worldvillage.com World Village is an enjoyable, family-oriented site with many areas of interest, including Gamer’s Zone (not to be confused with the Internet Gaming Zone). There you’ll find reviews of games, demos of commercial offerings, and interesting articles about various aspects of Internet gaming. The Game Cabinet http://www.gamecabinet.com This monthly gaming magazine is devoted primarily to reviewing commercial games. By now, it has accumulated a large database of game information. The “cabinet” has lots of drawers. If you reach way down into one of them, you’ll find the Web Games Terminal at http://www.gamecabinet.com/deeperDrawers/GamesOnTheNet.html, a page of links to several email and Telnet-based Internet game servers. GAMES Group Home Page http://web.cs.ualberta.ca/~games/index.html In this case, GAMES stands for Game-playing, Analytical methods, Minimax search, and Empirical Studies. Yes, you’ve reached the Computer Science department of the University of Alberta in Calgary. The folks at GAMES know how to have really serious fun. You can read a few academic publications if you like, but if you take my advice, you’ll step right up to the GAMES server. In addition to Chinook, the world champion of checkers I mentioned in Chapter 1, the GAMES computer will challenge you at chess or Othello. Web-Grognards: The Site for Wargames on the Web http://clever.net/ grognard The declared purpose of Web-Grognards is: “To provide a central location for the collection of various conflict simulation game (i.e., wargame) information, including (but not limited to) game errata, variants and reviews, and relevant FTP and Web sites.” The games covered by Web-Grognards aren’t really my style, but the site is so informative and well-organized that I thought it worth mentioning. The word Grognard, by the way, refers to one of Napoleon’s muddy old soldiers. Makes you want to jump right in, doesn’t it? Figure B.4 shows what you’ll see when you get there.
Figure B.4 The Web-Grognards welcome page. PBM And PBEM Play-By-Email seems to come in two flavors. The first uses email to play back-and-forth board games, while the second makes much more elaborate use of the medium to coordinate muliplayer, simultaneous-turn games. For this section, I chose the two finest implementations of the first style that I could find. For the second style, I chose a couple of Diplomacy sites. Playing Diplomacy by either postal mail or email is an achievement in its own right, regardless of the quality of the game itself. I chose two Diplomacy sites, which can be characterized as minimalist and maximalist. In addition, I slipped in a site where you can find a number of tools
of general use to Internet gamers, but especially to PBEMers. Richard’s PBEM Server http://eiss.erols.com/~pbmserv This server, run by Richard Rognlie, is a marvel. It provides an arena for playing tens of different games by email. It also maintains a database of the history and current board positions of games in progress so you can make sure that you’re up-to-date on your own games, as well as your friends’ games. Along with that, the server lets you send messages to all the players of a given game. Not a bad kibitzing tool for an email-based system! The server also has broadcast scripts, allowing you to issue challenges to the world. Welcome to Irony Games http://www.irony.com Although it’s not really a PBEM site, I included it here because it’s so useful to PBEM gamers. It contains a number of tools, including a graphical dice server, which is much more convenient than the elaborate protocol for the email dice server I described in Chapter 1. The other Irony tools can generate maps and scenarios for role-playing games. Clever stuff. DP: The Diplomacy Home Page http://www.csn.net/~mhand/DipPouch Even by game devotee standards, the scope of the Diplomatic Pouch is impressive. It includes a magazine with substantial articles on Diplomacy-related topics, a showcase for analysis of games in progress, and separate areas covering the FTF, postal, and email versions of the game. Email Diplomacy Step By Step http://www.sn.no/~arannest/dip/dipstep.htm For those anxious to start playing email Diplomacy, and who don’t want to wade through all the material at the Diplomatic Pouch, this site provides a seven-step checklist that will get you going as quickly as possible. No muss, no fuss, with nary a wasted word. IRC-BASED GAMES The world of IRC gaming is at once frantic and highly social. If your idea of a good time is to crack brain-teasers at high speed with your left hemisphere, while making small talk with your right, make the following site your launching pad to a career of Internet Relay Chatting. Internet Relay Chat Games http://www.cs.uregina.ca/~hoyle/Games What is it with these people at Canadian universities? Must be the cold winters. Whatever it is, Michelle Hoyle cares deeply about IRC games. Her site doesn’t just include extensive HTML pages devoted to each game. She has posted copies of every media article about them she could find. You can use her site as a gateway to the world of IRC in general, and IRC gaming in particular. TELNET-BASED GAMES You can find a good list of Telnet-based games on the Web Games Terminal page I described earlier. Here are a few that seem to be particularly popular: Free Internet Chess Server at telnet://ics.onenet.net:5000 Just choose a user ID and you’re registered. IGS Go Server at telnet://igs.net.net:6969
Enter “guest” to receive a guest pass, or enter “register” to create a registered account. MarlDOom Crossword Game Server at telnet://eel.st.usm.edu:7777 This server generates crossword games, with configurable playing boards. Great for sharpening Scrabble skills. Register using your email address to receive a logon ID and password. MUDS AND ALL THE OTHER “M” WORDS As we talked about in Chapter 1, MUDs almost took on a life of their own when their creators gave players the ability to extend the fantasy setting of the MUD itself. The following sites trace the entire development of MUD theory and practice. For a glimpse into the admittedly MUDdy future, check out the last site in this group. Welcome to MUDdom http://www.shef.ac.uk/uni/academic/I-M/is/studwork/groupe/home.html If you want to start your venture into MUD gaming with some background info, work your way through this site from the University of Sheffield. This site was created by True Believers. Not only does it review the nature and history of MUDs and all their offspring, it also speculates on the beneficial effect MUDs could have on society at large. I’ll leave you to judge where fact blends into fancy at this site, but I guess that’s what MUDs are all about. Figure B.5 shows the first page of the huge MUDdom site.
Figure B.5 Up to your eyeballs in MUD. The University of Texas MUD Gopher gopher://actlab.rtf.utexas.edu/11/MUD You’ve read all you want to in MUDdom. Now, you want some action. Make this Gopher site your first stop. It contains lots of hereand-now MUD resources. It also has a folder called “Plug In” with links to tens of MUDs that will run on Telnet clients. Choose one and jump in. Just don’t forget to come up for food once in a while. The WWW Dungeon http://www.cling.gu.se/~cl0polau/3wd/3wd.htm This is an attempt at a dungeon implemented right in Web pages. It’s a little awkward, but certainly a great first effort in the direction of visual, interactive MUDs. I wonder if graphical representation of the MUD world will actually take some of the magic out of it. It will be interesting to see whether advances in Web technology cause changes in the habits of die-hard MUDders. I wouldn’t be surprised if they ignore the whole business. President ’96 http://www.pres96.com/index.html This site may more accurately foreshadow the transition to Web-based MUDs. In this game, the “dungeon” is the White House, and you, the player, become an insider in a version of this year’s presidential campaign. Instead of trying to re-create a traditional dungeon in HTML, President ’96 uses the richness of a Web site to allow the player to jump from press conference, to back room, to ad video screening. The site’s sponsors, who include America Online, toss in news items and other developments each day to keep
things lively. This game is a long way from the efforts of twenty years ago, when programmers were thrilled to write code that modeled a few rooms with passageways between them! FAN CLUBS In Chapter 1, I described two superb fan clubs, one for chess and one for bridge. Here I’ll add a couple of sites devoted to Abalone. Abalone is played with tokens on a small hexagonal board. The objective is to push your opponent’s tokens off the edge of the board before she does it to you. The rules are few and easily described, which makes it a favorite object of Artificial Intelligence efforts. I was surprised to learn, while researching this book, that Abalone is a commercial invention. Although it seems like such a natural, like checkers, Abalone was actually created by Michel Lalet and Laurent Levi, and its copyright is held by Abalone Games Corporation. Nevertheless, several online versions are available. Here are two that you can play against through the offices of Richard’s PBEM server, which I described earlier. Richard brokers play against a third Abalone server, el-boana by Colin Springer, which is actually the machine-against-machine champ. But el-boana doesn’t have its own Web site. AI-Aba http://www.qucis.queensu.ca/home/colley/ai-aba-faq.html Another playful Canadian (hmmm), Paul Colley is the author of AI-Aba, a program that plays Abalone. To play against AI-Aba through PBMServ, choose the game called AI-Aba. Abalone Home Page http://www.cs.indiana.edu/hyplan/danlip/abalone/abalone.html This site offers a general introduction to Abalone, plus links to other Abalone resources on the Web. It was put together by Dan Lipofsky, who together with Matthias Scheutz, wrote another Abalone-playing program. To play against Dan and Matthias’s program through PBMServ, choose the game called abby. GAMES I know. You came to play, and here we’ve been debating Web browsers, pontificating about Java Beans, comparing Abalone algorithms, everything but playing. Here are a few games I like that didn’t make it into Chapter 1.
CGI-Based Games The thing about CGI-scripted games is that they’re so darned much work. Even the simple-looking Tic-Tac-Toe implementation I mention in this list requires on-the-fly HTML page generation. The creators of all the games in this list deserve a reward, or at least recognition for their effort. BU’s Interactive WWW Games http://www.bu.edu/Games This is a neat little collection. Aaron Fuegi has given us both Java and non-Java games here. The non-Java games include Tic-TacToe, Pegs, Minesweep, Nine Puzzle, and Battle Five. They all have a crisp, clean look and tolerable response time. The Java games include Battleship, JavaSweeper, and Nine Puzzle. Hangman at COMMA in Cardiff http://www.cm.cf.ac.uk/htbin/RobH/hangman?go Just to prove that the Welsh also know how to have fun, Robert Hartill has created a cute Hangman (if you can imagine such a thing). Universal Access Blackjack Server http://www.ua.com/blackjack/bj.html
This is the most configurable game site I’ve ever seen. Created for the serious blackjacker, it boasts options I have never even heard of. I stopped flicking switches after trying out the “plain” and “professional” flavors. Like the real thing, this blackjack is fast paced and addictive. You’ve been warned. Marcel’s WebMind http://einstein.et.tudelft.nl/~mvdlaan/WebMind/WM_intro.html Being a cerebral guy, I’m happy to admit my addiction to this elegant, Dutch Web version of Mastermind. Choose those colors, place those pegs. See you tomorrow.
Java Games Once again, I’ll use this space to reward a few programmers whose work impressed me. Some of them generously let you download their source code so you can see how they did it. The game of Chess http://www.kjemi.unit.no/~hhansen/chess.html Harald Hansan of Norway has put a Java user interface in front of the free gnuChess server (from our friends at Cygnus Support). He describes his work and offers you the source code. Definitely worth a look. Virtual Reality Magic Cube http://www.npac.syr.edu/users/yjchang/magic/Magic.html Seriously cool! Michael Chang takes the Rubik’s Cube to the max. You can choose the dimensions of each face of the cube and set the difficulty level of the scrambled pattern. Still too hard? Press the solve button and let the VRMC show you how it’s done. Figure B.6 shows one I didn’t even come close to solving.
Figure B.6 Can you solve this? VPRO’s Java GameServer http://www.vpro.nl/www/interaktief/java/gameserver.html This site suggests the framework for a Java game server. It describes the network and messaging services necessary to get multiple players talking to each other. You can download the source and see how its creators have used it as the basis for simple chat and whiteboard applets. Dor-Cino Web Casino http://www.dorcino.com Doron Kramarczyk’s casino features mostly non-Java games, but there is an adorable Java-based horse race that I took a fancy to.
Europa Mailing List [email protected] This is an extremely instructive mailing list for the aspiring Java games programmer. Its subject matter ranges far beyond the “Please restart the server” and “Doesn’t anybody want to play?” messages that one usually sees in lists of this sort. Europa itself is a demanding testing ground for Java-enabled Web browsers, which prompts detailed discussions of which features work properly for whom and on what platform. Beyond that, Alex Nicolau is open and articulate in his deliberations about which directions Europa should take. This mailing list is a window into the decision-making of a skilled Java game developer. To subscribe, send a message to the address listed here. The message body should consist of “subscribe europa-players”. UltraMaster Dethtris http://ariel.cobite.com/ultram/tetris/index.html Charming little Tetris gets blown out of all proportion in yet another addictive single-player Java game. This version goes up twenty levels or more, with a few surprises along the way. Figure B.7 catches me in a losing effort.
Figure B.7 UltraMaster Dethtris turns up the heat. Hot Mudslide Home Page http://www.drscc.com/mudslide/index.htm This Java-based MUD client can replace the usual Telnet client in most situations. Nothing fancy here, but a solid applet that quietly does its job. Sokoban http://www.blacksteel.com/~yossie/Sokoban This cute puzzle game’s objective is to push dollar signs off of an irregularly shaped playing area. It’s way more difficult that it looks, and I had trouble tearing myself away. Luckily, the Sokoban lets you retrace your steps to try and keep from making the same mistake twice. Well, maybe three times… Snugs’ Trackattack http://www.duke.edu/~jcpdukeu/tune.html This one wins my award for originality. It runs a “Name That Tune” style quiz, using a CD player interface that plays song fragments and challenges you to guess what they are. Internet programmers probably wouldn’t even have thought of a game like Snugs’ Trackattack until Java came along. JavaGammon Home http://www-leland.stanford.edu/~leesmith/JavaGammon.html This beautifully realized multiplayer backgammon does it all. The graphics of the game board are vivid. The contestant brokering is smooth. The sound effects are amusing. And most importantly, play proceeds quickly, with rapid updates as each player takes his turn. Well done!
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
Index 2D bitmap-based games, 53 40k MIRACLE Monthly, The (Web site), 22
A Abbysinian Prince, The, magazine, 5 Abstract class, 276 acceptDrop method, 142 Action keys, 126 Actors automated, 283–296 boundaries and, 177 caching, fixed, 148–150 collision detection and, 178–181 data, sending, 384–388 Donut, code for, 43–44 Donuts game and, 32 dropping, 142 explosions as, 42 framework and, 54–58 Java code for picking, 138 JellySquirt, 40 jumping, 140 KeyTestActor, 124 MansBestFriend, 290 MazeWars game and, 283–288 moving, 133 painting, 79–106 registration with EventManager, 119 Actor class, 63–66 ActorManager class, 66–67, 181 addDropSite method, 141 Adobe Acrobat, 23 AI (Artificial Intelligence), 21–22, 263–280 Web sites for, 22 AlphaFilter, 104 Animating locally, 399–402 APIs for networking, 452 Applet, Domination game, 336–338 Application connection topology, 306–310 Arbiter, 450-451 Artificial Intelligence. See AI. Asteroids, 31 Audio, 217–226. See also Sounds. AudioClip objects, 218 AudioClipRegistry class, 222 Auto actors, 283–296 rules for, 295–296
AutoActor class, 288–290 speed- and motion-related methods, 289 Automated players and weapons, 281–303 AWT, 311, 359
B BackdropManager interface, 105–106 code for, 105 Binary Space Partition (BSP) tree, 446–447 Bots, 11 Boggle, 12 Boundary conditions, 177 Bounding-box comparison, 179 improving, 180 Bridge game Web sites, 19 Broadcast delay, 404 BSP (Binary Space Partition) tree, 446–447 Busy signals, 351–355
C CAB files, 415 Caching, 98, 445 registered events, 369 canDrag method, 139 Card class, 157 Card game, simple, 153–158 CardGame class, 158 CardStack class, 158 Celled images, 84 CGI Connect4 script, 20 limitations for gaming, 21 Chatting between players, 357–359 Checkers, 3 Web site for, 21 Chess Play-By-Email (PBEM), 6 Play-By-Mail (PBM), 6 Web sites related to, 18 Chinook, 3, 21 Class class, 453 Classes abstract, 276 Actor, 63–66 ActorManager, 66–67, 181 AutoActor, 288–290 Card, 157 CardGame, 158 CardStack, 158 Class, 453 Clock, 159–170 ConnectionLayer, 353 CRC cards and, 59–60 DirtyRectangleManager, 186 DominationEvent, 330–331 Donut, 43, 45–46 Event, 109–110 EventInterface, 115 EventManager, 114–122 Game, 67
GameApplet, 115 GameEvent, 328–330 GameLayer, 331–332 GameMoveManager, 275–280 GamePanel, 115 GameStats, 210 GameWorks, 31–34, 54 HighScoreRecorder, 208 Hunter, 292–293, 393 Image, 82 ImageConsumer and ImageProducer, 95 Map, 430 message interaction diagrams and, 60–61 MultipleImageHandler, 92 Mystery, 298 naming, 122–123 network connection, 334–336 NewtonMovement, 173 Player, 431 RealClass, 453 RemoteActor, 388–392 ScoreManager, 194–196 Ship, 37–39 SingleImageHandler, 88 StageManager, 62–63 StatusCanvas, 351–355 WeaponGameBridge, 299–302 WrapRectBoundary, 177 Client game state, 317 Clipping graphics, 91 Clock class, 159–170 speed, 164 watchers, 159 Code. See Java code. Code libraries, 50 Coding data in source files, 326 Collision detection, 178–181 bounding-box technique for, 179 Common Gateway Interface. See CGI. CONNECT!Quick client, 416 Connect4 Web site, 20 Connection latency, 407 ConnectionLayer class, 353 notification codes, 353 Connections, external, 71–74 containsPoint method, 142 CRC cards, 59–60 createAutoActor method, 294 createDragDropManager method, 136 createEventHandler method, 119 createFadeImages method, 104 Crossword puzzles, Web site for, 23 CyberSite framework, 53
D Data transfer model, network, 313 Database, high score, 201–203 Decoding key values, 124 keys during game play, 127 modifiers, 128
special keys, 126 Decoupling from the network, 359 Dedicated client games, 16–17 Deep Blue, 263 Delivering a Java appet, 414–416 Designing a framework, 58–74 Dice servers, 8 Direction of view (DOV), 434 Dirty rectangles, 184–192 DirtyRectangleManager class, 186 Domination game, 323–348 applet, 336–338 environment, 324–326 extended, 349–371 flow of code for, 342–348 DominationEvent class, 330–331 DominationServer application, 338 Donuts game, 31–47 Doom FAQ, 4 Double X and Y coordinates, 176 Double-buffering, 145–147, 183 DOV (direction of view), 434 Drag and drop, 134–144 manager, 134 returns, 167 dragTo method, 137 Drawing graphics, 80 image, 80 Drill-down/drill-up algorithm, 110 Drop sites, 140 DropSite interface, 143 Java code for, 143 Dynamic loading, 453
E Einstein, Albert, 174 Email address for Abbysinian Prince, The, magazine, 5 dice server, 8 Fred game, 456 Error management networked games and, 313–314, 362–370 Europa Web site, 27 Event class, 109–110 Event handler methods, 108 keyDown, 124 keyUp, 124 Event handling, 107–131 Event objects, 109 Event threading, 113 EventInterface class, 115 Java code for, 119–121 EventManager class, 114–122 CRC card for, 114 Java code for, 116–118 Events keyboard, 110, 124 mouse, 137 types of, 110 Explosions, 42 External connections, 71–74
F Fading effect, 103 FAQs, 4 Fast redraw, 144 Feedback, network issues, 317 Field of view (FOV), 434 findMax method, 273 Fixed-actor caching, 148–150 Formula for movement, 173 FOV (field of view), 434 Fractal Design’s Dabbler, 34 Frameworks, 49–78 actors and, 54–58 CyberSite, 53 designing, 58–74 Gamelet, 53 GameWorks, 52–78 Min-Max alogrithm and, 272 versus code libraries, 50 Fred game, 425–456 gameplay, 427 optimizing, 445 FTP site for Go FAQ, 4 Fundamentals of Computer Graphics book, 432 Fuzzy logic, 451
G Game class, 67 Game clock, 162–164 Game design external, 349–350 internal, 350–351 Game framework, event handling, 113 Game object, 119 Game state management, 314–319 Game watching, 355–357 GameApplet, 72 class, 115 GameEvent, 366 caching, 369 class, 328–330 error-detection and, 367 GameFrame, 73 GameLayer class, 331–332 Gamelet framework, 53 GameMoveManager class, 275–280 TicTacToe and, 276 GamePanel class, 115 Games card, 153–158 Domination, 323–348 Domination, multiplayer, 349–371 Donuts, 31–47 Fred, 425–456 Iceblox, 264–268 MazeWars, 281–302 MazeWars, networked, 373–395, 397–411 Solitaire, 243–262 GameStats class, 210
GameWorks classes, 31–34, 54 framework, 52–78 image support and, 83 painting actors and stage for, 79–106 Generic server Java code, 204–206 getBestMove method, 272 GIFs, 33–37, 80 joining multiples, 36 transparency and, 37 Giftrans utility, 37 Go FAQ, 4 GoldWave sound editor, 46 Web site for, 218 Graphics. See also Images. clipping, 91 coordinate system, 81 drawing, 80 Great Bridge Links (GBL) Web site, 19
H handleEvent method, 109 Handlers, 74 Hardwick, Jonathan, 413 haveModifiers method, 128 Heuristics, 272, 280 High score server, 196–214 commands, 199 configuration, 198 database, 201–203 Java code for, 206–207 HighScoreRecorder class, 208 hnefa-tafl (“King’s Table”) game, 4 Hornell, Karl, 264 Hostile Applets Web site, 221 HTHP (Head To Head Protocol), 17 HTML tag, <Mailto>, 19 Human factors in game design, 319–322 Hunter class, 292–293, 393
I Iceblox game, 264–268 Web site for, 264 ichess Web site, 29 Image class, 82 ImageConsumer and ImageProducer classes, 95 ImageHandler interface, 86 Actor class and, 87 ImageManager, 99–105 Images celled, 84 drawing, 80 GameWorks support for, 83 filters, 102 handlers for, 84–94 loading, 82, 94–99 multiple, 84 non-rectangular, 82 preloading, 99 scaling, 100
sharing, 93 special efffects and, 102 tiled, 101 transparency and, 83 Implementing a game, 324–326 implements keyword, 121 Input detecting, 136 joystick, 107 Input handling, 107–131 Input/output model, 310–313 Integrating external game information, 388–391 Intelligence. See AI. Interconnect topology, 308–310 Interfaces ActorToGame, 286-287 GameToActor, 285 Internet Explorer, 415 Internet Gaming Zone, 16 Inter-player chat, 357–359 Internet Relay Chat (IRC), 11 IO. See Input/output model. IRC-based games, 11–13
J JAR files, 416 Java benefits for Web gaming, 23–25 classes. See Classes. coding data in source files, 326 multiplayer games, 26 single-player games, 25 Java Boutique Web site, 26 Java code for Actor class, 65 ActorManager class, 66, 182 animating remote actors locally, 400 BackdropManager, 105 bounding-box collision detection, 180 button with sound, 218 Clock and ClockWatcher classes, 160–162 clock support, 163–164 decoupling from networked game, 361 DirtyRectangleManager class, 186 DominationEvent method, 330 Donut actor, 43–44 Donuts class, 45–46 donuts spinning onscreen, 32 double-click detection, 131 drag-and-drop mover, 167 DropSite interface, 143 encapsulating a game mode, 333 EventInterface class, 119–121 EventManager class, 116–118 fading image, 103 findMax method, 273 flow of Domination game, 342–348 Game class, 68–70 GameApplet, 72 GameFrame, 73 GameMoveManager class, 275 generic server, 204–206
high score management, 210–214 high score server, 206–207 HighScoreRecorder class, 208–209 Hunter method, 292–293 Iceblox flame, 264 ImageHandler class, 88 JellySquirt, 41 joining a game, 382 KeyTestActor class, 125 latency compensation, 409 MansBestFriend method, 290–291 MazeWars network initialization, 378 Min-Max algorithm, 272 motion, 266 MultipleImageHandler class, 92 onNotify method, 354 picking an actor, 138 remote actors, 389 Render method, 443 ScoreLabel class, 196 ScoreManager class, 195 ScoreManager client, 214–215 simple card game, 153–158 sorting by Z-order, 152 sound loop, 220 sound, multithreading, 223 spaceship, 37–39 StageManager class, 63 StageManager with double buffering, 145 StageManager, highly optimized, 149 StageManager, upgraded, 190 startDrag method, 138 stopDrag method, 142 TicTacToe, 277–279 timer, 166 WrapRectBoundary class, 177 Java Media Framework API Overview Web site, 218 Java Programming EXplorer,book, 24 Javaworld magazine, 421 JellySquirt, code for, 41 Jonathan Hardwick’s Java Optimization Web site, 413 Joystick input, 107 JPEGs, 80 Jumping actor, 140
K KALI, 17 Keyboard decoding special keys of, 126 events, 110, 124 useful values of extra keys, 128 keyDown, 124 KeyTestActor class, 124 Java code for, 125 keyUp, 124 Keyword, implement, 121 King’s Table, 4
L Latency, 405–411, 448–451
data, using, 408 Layering, 151–153 Loading images, 94–99 Looping a sound, 219–222
M <Mailto> HTML tag, 19 Managers, 74 MansBestFriend actor, 283–296 method, 290–291 Map class, 430 Mapping, 430 Math.random method, 266 Maze game, 268 MazeWars game, 281–302 actor data and, 384–388 actors in, 283 joining, 381 network archictecture of, 373 networked, 373–395 networked, extended, 397–411 playing area, 282 MazeWarsServer application, 375–377 Message interaction diagrams, 60–61 Methods acceptDrop, 142 addDropSite, 141 canDrag, 139 containsPoint, 142 createAutoActor, 294 createDragDropManager, 136 createEventHandler, 119 createFadeImages, 104 DominationEvent.writeDataContents, 330 dragTo, 137 factory, 75 findMax and findMin, 273 getBestMove, 272 handleEvent, 109 hit, 42 Hunter.onTick, 292–293 initialization for networked games, 378 Math.random, 266 onNotify, 353 paint, 79, 105 play, 46 removeDropSite, 141 Render, 443 setVelocity, 176 startDrag, 137 stopDrag, 137 tick, 159 toString, 127 weapons-related, 297 Microsoft. See Internet Explorer. Min-Max algorithm, 269–275 code for, 272 Modifiers, decoding, 128 Motion code, 266 Mouse double-click, detecting, 130
single click, detecting, 129 Mouse events, 128–131 mouseDown event, 137 mouseDrag event, 137 mouseUp event, 137 Movement actors and, 133 formula for, 173 in gaming, 133–158 smooth, 144 sprites and, 172–176 MUDs (MultiUser Dungeons), 13–16 Database Definition Language and, 13 Web site for, 22 Multiplayer games, 26, 305–322, 349–371 Fred, 425–456 MazeWars, 373–395 MazeWars, extended, 397–411 registry for, 375 MultipleImageHandler class, 92 Multithreading sounds, 222–226 MultiUser Dungeons. See MUDs. Mystery cass, 298
N Navigator. See Netscape. Neil Bartlett’s Web site, 78 Netscape Java optimization and, 415 plug-ins and, 22 Networked games, 305–322. See also Multiplayer games. architecture, 373 broadcast delay and, 404 connection classes, 334–336 creating, 380 decoupling from, 359–362 error-handling in, 313–314, 362–370 initializing, 377–379 joining, 381 latency and, 405–411 remote actors in, 388–392 security and, 455 Newton, Sir Isaac, 173 NewtonMovement class, 173 Notifiers, 75
O Object, Game, 119 Observers, 75 OKBridge Web site, 19 onNotify method, 353 Optimization, 150, 413–423 Fred game and, 445 graphics and, 439 Internet Explorer and, 415 Netscape and, 415 performance and, 416 reducing file size, 421 speeding up applications, 421 Output model, network, 313
P paint method, 79, 105 Painting, 79–106 Panel component, 112 Patterns, 76 Patterns Home Page Web site, 76 PBEM (Play-By-Email), 6–11 dice servers, 8 opponent-brokerage service, 9 PBM (Play-By-Mail), 6 Pendragon’s Java Performance Report, 422 PGP (Pretty Good Privacy), 8 Picking, 137 Piclab, 36 Pit, The, 22 Pixel comparison, 178 Pixel Pete, 264–268 Pixels, 82, 97 Play-By-Email. See PBEM. Play-By-Mail. See PBM. Player class, 431, 454 Players, automated, 281–303 tips for pleasing human, 319–322 POV (point of view), 434 POV-ray, 35 script, 35 Pretty Good Privacy (PGP), 8 Profiling, 419 output from, 419 Progressive rendering, 441 Projection plane, 435 Pursuit algorithms, 268–276
R Raycasting, 431–439 3D Pipeline versus, 445 speeding up, 440 Raytracing, 35 raycasting versus, 432 RealClass class, 453 Rectangles adjacent, 185 dirty, 184–192 overlapping, 185 Redraw fast, 144 out of eyesight, 144 speeding up, 147 Registry for multiplayer game, 375 Remote actors, 399–402 RemoteActor class, 388–392 removeDropSite method, 141 Render method, 443 Rendering, 431 delayed, 443 progressive, 441 Return animator, 167 Returns, 144
Rules, auto actors and, 295–296
S Scaling images, 100 ScoreManager class, 194–196 Scores array, 43 Scoring, 193–216 client and, 214 security, 198 server and, 196–214 user reports and, 198 Scrabble, Web site for, 19 Secure scores, 198 Security, networked games and, 455 Servers Domination game, 338 Fred game, 427 high score, 196–214 MazeWars game, 375–377 setVelocity method, 176 Shockwave, 22 SingleImageHandler class, 88 Single-player games, 25. See also Games. Size reduction, 421 Sounds, 217–226 creating, 46 explosions, 42 looping, 219–222 multithreading and, 222–226 Source files, coding data in, 326 Spaceship, 37 code for, 37–39 movement of, 39 Special effects, 102 fading, 103 Speed, 421 Sprites, 171–192, 437 movement and, 172–176 Stage, BackdropManager for, 105–106 StageManager highly optimized, 149 with double-buffering, 145 StageManager class, 62–63 Standardization in gaming, 4 Star topology, 307–308 startDrag method, 137 Java code for, 138 StatusCanvas class, 351–355 methods, 352 stopDrag method, 137 Java code for, 142 Sun Microsystems, 416 Synchronization, 448
T Texture mapping, 440, 447 The Maclin Times Web site, 18 Threading, 359-360 broadcast delay and, 404-407 Domination applet and, 336-337
images and, 95 MazeWars game and, 379-385 network input and, 311 Tick method, 159 TicTacToe code for, 277–279 GameMoveManager and, 276 Timer, 165–167 Timestamp, double-clicks and the, 130 Timing network issues, 317 Topologies, 306–310 interconnect, 308–310 star, 307–308 toString method, 127 Transparent images, 83
U Unearthed Web site, 29 Unicode value, 124 Universal busy signal, 351–355 Usenet newsgroups, 4
V VA (viewing angle), 434 Velocity, 176 Viewing angle (VA), 434
W Wacom ArtPad II, 34 WAV files and Java, 218 WeaponGameBridge class, 299–302 Weapons, 281–303, 296–302 interfacing with, 302 methods, 297 Web site for 40k MIRACLE Monthly, The, 22 Adobe Acrobat, 23 AI, 22 bridge, 19 chess, 18 PBEM, 6 Chinook, 21 computerized opponent, 9 CONNECT!Quick client, 416 Connect4, 20 crossword puzzles, 23 dice, email-based, 8 Doom FAQ, 4 Europa, 27 GoldWave, 218 Hostile Applets home page, 221 Iceblox game, 264 ichess, 29 Internet Explorer and Java, 415 Internet Gaming Zone, 16 JAR files, 416 Java Boutique, The, 26
Java Media Framework API Overview, 218 Java optimization, 413 Java performance, 422 Javaworld magazine, 421 Middle Earth card game, 9 MUD, The Pit, 22 Neil Bartlett, 78 patterns, 76 PGP (Pretty Good Privacy), 8 Scrabble, 19 Shockwave, 22 Unearthed MUD, 29 WinZip, 415 WinZip, 415 WrapRectBoundary class, 177
Z Z-order, 152 Java code for sorting by, 152
Table of Contents