Game Code


The amount of code used in the Actions frame of the game movie clip in this file is intimidating. There are nearly 700 lines of code used to make this game work. But don't despair a large chunk of this code is from functions you've already seen throughout the book. For instance, we use frame-independent collision detection and ball-ball collision reactions. Both of those were explained in detail earlier in the book and are fairly lengthy functions.

In this section I'm going to give you an overview of how the code in this game works. Then we'll talk about some specifics.

General Overview

After both players are in the game, Player 1 has ball-in-hand. When Player 1 places the ball, Player 2 is updated with the cue ball's new position. Player 1 can then aim and break. As soon as Player 1 releases the mouse button to shoot, Player 2 is informed of the shot and the balls begin moving on both players' screens. As the shot plays out, each player's game keeps track of whether any rules have been broken. We watch to see if a ball is scratched, if the lowest-numbered ball was hit first, and if a ball is pocketed. If a ball gets pocketed, then we check to see if it was the cue ball or the 9 ball. If it was the 9 ball, then the game is over. If it was the cue ball, then only the turn is over. Play continues in this manner until the 9 ball sinks.

This code is lengthened by the inclusion of some optimizations I've come up with. This is the fourth time I've created a game of 9 ball. (One thing you'll find as you program more games is that by the time you're finished programming a game, you already have ideas about how to do it better next time around.) Every time I've programmed this game, the code gets longer but there are fewer bugs and the game play is smoother.

The one major enhancement I've included in this version of the game is how the code determines if it should detect the collision between two balls. In previous versions of 9 ball, the code constantly checks for collisions between every ball. So, for instance, if you were to hit the cue ball into the 3 ball and all of the other balls remained untouched, the code would still do collision detection between balls 5 and 6. There is no reason why I should check for a collision between balls 5 and 6 if neither of them is moving a collision isn't possible!

In this version of 9 ball I've created two arrays, called moving and notMoving. The names pretty much tell all: The moving array contains references to the balls that are moving, and the notMoving array contains references to balls that are not moving. When a ball is hit, it is removed from the notMoving array and placed in the moving array. When a ball stops moving, it is removed from the moving array and placed in the notMoving array. When the moving array is empty, that means that all balls have stopped and the turn is over. This technique helps me to more efficiently determine which balls should have collision detection. I check every moving ball against every other moving ball, and every moving ball against every stationary ball. When many balls are moving, this detection can run a little slow. But for most shots there are only going to be a few balls moving, and so we greatly reduce the number of collision-detection checks.

Optimization Analysis

It is important to understand why 10 balls in the game of 9-ball is a more reasonable number for us to use than the 16 required for a game of 8-ball. Common sense dictates that if you have a smaller number of balls, then the number of collision-detection checks goes down. That is true. And collision detection is the "expensive" script we are looking to minimize.

Flash actually doesn't have much trouble moving that many balls around. The problem is with the intensity of calculations. A ten-ball game is not just a little faster it is substantially faster. If all of the balls on the table are moving in a game of 9-ball, we have 45 collision-detection checks per frame. If we add just six more balls, this flies up to 120 collision-detection checks per frame nearly three times as many checks for just six more balls. Through my tests I found that a 13-ball game still runs at a reasonable speed. That takes 78 collision checks per frame. Remember, though, that these are the maximum numbers of collision checks per frame if all balls are moving. When just a few balls are moving, this number is greatly reduced because of the optimized way in which we check collisions in this game (described above in this section).

When you code something that appears to be taking a lot of CPU power, it is very important to spend some time trying to understand why the code is so intense. There are usually ways to reduce a script's CPU load by analyzing why the script is slow and guessing at alternative ways to code it. If you can come up with a great way in this pool game to, say, divide the balls (in memory) into four quadrants of the table and only perform collision detections based on table location, then you might be able to drop the CPU load enough to make a game of 8-ball that runs well!

Who's on First?

I have implemented another technique in this game that I have never used before: collision order. It is entirely possible for more than two balls to collide during the same frame. In fact, it is possible to detect that two balls have collided when they shouldn't collide. Let me explain. Imagine that there are three balls moving on the table. Balls 1 and 2 are moving toward each other from opposite sides of the table and are on a collision path. Ball 3 is moving perpendicular to these two balls on a collision path with ball 1. Because of the way we normally perform loops to detect collisions, we may erroneously detect a collision between balls 1 and 2, even though ball 1 actually collides with ball 3 a fraction of a second earlier. If ball 1 collides with ball 3 first, then it will probably be deflected out of the way of ball 2, and so what seemed an inevitable collision with ball 2 will never happen. Our collision-detection scripts look at the collision possibilities between two balls and don't even consider the fact that other balls exist. Do you see the problem? We may detect three collisions with ball 1 in one frame, but which is the real collision? After all, a ball can only collide with one other ball at a time. Here is the solution.

  1. We run through all of the possible collision-detection comparisons. This means that we check every ball in the moving array against every other ball in the moving array, and then every ball in the moving array against all of the stationary balls in the notMoving array.

  2. For every collision determined, we temporarily store in an array the time of this collision and the names of the balls involved in the collision. Remember this from the frame-independent collision-detection scripts we developed in Chapter 5, "Collision Detection": When a collision is detected, we are given the amount of time that has passed from the previous frame to the time of the collision. If this number is .2, then one-fifth of a frame passed before the balls collided. We do not perform any collision reactions here; in this step we are simply collecting times and ball names.

  3. When the collision-detection script is done, we sort the array so that the collision with the shortest time is at the 0th index in the array. This is the collision that was the first to occur.

  4. We calculate a collision reaction for the first collision to occur. Since this collision can affect whether any of the other collisions are valid, we abort the script here and start it over (back up to step 1).

We perform these four steps in a loop until there are no collisions on the table, or until we abort the loop for any other reason.

The Functions

At this stage in your career as a Flash developer (or at least as a reader of this book so far), you have seen just about all of the functions that make this game run, or similar versions of them. In this section we will mention most of the major functions and what they do, and in some cases we will look at the ActionScript.

Open the Actions panel, and look at the ActionScript on the Actions layer. (Remember, we're still in the game movie clip of pool.fla, not back on the main timeline.) Before the script defines any functions, it lists several lines of code initializing some variables and objects that will be used in the game. Here are the first few lines:

 1   soundOn = true;  2   radius = 8.5; 3   mass = 1; 4   decay = .98; 5   runPatch = 0; 6   inPlay = false; 

Line 1 simply creates a variable that controls whether a sound is played or not. It sets the value of soundOn to true. If true, then when the balls collide, a sound will be played. If false, then the sound will not be played. We did not include a sound on/off toggle in this file, but if you decide to add your own sound toggle button, all you have to do is toggle the soundOn variable's value. In line 2 we create a variable called radius. This is the radius of the pool balls. Next we set the mass of the balls to 1. We then create a decay variable, which is used to slow the balls down over time so that they eventually come to a stop. In line 5 we set runPatch to 0. It is used by the patch() function, which we will talk more about later. Finally, we set inPlay to false. This variable is used in the onEnterFrame event to determine if we should run the movement and collision-detection functions.

A few more things happen in the ActionScript on this frame before the function definitions begin. We create an object called game that will be used to store most of the information about the game. On the game object we create several other objects, including one for each ball, called ball1 through ball10, and one for the cue stick called stick.

startGame()

Now let's look at the startGame() function:

 1   function startGame() { 2      flagStopped("yes"); 3      inPlay = false; 4      if (player == 1) { 5         game.myTurn = true; 6      } else { 7         game.myTurn = false; 8      } 9      sinkList = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 10     currentBall = sinkList[0]; 11     game.moving = []; 12     game.notMoving = []; 13     rack(); 14     if (game.myTurn) { 15        ballInHand("partial"); 16     } 17     moveVariables(); 18     popup.gotoAndStop("game started"); 19     if (game.myTurn) { 20        popup.msg.text = "The game has begun. It is your            turn."; 21     } else { 22        popup.msg.text = "The game has begun. It is your            opponent's turn."; 23     } 24  } 

This function is called when both players first arrive and when a game is restarted. First, the flagStopped() function is called. This creates a room variable saying that this user is ready to send or receive a move. Next, the inPlay variable is set to false. This variable is used in the onEnterFrame event to determine if some function should be executed. If false, the functions are not executed. It is set to true when the cue ball is shot. Then, using the player variable, we determine whose turn it is. If it is Frank's turn, we set game.myTurn to true in Frank's game instance; otherwise we set it to false.

In line 9 we create an array called sinkList. Remember that the lowest-numbered ball on the table always has to be hit first. The number of the lowest-numbered ball on the table is sinkList[0]. Whenever a ball is pocketed, its number is removed from the sinkList array. In this way, sinkList[0] will always contain the lowest-numbered ball on the table. In line 10 we set a variable called currentBall, which is used to store the number of the lowest ball on the table, to sinkList[0]. This stores the number of the lowest ball on the table. Next we create new moving and notMoving arrays. They are used to store the objects that represent the balls that are moving and the balls that are not moving. When a ball starts moving, it is removed from notMoving and inserted into moving. Likewise, when a ball stops moving, it is removed from moving and inserted into notMoving.

We then position the balls correctly on the table by calling the rack() function. Then, in line 15, if it is Estelle's turn, she is given ball-in-hand by calling the ballInHand() function. The string "partial" is passed into ballInHand(). That string signifies that she has ball-in-hand behind the head string. If "full" was passed in, then she gets ball-in-hand with no restrictions. The final few lines in this function are straightforward: The moveVariables() function is called (it initializes some variables and is called before every shot). Then the pop-up graphic appears and informs you that the game has begun.

onEnterFrame

At the bottom of the Actions frame there is an onEnterFrame event:

 1   this.onEnterFrame = function() { 2       //l = getTimer(); 3       if (inPlay) { 4          moveBalls(); 5          keepGoing = true; 6          timer = 0; 7          while (keepGoing && ++timer<10) { 8             ball2Ball(); 9          } 10         detectWalls(); 11         patch(); 12         renderBalls(); 13         if (game.moving.length == 0) { 14            moveDone(); 15         } 16     } 17      //trace(getTimer()-l); 18  }; 

The actions in this event are executed in every frame, if inPlay is true. That variable is set to true when the cue ball is shot. In line 4 we do something that you have seen all throughout this book: update the positions of the objects in memory, but not on the stage. Next, we set keepGoing to true and timer to 0. Earlier in this chapter we discussed the way we are performing collision detection in detail. We store all collisions, sort the array, and then calculate a collision reaction for the first collision in the array. In lines 7 9, we run through the collision-detection script again and again, until all collisions have been detected. When no more collisions have been found, the ball2Ball() function sets keepGoing to false, and the loop terminates. However, you've probably already noticed that we've put another limit on this loop as well we don't let it execute more than ten times per frame. I chose the number 10 as the upper limit (called a loop cap) by experimentation. I played several games of pool using various loop caps and watched the physical results versus the amount of time the loop took to execute. In the end, a cap of ten loops gave the optimal physical performance with little loop-time overhead. In line 10 we run the script that checks for collisions between the balls and the cushions. If a ball is colliding with a cushion, we also check it for a collision with a pocket, using the detectPocket() function. Checking for pocket collisions with every ball on every frame would use needless overhead processing, so we only check when a ball is colliding with a wall. That helps reduce the amount of code being executed every frame.

In line 11 we execute the patch() function. Why a patch? With all of the math and physics used in this game to give the high level of realism, there is still a nonrealistic reaction problem that can occur. Approximately 1 out of every 50 or 100 shots results in two balls' sticking together just a bit, but unmistakably touching. This is a bug that I will undoubtedly solve on the next build of this game, but as of this writing I'm stumped. As a last resort for a situation like this, I've built a special time-based function to check for problems. This function has an internal timer that only lets it execute about once every 20 frames. When it executes, it checks for hitTest() between the actual ball movie clips. No two balls should ever be touching that, of course, constitutes a collision. If the hitTest() returns true, then the balls are touching and our bug is happening. We then slightly nudge the balls to the side so that they break apart. As mentioned above, this doesn't happen very often, but when it does happen, we are prepared.

In line 12 we take the positions of the balls in memory and place them on the screen. If the length of the moving array is 0, then all of the balls are stopped and the turn is over. When that happens, moveDone() is called, which analyzes the results of your shot and determines whose turn it is now.

Analyzing Code-Execution Time

Did you notice the two lines that are commented out in the ActionScript above (lines 2 and 17)? When uncommented, they trace the amount of time in milliseconds (ms) that it takes for everything in the onEnterFrame event to completely execute. Approximately 24 times per second, this trace puts a new number in the output window. This is one of my most often-used tools when looking for optimizations in a game. I use it when I'm trying to find out what is the slowest part of the script. I can watch the numbers as the pool rack is broken and as the cue ball is being placed with ball-in-hand to see if things are executing at a reasonable speed. What is considered reasonable? You will have to use your own experience to make that call. But here is a starting place: We are working at 24 fps. There are 1000 ms in 1 second approximately 41 ms per frame. If the trace time for your onEnterFrame event is greater than 41 ms, then the movie will not play at the full frame rate. So you would want to do your best to try to keep the number below 41. I would consider it reasonable to have occasional spikes above 41 (such as when breaking in pool), but not to average above 41.

moveDone()

This function handles the logic to determine if you get to keep your turn or if the game is over. When you shoot the cue ball, several game actions are tracked. If you collide with the correct ball first, then correctFirstHit is set to true. If you sink a ball, then ballSank is set to true. If the cue ball sinks, then cueBallSank is set to true. If the 9-ball sinks, then nineBallSank is set to true. These four Boolean values can determine if you get to keep your turn and if the game is over. If the game is over, then this script will also determine who the winner is:

 1   function moveDone() { 2      roundPositions(); 3      flagStopped("yes"); 4      inPlay = false; 5      var loseTurn = false; 6      var gameOver = false; 7      var scratch = false; 8      if (nineBallSank && !cueBallSank && correctFirstHit) { 9         var gameOver = true; 10      } else if (nineBallSank && (cueBallSank ||         !correctFirstHit)) { 11         var loseTurn = true; 12         var gameOver = true; 13      } else if (!nineBallSank && (cueBallSank ||         !correctFirstHit)) { 14         var loseTurn = true; 15         var scratch = true; 16      } else if (!nineBallSank && !cueBallSank &&         correctFirstHit && !ballSank) { 17         var loseTurn = true; 18      } else if (!nineBallSank && !cueBallSank &&         correctFirstHit && ballSank) { 19          //you did good 20      } 21      if (gameOver) { 22         if (loseTurn) { 23            if (game.myTurn) { 24               iWin = false; 25            } else { 26               iWin = true; 27            } 28         } else { 29            if (game.myTurn) { 30                iWin = true; 31            } else { 32               iWin = false; 33            } 34         } 35         popup.gotoAndStop("game over"); 36         if (iWin) { 37            popup.msg.text = "You win!"; 38         } else { 39            popup.msg.text = "You lose!"; 40         } 41      } else if (loseTurn) { 42         game.myTurn = game.myTurn ? false : true; 43         if (scratch) { 44            game.ball1.x = (game.middle+game.left)/2; 45            game.ball1.y = (game.top+game.bottom)/2; 46            game.ball1.clip._x = game.ball1.x; 47            game.ball1.clip._y = game.ball1.y; 48            game.ball1.clip._visible = true; 49         } 50      } 51      moveVariables(); 52      if (game.myTurn && !gameOver && !scratch) { 53          initializeStick(); 54      } else if (game.myTurn && scratch) { 55         ballInHand("full"); 56      } 57  } 

The first thing we do in moveDone() is call the roundPositions() function. (See the sidebar on page 498 for a description of why that function is needed.) We then set player1stopped or player2stopped to "yes" on the server. Remember that that variable represents the status of the screen. When it's "yes", that means both screens are in sync and stopped. After initializing several variables used by the function, we launch into a large conditional statement. We run through a series of conditions (lines 8 20) to determine the result of the shot that was just made. If the 9 ball sank and the lowest ball was hit first and you didn't scratch, then the game is over and you win! If the 9 ball sank but either you scratched or you didn't hit the correct first ball, then the game is over and you lose. If the 9 ball didn't sink and you scratched, then you lose your turn. If you sank a ball and didn't scratch, it's still your turn.

We then have another conditional statement that checks to see if the game is over (lines 21 50). If the game is over and whoever shot lost his turn, then that player loses. If the player who shot did not also lose his turn, then he wins. If the game isn't over but the current player has lost his turn, then we change whose turn it is (line 42). Also, if that player scratched, then we center the cue ball behind the head string (lines 43 49).

Why the roundPositions() Function Is Necessary

In the second line of the moveDone() function, you see the roundPositions() function. The purpose of this function is to eliminate the minor math calculation discrepancies between different Flash clients.

Multiplayer gaming comes in at least two flavors. One type is called authoritative server, in which the server calculates everything for the client (that is, your game) and tells the client where to put the objects. In Flash we use the other type of multiplayer gaming, called authoritative client, in which the server is just there to route the information correctly and is ignorant of the game's details. We pass the minimal amount of information from one client to another and hope that Flash calculates precisely the exact same outcome on both machines. In 9-ball, this means that we can pass the cue ball's position as well as the angle and speed at which to hit the cue ball; and your opponent's game instance can take that information and precisely re-create the same outcome as what occurred on your own machine. We rely on the fact that given the same initial state, both clients will calculate precisely the same results from identical input.

But I'm here to tell you not to rely on this too heavily. While both clients appear to give the same results, those results are ever so slightly different. I played several games of this version of 9-ball while developing it. After 10 or 15 shots, I saw that the positions of the balls on the two screens were a little bit off from each other. I then put on my sleuthing hat and decided to get to the bottom of it. It turns out that even after the very first shot, the balls were not in precisely the same place on both machines! They were very close, though. Want to know how close? Flash calculates numbers out to 15 decimal places, and it was at the 15th decimal place where the positions were different. After the first shot or two, this is still a negligible difference. But because of propagation of error, this discrepancy increases with every shot. After about 10 shots, the difference becomes visible possibly even enough for a ball to sink on one screen and not the other.

This brings up two questions: Why do we have a discrepancy, and what can we do about it? I don't know the answer to the first question. I can only speculate that it is some sort of a rounding error, or that at 15 decimal places there are some sort of random variations from machine to machine (variations that Macromedia didn't know about or didn't think would ever be a problem). But I do know that the solution to the second question is quite simple: We round all positions to three decimal places, which is accurate enough for us, by including the roundPositions() function on line 2, as we've done in the moveDone() function above. Since the discrepancies are never even close to three or four decimal places after the first shot, we will always be able to round predictably on both clients. This means that after every shot, the balls will be positioned in precisely the same spot on both players' machines.

In line 51 we reset some variables by calling moveVariables(). This function was also called from the startGame() function. Finally, we either initialize the cue stick to rotate around the cue ball, or we give the player ball-in-hand.

shoot()

We have talked a lot about the functions that move the balls around. But let's look at the function that tells the cue ball to move in the first place:

 1   function shoot(speed, angle) { 2      if (!inPlay) { 3         flagStopped("no"); 4         game.stick.moveStick = false; 5         game.stick.rotateStick = false; 6         game.stick.clip._visible = false; 7         line._visible = false; 8         inPlay = true; 9         var ob = game.ball1; 10        var cosAng = Math.cos(angle); 11        var sinAng = Math.sin(angle); 12        ob.xmov = speed*cosAng; 13        ob.ymov = speed*sinAng; 14        addMoving(ob); 15        var message = "Table locked!"; 16        changeMessage(message); 17     } 18  } 

This function is called after the angle and the power (speed) of the shot have been set. Both of these values are passed in. We call flagStopped() and pass in "no". This locks the screen so that it can't receive any moves. In the next few lines we toggle several variables that allow the user to rotate the stick and set the speed. We then set inPlay to true so that the onEnterFrame event can do its job. Next we calculate the ball's speed, set the speed on the cue ball's object, and add it to the moving array by calling addMoving(). In the last couple of lines we create a message on the screen saying "Table locked!"



Macromedia Flash MX Game Design Demystified(c) The Official Guide to Creating Games with Flash
Macromedia Flash MX Game Design Demystified: The Official Guide to Creating Games with Flash -- First 1st Printing -- CD Included
ISBN: B003HP4RW2
EAN: N/A
Year: 2005
Pages: 163
Authors: Jobe Makar

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net