Creating Pachinko


Creating Pachinko

It's time to take what we've seen so far in terms of physics modeling and apply it to a larger project. The idea is to simulate a pachinko game. You can see a picture of our Pachinko game (so that you can get an idea what it is if you have never seen pachinko) in Figure 11.15.

click to expand
Figure 11.15: Pachinko is a lot like pinball. Steel ball bearings bounce around a table. Unlike pinball , however, pachinko has no flippers.
Note  

Pachinko is a Japanese game that uses ball bearings as a sort of bullet that the player shoots into an array of pins. The ball bounces through the pins into various pockets. Certain pockets cause the machine to give the player more bullets. Modern pinball games are a direct descendent of pachinko games .

Before we get started, I want to talk about what kind of physics we're going to need. I intend to simulate the entire game using circles as our balls and points as the pins. Special pins will act as pockets that the balls can go into.

The physics we'll need will be similar to our marble examples, with a few parts thrown out. For example, we won't need acceleration in Pachinko . However, one area that we do have a problem with is collision detection. The number of pins in our Pachinko game could be in the hundreds. Also, we want the user to be able to shoot several balls quickly so that many can be bouncing around the board at the same time.

If we have three balls bouncing around at one time and there are more than 100 pins, the number of hit test calculations we're going to need to do every iteration starts to add up. In fact, if we actually do all those hit tests, our frame rate will hit the floor and our game will chug like crazy. To solve this problem, we're going to have to get a bit creative with our collision detection system. We'll see how that works later.

Okay, that's enough of what's to come in terms of physics. Let's get back to the beginning. It's time to gather our art assets and stock our library.

Art

I asked Rachel to develop something for Pachinko , and she created the art you saw in Figure 11.15. From there, I broke Rachel's art into pieces in Flash so that I could layer and manipulate things like I needed to.

The library contains a number of symbols, seen in Figure 11.16. Many of the symbols are kept in folders for organization. Most were imported from Rachel's art, but some, like the ball , ballPath , flipper , cup , and pinContainer , I created in Flash.


Figure 11.16: The library contains many symbols, most organized into folders.

If you look at the pachinko.fla file in the Chapter 11 directory of the CD, you'll see that I've instantiated many objects on the stage. Because this is a complex layout, it's far easier to instantiate manually than with ActionScript. By placing the elements manually, I can position each item in relation to the other elements instead of having to adjust and test repeatedly like we would with dynamic instantiation.

Rather than describe the symbols in the library, I will describe the instances on the stage, layer by layer, starting at the bottom layer. Look at the layers in the timeline, shown in Figure 11.17.


Figure 11.17: The timeline has a number of layers, each with one or two symbols instantiated in it.

The backgrounds Layer

The bottom layer, called backgrounds , contains a few symbols from the library that create the shaded background at the back of the Pachinko table. Figure 11.18 shows the contents of the backgrounds layer.

click to expand
Figure 11.18: The backgrounds layer contains two symbols that form the back plate of the Pachinko game.

The fascia Layer

The next layer up, called fascia , contains the main art for the Pachinko background. You can see this piece in Figure 11.19.

click to expand
Figure 11.19: The pachinko machine's background has a face and a water dragon on it. This art resides in the fascia layer.

The ringBorder Layer

The layer above the fascia is named ringBorder . It contains a purple ring that goes around the playing area of the pachinko machine. The balls never go outside this border. Figure 11.20 shows the contents of the ringBorder layer.

click to expand
Figure 11.20: The ring border is the boundary for the balls in our Pachinko game.

The ballPath Layer

The next layer up from the ring border is named ballPath . This layer contains an instance of the ballpath symbol, which is used to animate the shooting of a ball. Because the code involved in shooting the ball would be hard to do in code but easy with a tween, I have hand-animated this action and placed it in the ballpath symbol. Figure 11.21 shows the ballPath layer with its contents.

click to expand
Figure 11.21: The ballPath layer contains animation of the ball being fired so that we don't have to do this in script.

The gutter Layer

Above the ballPath layer is the gutter layer. This contains a black piece that will act as the hold the balls go through when none of the special pockets have been hit and the ball gets to the bottom of the ring border. Because the background is black, the gutter does not show up against it. I have made the background and gutter visible in Figure 11.22.


Figure 11.22: The gutter, which is the black slice in this figure, traps balls that reach the bottom of the ring border.

The board Layer

The next layer up from the gutter layer is the board layer. This is where all the really important bits are located. Everything that pertains directly to the game (except the ring border) is kept in this layer. Because this layer is higher in the layer list, it appears on top of everything else we've looked at.

The eight instances in the board layer are listed next and labeled in Figure 11.23.

click to expand
Figure 11.23: There are eight instances in the board layer.
  • ballCreationLocation

  • pinContainer

  • Cup instance

  • Text field on top of cup instance

  • Frame counter

  • flipper

  • fakeBall

  • powerBar

In the preceding list, instances that are named are given in monospace. The instances that are not named are in the normal font.

Let's look at each instance in the board layer individually.

ballCreationLocation

The ballCreationLocation is an instance of a ball. ball is the symbol for our ball bearings. These balls will bounce around the pachinko board after the user fires them. This particular instance of the ball ( ballCreationLocation ) tells us exactly where the ball will appear when created.

When we get into the script, we will make ballCreationLocation invisible so that the player cannot see it. It will never move, but it will signal us in script as to the position of newly created balls.

pinContainer

The pinContainer instance has many instances of the pin symbol. This symbol represents a small metal spike that is placed into the face of the pachinko board. When the balls come out of the chute (the ball path ), they bounce off pins around the face of the pachinko machine. To make hit testing easier, all pins are child clips of the pinContainer clip. This makes adding, moving, and removing pins easy.

Notice that the pinContainer instance is positioned at (0,0). That will be important later when we're hit testing. Because the pins are children of the pinContainer and the balls will eventually be children of the ballContainer , when we hit test, we need their coordinates to be relative to the same point. In other words, we want to be able to directly test the position between pins and balls without messing around with converting coordinate systems to do it. By placing both the pinContainer and ballContainer (created later with script) at (0,0), we are assured that their coordinates match.

Cup Instance

The cup instance doesn't actually do anything. In a fully realized version of this game, I might have put more balls in it as the user's ball count (current number of balls left to shoot) became higher. In a real pachinko game, there is a cup or trough that the balls come out of. When the user gets a ball into a special pocket on the board, many balls come out into this cup.

Our cup is just a static piece of art that sits there. On top of it is a text field.

Text Field on Top of Cup Instance

The text field instance on top of our cup tells the user how many balls are in the cup. When the game starts, the user is given a number of balls already in his cup. If he shoots his balls into the special pockets on the board, more balls fall into the cup, increasing the value in this text field.

The variable field of this text field is set to _root.balls . That should tell you that a variable named balls will be used to keep track of the current ball count.

Frame Counter

There is also a frame counter that you can use to help you tune your game and optimize it. When we first implement, our engine is going to chug and frame rate will be low. However, as our collision detection becomes faster, the frame rate rises.

flipper

The flipper is used to give the player the idea that he's flipping a ball to put it in play. The user flips balls by pressing the flipper and dragging it. The further the user draws the flipper back, the faster the ball is shot. To show the drawing back of the flipper, we'll rotate it in code.

fakeBall

The fakeBall instance appears to be an ordinary ball, but it is not. In fact, it is an instance of fakeBall . The fakeBall symbol is used to animate the ball firing. As I said before, doing this is script would take a good bit of work to accomplish the same result we get with a tween. Because we can use the same animation for every ball firing, a tween is sufficient. Edit the fakeBall symbol if you want to get a look at how I'm tweening the ball.

powerBar

When the user draws back the flipper, a powerBar shows red to indicate the amount of force that will go into the ball if the flipper is released. By watching the powerBar , the user can carefully control the force on the ball with the intention of getting it into one of the special pockets.

That completes the board layer's instances. We have just a couple layers left to do before we get into the script design.

The pockets Layer

In the layer above board is a layer named pockets . This layer contains the five special pockets that the player is aiming for. The code for special pockets will have nothing to do with these pockets. Figure 11.24 shows these pockets.

click to expand
Figure 11.24: The pockets are special places the user shoots for.

The pocket graphics in the pocket layer are there only to show the user what he's shooting at. The actual bouncing of balls into pockets and the eventual logic to control a pocket shot are done with a symbol called the special pin.

The rim of the pocket is actually made up many pins lined up together in the pin container. These pins are close enough that the ball cannot get through and the ball will look like it is bouncing off the prettier graphic of the pocket rim. At the bottom of the pocket is a special pin that, when hit, triggers the removal of the ball and the accumulation of more balls in the cup. The special pin is shown in Figure 11.25.


Figure 11.25: Pins are aligned under the pocket art. The bottom pin is colored differently and is an instance of a special pin.

The special pin looks identical to the normal pin but is a different color . The player never sees the special pins because they are covered by the pocket art.

The script Layer

The top layer in Pachinko 's timeline is named script and contains the root scrip for our game. Because we've defined all the layers in our movie, we're ready to start adding script to frame 1 of our script layer. I went ahead and locked all the layers except for script so that things wouldn't get moved accidentally .

Script Design

I'm going to break the overall design of this game into two parts. The first part includes the game setup, user interface, and firing animation. The second part includes the ball-handling functions. These functions are responsible for updating the ball's testing for and responding to collisions, and so on. The break in our design really turns our implementation into two separate pieces, which I will lay out and implement separately. This will simplify things and hopefully allow you to work through these sections more easily than if I dumped 20 functions on you at once and then showed you the implementation of each.

Part One: Setup, Interface, and Animation

As I said, the first part of our design will involve the game's setup, the firing animation, and the user interface. We'll begin with a design section that lays out the functions and gives an idea of the basic interaction between them. Then we'll break open the functions and look at the specific implementation for each one.

Design

We begin in the same way we begin every game in this book: setting things up with a function that will be run one time. As usual, it's called initGame , and our script begins with a call to it:

 initGame(); 

We also need a set of mouse handlers to trap input from the user and fire balls as the user desires. This comes in the form of three functions that we attach to the onMouseUp , onMouseDown , and onMouseMove event handles of the _root timeline. Because nothing will share these functions and they will never be disabled, I have created anonymous functions for them, as follows :

 this.onMouseDown = function(){} this.onMouseUp = function(){} this.onMouseMove = function(){} 

These three handlers will take care of the user interface when we implement them.

The only thing left is to create functions to handle the ball firing animation. I'm doing this in the following manner. Any time the ball chute is empty and the user has more balls in the cup, we're going to load a ball into the chute for the user to fire. This is done with a call to a function we're calling loadBall :

 function loadBall(){} 

The loadBall function is responsible for putting the ball in the chute, reducing the ball count, and so on, but we'll get to the details during implementation. For now, just realize that loadBall is going to load a ball to be fired by the user.

When the player uses his mouse to fire a ball that has been loaded, we call a function called fireball :

 function fireBall(){} 

The fireBall function is responsible for starting the fire animation. We do this by creating an interval that will move the playhead of the fakeBall instance. In other words, we're not going to let Flash play the animation itself by calling play . Instead, we're going to do the frame changes ourselves with calls to nextFrame . This allows us to give a consistent ball-firing animation regardless of frame rate.

The function that is set up under the interval that actually does the frame changes is called updateFireAnimation :

 function updateFireAnimation(){} 

The fireBall function sets up an interval that calls updateFireAnimation regularly until the ball is at the end of the chute. Finally, when updateFireAnimation is finished moving the fakeBall up its path, it resets the animation and makes the fakeBall invisible again until a new ball is loaded with loadBall . This loading happens immediately, but only if the user has more balls in the cup.

That's all the functions we're going to need for part one of our implementation. After we get this batch implemented, we'll move on to part two, where we will define more functions for use in ball handling.

Implementation

It's time to implement the functions I just described to you. We'll look at them one at a time as usual, in the same order I presented them to you.

The initGame Function

Called only once at the beginning of our code, initGame sets up everything we need to get started. We begin by opening the function:

 function initGame(){ 

When the game begins, we give the user 25 balls. As you saw in an earlier section, the current number of available balls is kept in a variable called balls . We need to define the initial ball quantity so that when the user starts a new game, we can reset balls to the proper amount. I do this in a variable called total_balls :

 total_balls = 25; 

The current number of balls needs to start out at 0. After the user has read the instructions (which we'll get to later), he clicks to start his game. At that time, the balls variable is set from 0 to total_balls , and the game can begin. Normally I wouldn't initialize a variable that started at 0; however, because we have a text field instance (over the cup) that refers to this variable, we should initialize it to 0 so that something shows up in the field:

 balls = 0; 
Tip  

If we didn't initialize balls to 0, it would be undefined . An undefined value in a text field results in nothing showing up in the field.

The powerBar instance in the board layer needs to be set so that it shows no power on the flipper. When the user draws back, the powerBar will grow. The powerBar instance has within it a child clip called bar that contains a red block used to denote the power. We need to decrease this bar 's yscale so that it shows no red. As the user pulls farther back on the flipper, the bar slowly grows to full size , indicating maximum power. We set the yscale of the powerBar 's bar movie clip to 0:

 powerBar.bar._yscale = 0; 

Our mouse event handlers need a boolean variable to indicate whether the flipper is being drawn back. If the user clicks and releases the button with no drawback, we don't want to fire a ball. We need to indicate when the user has begun a drawback for use in the onMouseUp handler. I've named this boolean flag variable drawback :

 drawback=false; 

When the user fires the ball and our updateFireAnimation function is called repeatedly by its interval, we need some variable to indicate the size of that interval. I've named this variable animationFrequency and given it a value of 30 ( milliseconds ):

 animationFrequency = 30; 

Recall that the fakeBall shows the ball firing animation. It needs to be invisible until the next ball is loaded by loadBall :

 fakeBall._visible = false; 

The other ball instance in the board layer is called ballCreationLocation . It positions new balls as they are created after firing. Therefore, the ballCreationLocation needs to be invisible:

 ballCreationLocation._visible = false; 

Now we're ready to define some depths for use with our dynamically attached movie clips. There are only three depths that we need in this game: one for the ballContainer , one for the Instructions panel that we'll talk about later, and one for the sound manager, which we'll also talk about later. The following sets up all our depths:

 ball_container_depth = 1;     instruction_depth = 2;     soundManager_depth = 3; 

We'll make a call to loadBall to get things started:

 loadBall(); 

I've discussed loadBall a bit already. I told you that loadBall 's job was to load a ball. That's not the whole story, though. loadBall first checks to see if you have any balls left to load; if you do, loadBall loads one ball into the chute. However, if no balls are left in the container, loadBall pulls up the Instructions panel indicating that the game is over and a new game can be started. The Instructions panel, when removed by the user's click, puts balls into the cup again. Then the next time loadBall is called, instead of putting up the instructions, it loads a ball into the chute.

Finally, we conclude our game initialization with a call to a function I haven't talked about yet: initPhysics :

 initPhysics(); 

Because I'm breaking this development into two parts, I wanted to keep the physics initialization away from the rest of the initialization. I've done this by putting part one's startup code in initGame and part two's setup code in initPhysics . When we get to part two, we'll look at the implementation of initPhyiscs .

The last thing I'd like to do in initGame is to add a dummy onRelease handler to the flipper movie clip. This will cause the cursor to turn into a hand when it is over the flipper to tell the user that it is interactive. The reason I say that this is a dummy handler is because we will be controlling the flipper through another means, and this handler is there only for the hand:

 flipper.onRelease=function(){throwAway=0} 

The variable throwAway is something that will never be used. For this handler to work, the function cannot be empty. The least significant thing I could think of to do in this function was to create a variable that would never be referenced.

That completes initGame , so we can close the function:

 } 
The this.onMouseDown Function

We begin by opening this function:

 this.onMouseDown = function(){ 

As I mentioned earlier, when the Instructions panel is being shown, we are waiting for the user to click. Because I want the user to be able to click anywhere and have the instructions disappear, we need this logic to be inside the _root 's onMouseDown function. Therefore, the first thing we do is test to see whether the Instructions panel is up. We do this by testing the name we use for the instructions instance. Later, when we see the code that actually instantiates the instructions, you'll see that I'm naming it instructions . We can test to see whether the Instructions panel is up with the following script:

 if(instructions){ 

If the instructions are up, the previous if statement will be true . If the instructions have been removed, the if statement will be false . If it's true and the instructions are up, we need to remove them:

 instructions.removeMovieClip(); 

We also need to initialize the cup with some balls:

 balls = total_balls; 

Finally, we need to load a ball:

 loadBall(); 

That completes the if(instructions) block, so we close it.

 } 

If the Instructions panel is not up, we have to begin by drawing back the flipper. We only want to draw back the flipper if the user has clicked directly on it. If the user clicked somewhere other than the flipper, we don't want to do anything. This requires a hit test with the flipper. We set this up as an else if that follows the previous test for the Instructions panel:

 else if(flipper.hitTest(_xmouse,_ymouse)){ 

The only way we'll get into this else is if the instructions are not up and the user has just clicked on the flipper. In such a case, we need to begin the drawback. We do this by setting our drawback variable to true to indicate that we are in Drawback mode:

 drawback = true; 

We also need to record the current y position of the mouse so that we know how far the user has drawn back. I've named this variable drawbackOrigin :

 drawbackOrigin = _root._ymouse; 

That completes the else if as well as the entire function, so we close both:

 } } 
The this.onMouseUp Function

We begin by opening the function:

 this.onMouseUp = function(){ 

When the user releases the mouse we need to fire the ball ”but only if we are in Drawback mode, and only if the user has drawn back at least a little on the flipper. That means we need to test drawback as well as the current amount of power. The current amount of power is determined by subtracting the current y position of the mouse from the drawback origin obtained in the onMouseDown function. This gives us the following test for the first line of onMouseUp :

 if(drawback && drawbackOrigin - _ymouse > 0){ 

If this is true , we need to do several things ”the first of which is to come out of Drawback mode:

 drawback = false; 

We need to record the current amount of force so that when we actually create the ball at the end of the fire animation, we know how fast it should be moving. But we cannot determine this directly from the drawback origin. The distance from the mouse to the drawback origin could be any number greater than 0. But the way I will be creating the initial velocity for a new ball is to assume the power is a number greater than 0 but no larger than 100.

So we need to cap the force we are about to record so that it is a number between 0 and 100. Fortunately, this will already be done for us during the onMouseMove function. When we implement that function in the next section, we'll need to scale the power bar so that it doesn't grow over 100% in size. If we're already capping the scale of the power bar between 0 and 100, we can just use that for the recorded force, as follows:

 loadedBallForce = powerBar.bar._yscale; 
Note  

It's not the scale of the powerBar instance that we record, it's the scale of the bar instance within the powerBar instance. That's because the red block that shows power is inside the powerBar clip and our code will scale it to show the current power. The powerBar clip itself is never scaled.

Another thing that's going to happen in onMouseMove is that if we are in Drawback mode, we're going to rotate the flipper as the player draws back on it. That means that here in our onMouseUp function we need to return it to its original rotation, as follows:

 flipper._rotation = 0; 

We also need to return the powerBar 's bar instance to a 0 scale so that the user is ready for the next ball load:

 powerBar.bar._yscale = 0; 

Finally, we can call fireBall to begin the fire animation:

 fireBall(); 

That completes the if block as well as the function, so we can close both:

 } } 
The this.onMouseMove Function

Again, we open the function:

 this.onMouseMove = function(){ 

If we're not in Drawback mode, moving the mouse around does nothing for our game. But if we are in Drawback mode there is much we must do. So our first order of business after establishing the user moved the mouse is to see if we're in Drawback mode:

 if(drawback){ 

If we are in Drawback mode, we need to set up the powerBar . We do this by finding the distance from the drawback origin to the current mouse position, as follows:

 powerBar.bar._yscale = drawbackOrigin - _ymouse; 

Now we need to keep this inside the range from 0 to 100. We do this with an if and an else if :

 if(powerBar.bar._yscale > 100)               powerBar.bar._yscale = 100;          else if(powerBar.bar._yscale < 0)               powerBar.bar._yscale = 0; 

As you can see, all we're doing in the previous script is checking to see if the scale has left our bounds, and if it has, we push it back to the boundary.

Finally, we need to rotate the flipper as the drawback occurs. Because we already have our powerBar set up with a range between 0 and 100, we can use this on the rotation so that the user can't rotate the flipper more than the limits of the power. Because a 100-degree rotation would be pretty severe, let's cut down the value by 5, making it a rotation from 0 to 20 degrees:

 flipper._rotation = powerBar.bar._yscale/5; 

That completes both the if and the function, so we close both:

 } } 
The loadBall Function

The loadBall function actually does a bit more than just loading. loadBall tests to see whether the cup has balls in it. If it does, loadBall loads the ball, and if it doesn't, it calls up the instructions. With this basic game plan in mind, we open the function:

 function loadBall(){ 

Now we test to see if balls are in the cup:

 if(balls){ 

If they are, we decrease balls by 1 to represent the ball going from cup to chute:

 balls; 

We show the ball as loaded by making fakeBall (the firing animation clip) visible:

 fakeBall._visible = true; 

Finally, we mark the fact that our ball has been loaded using a boolean variable called loadedBall . This boolean flag works like the drawback variable in that it indicates something has happened . In this case, we are loaded and ready to fire:

 loadedBall=true; 

That completes the if(balls) block, so we close it:

 } 

If the cup has no balls and the instructions page is not up, we need to instantiate it. We do this by using an else if to see if the instructions clip has already been instantiated:

 else if(!instructions){ 

If this is true and the instructions are not up, we attach them:

 attachMovie("instructions"," instructions", instructions_depth); 

That completes the else if(!instructions) block as well as the function, so we close both:

 } } 
The fireBall Function

Recall that when the user draws back the flipper and releases the mouse button, fireBall is called:

 function fireBall(){ 

Because fireBall is called whenever the user flips the flipper, regardless of whether a ball is loaded, we need to ensure that a ball is in fact loaded inside fireBall before we start an animation.

We can manipulate the flipper when one ball has already been fired but before the next ball is loaded. When this happens, loadedBall is false . We can test to see if we should begin the animation by checking loadedBall :

 if(loadedBall){ 

If loadedBall is true , we need to start the ball animation. We do this by first setting loadedBall to false so that another ball cannot be fired until the chute is loaded:

 loadedBall = false; 

Now we need to set up the animation interval that moves the fakeBall through its tweened animation. This is done with a setInterval call. We store the interval in the fireBallAnimationInterval variable and our interval frequency we defined in initGame to be animationFrequency . This gives us the following script:

 fireBallAnimationInterval =            setInterval(updateFireAnimation, animationFrequency); 

Notice that the function we call in our interval is named updateFireAnimation . This was discussed earlier.

That completes the if block as well as the function, so we close both:

 } } 
Tip  

Because the ball is technically launched at different speeds based on the power of the flipper, you could set the interval to the animationFrequency multiplied by the power ratio. Doing so would make the low-powered shots go up the chute a little slower than the high- powered shots. No matter what, this should be a reasonably subtle difference. If the ball moves too slowly, the illusion of realistic physics will be lost.

The updateFireAnimation Function

We've just set up our animation interval to call updateFireAnimation every animationFrequency milliseconds. We begin the implementation by opening the function:

 function updateFireAnimation(){ 

Here's the logic we need for this function: If the fakeBall 's animation is not over, we increment the playhead by one frame. However, if the animation is at the end, we need to create a real ball and reset all our firing animation variables .

We begin with a test to see if the fakeBall animation is over. If it's not, we increment the fakeBall animation to the next frame:

 if(fakeBall._currentframe != fakeBall._totalframes){            fakeBall.nextFrame();      } 

If, on the other hand, the animation is in its last frame, we need to reset things and create the real ball:

 else { 

The previous else opens if the fakeBall animation is over. When that happens, we reset the animation to the beginning:

 fakeBall.gotoAndStop(1); 

Then we make the fakeBall invisible:

 fakeBall._visible = false; 

Next we load a new ball:

 loadBall(); 

We can't forget to get rid of the fakeBall firing animation interval. If we don't do this, the animation continues to loop. We clear the animation interval by using the clearInterval function:

 clearInterval(fireBallAnimationInterval); 

Finally, we make a call to one of our physics functions, which is discussed later. For now, all you need to think about is that the following call to createBall causes the physics engine to spit a new ball out at the ballCreationLocation :

 createBall(); 

That completes the else block as well as the function, so we close both:

 } } 

At this point, the first part of our implementation is complete. You should be able to test what we've done so far and see the power-bar, flipper, and ball-firing animation working properly. The only problem is that nothing happens when the ball gets to the end of the chute. For that, we have to implement part two.

Part Two (Ball Physics)

With the UI out of the way, we can turn our full attention to the really interesting part of the game: implementing the physics engine.

Design

Our Pachinko physics model has a couple different parts. We have the initialization for the physics engine, ball creation, ball destruction, ball updating, collision testing, and collision resolution.

These topics take the form of 10 functions, the first of which you have already seen the call to. The initPhysics function was called by initGame at the end. The initPhysics function's job is to set things up for our physics model. This function is only called once: before the game starts.

We also have to create a new ball at the end of the firing animation. We create and destroy balls with the createBall and destroyBall functions, respectively.

The function that updates our balls every so many milliseconds is called updateBalls ; you will see this interval created in the initPhysics function. The actions performed here are similar to what occurred in the marble examples.

Our collision detection has three functions associated with it. The first, initCollisionGrid , is called by initPhysics to initialize what will be our new and improved collision detection system. Like initGame and initPhysics , initCollisionGrid only needs to be called once in the life of our game.

The remaining collision detection functions ” testCollisionGrid and testCollisionZone ” perform the actual collision tests. When we get to the implementation section, we'll talk about how those functions work.

Finally, our last set of functions handle the collision response. This group has three functions, which are named for the type of collision they resolve. They are resolveCollisionPin , resolveCollisionBall , and resolveCollisionRing .

Implementation

The physics for Pachinko shouldn't be harder than our marble examples earlier this chapter. The only new concept is the collision detection system. The rest is a rehash of the physics we used for our marbles .

The initPhysics Function

As you know, initPhysics was called by initGame . We begin implementation of initPhysics by opening the function:

 function initPhysics(){ 

We need to define a maximum velocity in this game. Doing so helps us avoid skipping by limiting the distance the ball can move in one update to the diameter of the ball. If you edit the ball symbol in the library, you'll find it is 10 pixels wide. Therefore, I'm going to set the maximum velocity to be 10, as follows:

 max_vel = 10; 

Now we need to define a gravitational constant. Because we're not basing any of the constants in this game on real-world values, the gravitational constant is just a number pulled out of the sky. This gravitational constant is added to the y component of the velocity to pull balls toward the bottom of the board. When our physics is all in place and we can start testing, we'll tweak this value if we need to simulate more or less gravity, as necessary.

Gravity causes acceleration of velocity for the ball. Although this could certainly be done with a full vector, accelerating the velocity by constant vector addition, it really isn't necessary here. Gravity is constant throughout the game, and it is only going to affect the y component. This pairing of constancy and simple implementation makes it easier to skip the vector addition and use the gravity constant to scale the velocity vector during each interval:

 grav = .7; 
Note  

Because the force of gravity is stronger than the air friction slowing the ball down, we can ignore friction. This has a slight effect on the rate at which the horizontal speed of the ball decreases, but not enough to notice.

We also want to define a constant for the ball update interval. I've chosen 30 milliseconds as an update increment, but you can change this during testing:

 updateFrequency = 30; 

When balls collide in real life, they deform. We've already said that we're using rigid bodies that don't deform. When balls deform in real life, some of their inertia is lost on that deformation. In other words, the speed at which something is moving before a collision is greater than the speed after the collision because some of that speed (energy) is lost during the collision. To simulate this loss of inertia, we're going to scale back our velocity after a collision. To do so, we need a collision scalar so that we know how far back to scale. I've chosen .6 as our scalar (60 percent), but we can change this during testing if necessary:

 collision_scalar = .6; 

We're also going to need the radius of the ball for hit testing. Remember that we prefer to do our hit testing with the square of the radius so that we can avoid costly square root functions in our distance formula at run-time. I define a constant named ballRadius2 to represent the square of the ball's radius:

 ballRadius2 = Math.pow(ballCreationLocation._width/2,2); 

Another piece of information we're going to need is the distance from the center of the board to the edge of the ring border. Knowledge of this distance allows us to do a hit test to make sure the ball is inside the ring border. When the ball goes outside that distance, we know we've collided with the border and need to resolve a collision.

Remember, though, that we like to do our distance checks using the radius squared so that we can avoid a costly square root function call during run-time. To make this work with the ring border, we need to square the radius of the ring border.

So far, we know we're going to take the radius of the ring border and square it. There's one more thing we need to do before we define this constant in code. When the ball's position is being checked against the ring border, it appears to be out when the center of the ball passes across the border. That makes it look like the ball is overlapping the ring border a bit, which is not the desired situation. Instead, we want the ball to bounce off the ring border as soon as the ball's edge touches the ring border. We do this by subtracting the diameter of the ball from the ring border's diameter before we divide the diameter by 2 and square it. Consider the following:

 ringDist2 = Math.pow((ringBorder._width-ballCreationLocation._width)/2,2); 

When we perform our collision response, we're going to need a collision vector. Because I don't want to create a new vector object during every collision, I'll create the collision vector here and then reuse it during the collision response:

 collision_vector = {x:0,y:0}; 

Now we're ready to attach our ball container to the area into which all new balls will be placed:

 createEmptyMovieClip("ballContainer", ball_container_depth); 

We need to start the ball update interval, which causes the updateBalls function to be called every updateInterval milliseconds. This is just like any other interval we've created:

 ballUpdateInterval = setInterval(updateBalls, updateFrequency); 

Finally, we need to initialize the collision detection system. We do this with a call to initCollisionGrid , as follows:

 initCollisionGrid(); 

That completes initPhysics , so we can close it.

 } 
The createBall Function

You've already seen the createBall call. It came at the end of the updateFireAnimation routine when the animation was finished. The createBall function is responsible for creating a real ball and putting it into play. We begin by opening createBall :

 function createBall(){ 

The first order of business is to create the actual ball. This involves an attachMovie call:

 var newball = ballContainer.attachMovie("ball",    " b"+ballContainer.getNextHighestDepth(), ballContainer.getNextHighestDepth(),    {_x:ballCreationLocation._x,_y:ballCreationLocation._y,gutterBounce:0}); 

Notice that we're attaching the ball to the ballContainer . We're also positioning it at the ballCreationLocation 's coordinates and defining a gutterBounce variable. We'll cover this in detail later, but essentially , if the ball bounces down at the bottom of the board a certain number of times, it will be removed. Because we will be using incrementation, we must have the variable initially defined for Flash 7 to do the addition properly.

We need to set an initial velocity for our ball. I've done this by looking at the end of the chute and guessing the direction the ball should be moving when it comes out. It looks to be about a 2.5-to-1 ratio between change in x and change in y. The easiest way to set the initial velocity is to set the direction of the velocity and then scale it to a magnitude determined by the force the user put into his flipper for this shot. First we set the velocity direction:

 newball.vel = {x:-2.5, y:-1}; 

Now we need to find a scalar to use on the velocity. I've decided to do this using a constant plus the force used when shooting ( loadedBallForce ). By playing around a bit with these numbers so that the fastest the ball can be shot is around the maximum velocity, we get the following line:

 var newforce = 3.5 + loadedBallForce*.065; 

These numbers aren't quite as arbitrary as a few of the other constants have been. We've already set the maximum speed to 10, so I wanted to choose numbers that would produce a range of values that would go all the way up to but not exceed 100. I also don't want the ball to come out of the chute with a value of 0. I'd prefer the speed to be at least 3.5. With a range of 3.5 to 10, that leaves a wiggle room of 6.5. Because the loaded ballForce variable can be anywhere from 0 to 100, I took the value of 6.5 and divided it by the maximum value of loadedBallForce , which is 100. This gives us the value .065. If loadedBallForce is 0, newforce is 3.5; if loadedBallForce is 100, newforce is 10.

Now that we have our newforce , we need to scale our velocity to match it. We have a function called scaleVectorToMag from our marble example. I'm not going to reiterate that function here, but it will be in the complete code listing at the end of this chapter. It's also given with explanation in the marble examples earlier this chapter. Just make sure that your own code has this function defined in it. Also make sure that you have included the dist function in this game. This might be a good place to start your own physics toolbox ActionScript file.

Before we can scale the magnitude of our velocity, we need to find its current magnitude. (It's required for the call to scaleVectorToMag .) We'll use the distance formula like we did in the previous marble examples, as follows:

 var oldmag = dist(0,0,newball.vel.x, newball.vel.y); 

Now we're ready to make our call to scaleVectorToMag , as follows:

 scaleVectorToMag(newball.vel, oldmag, newforce); 
Note  

Be sure to add the functions from the "Implementing Physics Functions" section to your .fla.

There is one more property we need to set for our new ball. When the ball first leaves the chute, it is outside the ring border. When we develop our collision test with the ring border, we're going to get hits immediately after creating the ball. To fix this, I'm giving the new ball a property called justSpawned . The hit testing for the ring border does not begin until the ball hits at least one pin. When the ball hits its first pin, its justSpawned property is set to false and the ring border collision tests begin:

 newball.justSpawned = true; 

That's it for createBall , so we close it:

 } 
The destroyBall Function

When a ball goes into a pocket or into the gutter, we need to get rid of it. We do this with the destroyBall function. There isn't much to do for this implementation; all we really need is to remove the movie clip to be destroyed :

 function destroyBall(ball){      ball.removeMovieClip(); } 

As you can see, the clip targeted for destruction is given to the destroyBall function as an argument named ball . We remove it with a call to removeMovieClip .

The updateBalls Function

The updateBalls function is being called by an interval that we set up in the initPhysics function. It's responsible for updating the position of every ball on the screen on a regular basis. We begin by opening the function:

 function updateBalls(){ 

We need to update every ball in the container, so we use a for in loop to iterate them:

 for(b in ballContainer){          var ball = ballContainer[b]; 

As you can see, the movie clip in each iteration is named ball . We begin the update by recording ball 's current position in case of a collision after the update:

 oldPosition = {x:ball._x,y:ball._y}; 

Now we need to apply gravity to the velocity. Because there are no user forces during the life of the ball (only collisions and gravity), we can ignore acceleration and apply gravity directly to the velocity. We already have the gravitational constant defined as grav from initPhysics , so we can apply it to the velocity's y component now:

 ball.vel.y += grav; 

The previous application of gravity might have caused our ball's velocity to exceed its maximum. To fix this, we first find the magnitude of the new velocity. If this number exceeds the maximum velocity, we scale the new velocity down using scaleVectorToMag :

 var currentMag = dist(0,0,ball.vel.x, ball.vel.y);          if(currentMag > max_vel)                scaleVectorToMag(ball.vel, currentMag, max_vel); 

Now we're ready to update the ball's position based on its new velocity. We do this by adding the velocity to the ball's position, just like we did with the marbles:

 ball._x+=ball.vel.x;        ball._y+=ball.vel.y; 

The ball is now in its updated position, so we're ready to do collision detection. There are three stages to this. We have to test the ball against other balls, against the ring border, and against the pins.

To reduce the amount of work that our collision system does, we need to short circuit when we get a collision. In other words, after we find a collision, we move the ball back to its previous position and reflect the velocity. At this point, we don't need to do further collision tests. To keep track of when we want to short-circuit , we use a boolean flag called gotAHit , as follows:

 gotAHit = false; 

At this point, we have our ball to use for testing, and we need to test it against all the other balls. This means we're going to need another for in loop to iterate over the balls again. As in our marble example, we need to be careful with our naming:

 for(b2 in ballContainer){              ball2 = ballContainer[b2]; 

Now we have two balls: ball and ball2 . These need to be hit tested against each other, but we don't want to perform the test if ball and ball2 refer to the same ball.

We can test to make sure ball and ball2 are different in the same line as our hit test. The hit test is done by testing the ball 's width (radius times 2) against the distance between the balls. This results in the following conditional:

Note  

Because we're iterating over all the balls twice in a nested fashion, sometimes ball2 will refer to the same ball as ball . We don't want to hit test a ball against itself.

 if(ball != ball2 && dist(ball._x, ball._y, ball2._x,                     ball2._y) <= ballCreationLocation._width){ 

As you can see, we first test ball against ball2 . Then we test the dist between the balls against the width (diameter) of the ballCreationLocation movie clip, which is just a regular ball instance that happens to always be sitting there. If this entire statement is true , we have a hit. When that happens, we set gotAHit to true , call our collision resolution function for ball-against-ball collisions, and close the if statement, as follows:

 gotAHit = true;                resolveCollisionBall(ball, ball2);            } 

If a hit is found, we need to short-circuit the current for in loop. We do this as follows:

 if(gotAHit) break; 

Now when a hit is found, we can break out of the ball-on-ball for in loop. This completes that for in loop, so we can close it:

 } 

Now we're ready to do the ring testing. We can skip this collision test under two circumstances. If the ball has its justSpawned property set to true or if we've already gotten a hit (in the ball-versus-ball test), we can safely ignore this collision test. We combine tests for those conditions in the same line with the hit test itself. The hit test uses the ringDist2 that we determined in initPhysics to test against the distance from the ball to the ring's center squared. These three pieces, when combined, give us the following script:

 if(!gotAHit && !ball.justSpawned &&    dist2(ball._x,ball._y,ringBorder._x,ringBorder._y)>ringDist2){ 
Caution  

This part of the script functions because the registration point for the ringBorder movie clip is at the center of the circle. Because of this, the x and y properties of ringBorder can be used with dist2 to find the distance of the ball from the center of the ring.

When everything is true , we have a collision with the ring border. When that happens, we resolve the velocity of the ball by calling resolveCollisionRing with the ball as an argument:

 resolveCollisionRing(ball); 

We have one more task to finish the ring collision test. When the ball hits the bottom of the ring border, the gutter needs to suck it down and destroy it. However, we don't want to remove the ball the first time it touches the ring border while touching the gutter. Instead, I would like to see the ball bounce a bit at the bottom of the ring before being claimed by the gutter and removed from the game. We do this by counting bounces that occur while the ball is touching the gutter. To implement this, we check to see if the _y position of the ball is greater than the gutter's _y position. If this happens during a ring hit, we increment a property of the ball called gutterBounce . When gutterBounce exceeds five bounces, we remove the ball by calling destroyBall :

 if(ball._y > gutter._y + ball._width){               if(ball.gutterBounce++ > 5){                    destroyBall(ball);               }           } 

That completes the ring border test, so we close the if statement, which opened the test:

 } 

We're finally ready to test against the pins. We do this using an else if , which follows the conditional testing of the ring border. The condition that is tested is gotAHit ; if a hit was found earlier with the ball-to-ball or ring border tests, we don't test the pins:

 else if(!gotAHit){ 

When this is true , we need to test all the pins. The entire pin hit testing system is complex, so I have encapsulated it in a function called testCollisionGrid , which is called with the ball as an argument:

 testCollisionGrid(ball); 

We close the else if that opened the pin testing block:

 } 

That completes all hit testing, so we can close the for in loop that iterated over every ball for testing:

 } 

We've completed the updateBalls function, so we close it as well:

 } 
The Pin Collision System

We need to design a system to test the ball against all the pins in the pinContainer , but testing each one individually increases the CPU load far too much to have a smooth game. To combat this problem, we need to devise something better.

Note  

We must test adjacent zones when the ball's new position is in one zone but its previous position is in another zone.

One technique that can help us is often called zoning . The idea is that if we divide the pins into zones, we can determine which zone the ball is in and then only hit test pins in the same or adjacent zones.

When we're using this system, we must make the zones as small as possible without making them too small. If the zones are too large, we won't pick up much added efficiency. In contrast, if the zones are too small, the ball could skip from over a zone during an update, which could potentially leave out collision tests and result in a collision. Figure 11.26 shows the board with an overlaid grid showing a possible zone strategy.

click to expand
Figure 11.26: Zones are created such that each pin is contained in one zone. We can then test only the zones that are adjacent to the ball for collisions.

The best way to look at exactly what's going on with our zones is to implement the function to set things up.

The initCollisionGrid Function

The key for the pin collision system is to build a grid with correctly sized zones. If we allow our zones to be squares with a width and height equal to the maximum velocity, we'll have a system in which we have to test at four zones. We'll worry about exactly which zones need to be tested in a later function ( testCollisionGrid ). For now, let's stay on track with the building of our zones.

As I said, we're going to make the zone size based on the maximum velocity. We're going to use a two-dimensional array to represent the zones. We need to develop a way to take the position of a pin or ball and convert it into a row and column index for our zone array. Each array location (zone) will contain an array that will be filled with a reference to every pin in that zone.

After we put all the pins in the appropriate zone, we can find the zone indexes of the ball and do collision tests in the correct zones only.

To begin, we open the function:

 function initCollisionGrid(){ 

Now we need to figure out exactly how many zones there will be. We do this by dividing the width of the pinContainer by the maximum velocity to find the number of rows. We do the same with the width of the pinContainer to find the number of columns , as follows:

 numberOfRows = Math.floor(pinContainer._height / max_vel)+1;         numberOfCols = Math.floor(pinContainer._width / max_vel)+1; 

Notice that I'm using Math.floor . That's to ensure that we don't end up with a decimal answer. We'll use the same equation when determining the ball's indexes later, so everything will work fine.

Now we can declare our collision zone array, which I have named collisionGrid :

 collisionGrid = new Array(numberOfRows); 

We must create a set of nested loops such that each row contains an array of columns and each location in the column contains an array to store the set of pin references for that zone. This results in the following script:

 for(var i=0;i<numberOfRows;++i){           collisionGrid[i] = new Array(numberOfCols);           for(var j=0;j<numberOfCols;++j){                collisionGrid[i][j] = new Array();           }      } 

Now our collisionGrid is ready to go. We must iterate over the pins and place each one in the proper location in the grid. As usual, we use a for in loop to iterate over the pins:

 for(p in pinContainer){           var pin = pinContainer[p]; 

We need to find the index of each pin. We do this in the same way that we found the actual number of zones:

 var gridRow = Math.floor(pin._y/max_vel);           var gridCol = Math.floor(pin._x/max_vel); 

We can add this pin to the correct zone by pushing it into the array contained at the correct row and column index, as follows:

 collisionGrid[gridRow][gridCol].push(pin); 

The pin reference is stored in a third dimension of the array. This is because each zone can contain more than one pin. When we do a comparison with the pins of a particular zone, we can use a for loop to access an element of that nested array.

That completes the loop for every pin as well as our initCollisionGrid function, so we close both:

 } } 

The testCollisionGrid Function

We have our collision grid all set up. Now we need to choose the correct zones to test. Our actual hit tests will be done with the testCollisionZone function, explained later. Our current function, testCollisionGrid , has the responsibility of choosing the correct zones to test and calling testCollisionZone on each.

We could try to get tricky and figure out algebraically exactly which zones to test, but the added math would be cumbersome, difficult to explain, and give us little extra efficiency. Because our grid is so small, there are never more than a few pins in any zone. Doing extra work just to reduce the number of zones we need to test by a couple of zones won't do much. Instead, we're going to test more zones than we need to because that's easy and nearly as efficient as figuring out exactly which zones the ball traveled through.

Tip  

One of the reasons that figuring out the exact zones to test is difficult is because the ball can be in one zone throughout its entire update but collide with a pin in an adjacent zone. That's because the center of the ball determines the zone the ball is in, but the ball can collide at its edge, which might be in a zone adjacent to the zone the ball is in. This could cause missed tests, so we would have to be careful to test adjacent zones when the ball could be overlapping. Instead of worrying about these things, we're going to use a bit of brute force to check every adjacent zone.

If we test the zone the ball is at after the update, as well as the zones that are adjacent horizontally, vertically, and diagonally (nine zones total), we'll be assured that any collision will show up.

To implement this, we begin by opening the function:

 function testCollisionGrid(ball){ 

Now we need to determine which zone the ball is in. We do this in the same way that we determined where the pins were:

 var testRow = Math.floor(ball._y/max_vel);      var testCol = Math.floor(ball._x/max_vel); 

Now we're ready to do our nine zone tests. We want to short-circuit these tests such that if a collision is found, no more testing is done. We do this by using a boolean flag called hit . We'll assign hit the return value from testCollisionZone . The testCollisionZone function returns true when a collision has been found, so we can use hit to short-circuit. Consider the following block of code, which does our nine hit tests with short-circuiting :

 var hit=testCollisionZone(ball, collisionGrid[testRow][testCol]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow-1][testCol]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow][testCol-1]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow+1][testCol]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow][testCol+1]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow-1][testCol-1]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow-1][testCol+1]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow+1][testCol-1]);      if(!hit)hit=testCollisionZone(ball, collisionGrid[testRow+1][testCol+1]); 

Notice the way I'm adding and subtracting 1 from the indexes to get the adjacent zones. This set of nine zones guarantees that we'll find all hits (with the exception of hits that might have been missed due to skipping).

That completes the grid testing, so we can close the function:

 } 

The testCollisionZone Function

The testCollisionZone function is called to test one ball against all the pins in one zone. This should be pretty easy ”nothing you haven't done before. We begin by opening our function:

 function testCollisionZone(ball,zone){ 

We need to iterate over every pin in the zone given to us as an argument. That means a for loop, like the following:

 for(var i=0;i<zone.length;++i){           var pin = zone[i]; 

Now comes the actual collision test. We check the distance from ball to pin squared against the square of the ball's radius ( ballRadius2 ):

 if (dist2(ball._x, ball._y, pin._x, pin._y) <= ballRadius2){ 

When this is true , we have a hit. In that case, we need to set gotAHit to true for short-circuiting:

 gotAHit = true; 

Next, we need to remove the ball's justSpawned status so that it is tested against the ring border in later updates:

 ball.justSpawned = false; 

Now we're ready to resolve the collision with a call to resolveCollisionPin :

 resolveCollisionPin(ball, pin); 

That completes the block of code under the hit test, so we can close it:

 } 

Now we need a short circuit so that if the test we just did has a collision, we can stop testing. Remember that the testCollisionZone function is supposed to return true when a collision occurs. We short circuit and return the true value at the same time, as follows:

 if(gotAHit)return true; 

That completes the pin iteration loop, so we close it and the function:

 } } 

That completes the collision detection system. All that's left is to resolve collisions.

The Collision Response System

This part should be pretty easy. We're going to be doing the same kind of collision response we used in the marble examples. We have three kinds of collisions to deal with. As you know, I've put each type in its own function. We'll begin with the most basic of the functions: the collision between ball and pin.

The resolveCollisionPin Function

We're going to reuse the circle-to-point collision response algorithm that we developed in our marble example. It's almost the same code with a few variable names changed to reflect the variables in Pachinko . We begin by opening the function:

 function resolveCollisionPin(ball, pin){ 

The first thing we do when we have a collision is to reset the ball to its old position:

 ball._x = oldPosition.x;      ball._y = oldPosition.y; 

Now we need to record the magnitude of the ball's velocity for later use in scaling:

 var oldMag = dist(0, 0, ball.vel.x, ball.vel.y); 

It's time to construct the collision vector, as follows:

 collision_vector = {x: pin._x - ball._x, y: pin._y - ball._y}; 

We normalize the velocity and collision vectors:

 normalize(ball.vel);      normalize(collision_vector); 

Then we take the dot product of the two vectors:

 var tempDotProd = dotProduct(ball.vel, collision_vector); 

Next, we execute the velocity reflection:

 ball.vel.x -= 2 * tempDotProd * collision_vector.x;      ball.vel.y -= 2 * tempDotProd * collision_vector.y; 

We need to determine the new magnitude for the velocity:

 var currentMag = dist(0, 0, ball.vel.x, ball.vel.y); 

Now we're ready to scale the velocity so that it matches the precollision velocity in magnitude:

 scaleVectorToMag(ball.vel, currentMag, oldMag * collision_scalar); 

There is just one thing left to do now. If the ball has hit a special pin, we should remove it from the game and give the player a few more balls in his cup. I've implemented the special pins by placing a bit of script on the first frame of the specialPin symbol, which sets the specialPin property of the pin to true . In other words, if you have a random pin called pin and you want to test to see if it's a special pin, you can use if(pin.specialPin) , which only returns true if the pin is a special pin.

Note  

Recall that special pins sit at the bottom of a pocket to check when the user has dropped a ball into that pocket.

We test to see if the collision we just resolved was a special pin:

 if(pin.specialPin){ 

If it was a special pin, we increase the number of balls in the cup:

 balls += 4; 

Finally, we destroy the ball:

 destroyBall(ball); 

That completes the special pin work, so we can close that if block:

 } 

We're done with the collision response, so we close the function:

 } 

The resolveCollisionBall Function

To resolve collisions between balls, we're going to use the same strategy we used in our previous marble example. We begin by opening the function:

 function resolveCollisionBall(ball1, ball2){ 

We put the ball back where it was before the update:

 ball1._x = oldPosition.x;      ball1._y = oldPosition.y; 

Then we record the old magnitude for use later in scaling:

 var oldMag = dist(0, 0, ball1.vel.x, ball1.vel.y); 

After that, we find the collision vector:

 collision_vector = {x: ball2._x - ball1._x, y: ball2._y - ball1._y}; 

We then normalize both vectors:

 normalize(ball1.vel);      normalize(collision_vector); 

We find the dot product:

 var tempDotProd = dotProduct(ball1.vel, collision_vector); 

Next, we use our reflection formula to reflect the vector:

 ball1.vel.x -= 2 * tempDotProd * collision_vector.x;      ball1.vel.y -= 2 * tempDotProd * collision_vector.y; 

Then we determine the magnitude of the current velocity:

 var currentMag = dist(0, 0, ball1.vel.x, ball1.vel.y); 

We scale up the first ball's velocity:

 scaleVectorToMag(ball1.vel, currentMag, oldMag*collision_scalar/2); 

Next, we reflect the second ball's velocity:

 ball2.vel.x += 2 * tempDotProd * collision_vector.x;      ball2.vel.y += 2 * tempDotProd * collision_vector.y; 

We determine the current magnitude of the second ball:

 currentMag = dist(0, 0, ball2.vel.x, ball2.vel.y); 

Then we scale up the second ball's magnitude:

 scaleVectorToMag(ball2.vel, currentMag, oldMag*collision_scalar/2); 

That completes our ball-to-ball resolution, so we close the function:

 } 

The resolveCollisionRing Function

This last function is going to be easy because we should be able to use our existing ball-to-pin resolution function to handle it. We begin by opening our function:

 function resolveCollisionRing(ball){ 

We can use resolveCollisionPin if we find the correct point to use as our pin. We do this by comparing the ball's position with the ring border's position to find the point on the ring where the collision is taking place. We use this information as an object handed to the pin argument of resolveCollisionPin , as follows:

 resolveCollisionPin(ball, {_x:ball._x+(ball._x-ringBorder._x),           _y:ball._y+(ball._y-ringBorder._y)}); 

That completes the ring border resolution, so we can close the function:

 } 

Testing

During testing, we discover a problem. Sometimes the ball gets stuck on a pin that it bounced on vertically. My theory is that from doing the scaling on the velocity, we're loosing horizontal velocity that would normally keep us from getting stuck.

We could set up some script to test this hypothesis, but it would probably be faster to implement a solution. If the solution removes our problem, my hypothesis was correct.

To implement our solution, we need to look into the collision resolution routine. After the collision has been resolved, we're going to look at the X component of the velocity. If this component is below a given number, we'll bump it up so that the ball is never perfectly balanced on the pin.

We begin by defining this minimum horizontal speed inside the initPhysics function as follows:

 minimum_horizontal_velocity = .5; 

Now we can go into our resolveCollisionPin function. Just after we scale the velocity to its original magnitude, we add the following script, which forces the horizontal component of the velocity up to the minimum given earlier:

 if(Math.abs(ball.vel.x) < minimum_horizontal_velocity){           if(ball.vel.x >= 0) ball.vel.x = minimum_horizontal_velocity;           else ball.vel.x = -minimum_horizontal_velocity;      } 

After testing this script, I was unable to get the ball stuck anymore.

Oddly enough, if you play enough real pachinko games, you'll find that the ball sometimes becomes stuck. In our game, Pachinko , when a ball gets stuck, you can bump the ball loose by hitting it with another ball. (I'm glad we implemented the ball-versus-ball collision resolution.) Because I had more fun trying to knock stuck balls loose than I did at any other point during the development of this game, I've come to like the idea of balls getting stuck once in a while.

Complete Code Listing

It's time for the last complete code listing in this book. Use this listing to see all the Pachinko code in context:

 initGame(); function initGame(){      total_balls = 25;      balls = 0;      powerBar.bar._yscale = 0;      drawback=false;      animationFrequency = 30;      fakeBall._visible = false;      ballCreationLocation._visible = false;      ball_container_depth = 1;      instruction_depth = 2;      loadBall();      initPhysics();      createSoundManager();      flipper.onRelease=function(){throwAway=0} } this.onMouseDown = function(){      if(instructions){           instructions.removeMovieClip();           balls = total_balls;           loadBall();      }      else if(flipper.hitTest(_xmouse,_ymouse)){           drawback = true;           drawbackOrigin = _root._ymouse;      } } this.onMouseUp = function(){      if(drawback && drawbackOrigin - _ymouse > 0){           drawback = false;           loadedBallForce = powerBar.bar._yscale;           flipper._rotation = 0;           powerBar.bar._yscale = 0;           fireBall();      } } this.onMouseMove = function(){      if(drawback){           powerBar.bar._yscale = drawbackOrigin - _ymouse;           if(powerBar.bar._yscale > 100)                powerBar.bar._yscale = 100;           else if(powerBar.bar._yscale < 0)                powerBar.bar._yscale = 0;           flipper._rotation = powerBar.bar._yscale/5;      } } function loadBall(){      if(balls){           balls;           fakeBall._visible = true;           loadedBall=true;      }      else if(!instructions){           attachMovie("instructions"," instructions", instructions_depth);      } } function fireBall(){      if(loadedBall){           loadedBall = false;           fireBallAnimationInterval = setInterval(updateFireAnimation,    animationFrequency);      } } function updateFireAnimation(){      if(fakeBall._currentframe != fakeBall._totalframes){           fakeBall.nextFrame();      }      else {           fakeBall.gotoAndStop(1);           fakeBall._visible = false;           loadBall();           clearInterval(fireBallAnimationInterval);           createBall();      } } function initPhysics(){      max_vel = 10;      grav = .7;      updateFrequency = 30;      collision_scalar = .6;      minimum_horizontal_velocity = .5;      ballRadius2 = Math.pow(ballCreationLocation._width/2,2);      ringDist2 = Math.pow((ringBorder._width-ballCreationLocation._width)/2,2);      collision_vector = {x:0,y:0};      createEmptyMovieClip("ballContainer", ball_container_depth);      ballUpdateInterval = setInterval(updateBalls, updateFrequency);      initCollisionGrid(); } function createBall(){      var newball = ballContainer.attachMovie("ball",    "b"+ballContainer.getNextHighestDepth(), ballContainer.getNextHighestDepth(),    {_x:ballCreationLocation._x,_y:ballCreationLocation._y,gutterBounce:0});    newball.vel = {x:-2.5, y:-1};      var newforce = 3.5 + loadedBallForce*.065;      var oldmag = dist(0,0,newball.vel.x, newball.vel.y);      scaleVectorToMag(newball.vel, oldmag, newforce);      newball.justSpawned = true; } function destroyBall(ball){      ball.removeMovieClip(); } function updateBalls(){      for(b in ballContainer){           var ball = ballContainer[b];           oldPosition = {x:ball._x,y:ball._y};           ball.vel.y += grav;           var currentMag = dist(0,0,ball.vel.x, ball.vel.y);           if(currentMag > max_vel)                scaleVectorToMag(ball.vel, currentMag, max_vel);           ball._x+=ball.vel.x;           ball._y+=ball.vel.y;           gotAHit = false;           for(b2 in ballContainer){                ball2 = ballContainer[b2];                if(ball != ball2 && dist(ball._x, ball._y, ball2._x, ball2._y) <=    ballCreationLocation._width){                     gotAHit = true;                     resolveCollisionBall(ball, ball2);                }                if(gotAHit) break;           }           if(!gotAHit && !ball.justSpawned &&    dist2(ball._x,ball._y,ringBorder._x,ringBorder._y) > ringDist2){                resolveCollisionRing(ball);                if(ball._y > gutter._y + ball._width){                     if(ball.gutterBounce++ > 5){                          destroyBall(ball);                     }                }           }           else if(!gotAHit){                testCollisionGrid(ball);           }      } } function initCollisionGrid(){      numberOfRows = Math.floor(pinContainer._height / max_vel)+1;      numberOfCols = Math.floor(pinContainer._width / max_vel)+1;      collisionGrid = new Array(numberOfRows);      for(var i=0;i<numberOfRows;++i){           collisionGrid[i] = new Array(numberOfCols);           for(var j=0;j<numberOfCols;++j){                collisionGrid[i][j] = new Array();           }      }      for(p in pinContainer){           var pin = pinContainer[p];           var gridRow = Math.floor(pin._y/max_vel);           var gridCol = Math.floor(pin._x/max_vel);           collisionGrid[gridRow][gridCol].push(pin);      } } function testCollisionGrid(ball){      var testRow = Math.floor(ball._y/max_vel);      var testCol = Math.floor(ball._x/max_vel);      var hit = testCollisionZone(ball, collisionGrid[testRow][testCol]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow-1][testCol]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow][testCol-1]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow+1][testCol]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow][testCol+1]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow-1][testCol-1]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow-1][testCol+1]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow+1][testCol-1]);      if(!hit) hit = testCollisionZone(ball, collisionGrid[testRow+1][testCol+1]); } function testCollisionZone(ball,zone){      for(var i=0;i<zone.length;++i){           var pin = zone[i];           if (dist2(ball._x, ball._y, pin._x, pin._y) <= ballRadius2){                gotAHit = true;                ball.justSpawned = false;                resolveCollisionPin(ball, pin);           }           if(gotAHit)return true;      } } function resolveCollisionPin(ball, pin){      ball._x = oldPosition.x;      ball._y = oldPosition.y;      var oldMag = dist(0, 0, ball.vel.x, ball.vel.y);      collision_vector = {x: pin._x - ball._x, y: pin._y - ball._y};      normalize(ball.vel);      normalize(collision_vector);      var tempDotProd = dotProduct(ball.vel, collision_vector);      ball.vel.x -= 2 * tempDotProd * collision_vector.x;      ball.vel.y -= 2 * tempDotProd * collision_vector.y;      var currentMag = dist(0, 0, ball.vel.x, ball.vel.y);      scaleVectorToMag(ball.vel, currentMag, oldMag * collision_scalar);      if(Math.abs(ball.vel.x) < minimum_horizontal_velocity){           if(ball.vel.x >= 0) ball.vel.x = minimum_horizontal_velocity;           else ball.vel.x = -minimum_horizontal_velocity;      }      if(pin.specialPin){           balls += 4;           destroyBall(ball);      } } function resolveCollisionBall(ball1, ball2){      ball1._x = oldPosition.x;      ball1._y = oldPosition.y;      var oldMag = dist(0, 0, ball1.vel.x, ball1.vel.y);      collision_vector = {x: ball2._x - ball1._x, y: ball2._y - ball1._y};      normalize(ball1.vel);      normalize(collision_vector);      var tempDotProd = dotProduct(ball1.vel, collision_vector);      ball1.vel.x -= 2 * tempDotProd * collision_vector.x;      ball1.vel.y -= 2 * tempDotProd * collision_vector.y;      var currentMag = dist(0, 0, ball1.vel.x, ball1.vel.y);      scaleVectorToMag(ball1.vel, currentMag, oldMag*collision_scalar/2);      ball2.vel.x += 2 * tempDotProd * collision_vector.x;      ball2.vel.y += 2 * tempDotProd * collision_vector.y;      currentMag = dist(0, 0, ball2.vel.x, ball2.vel.y);      scaleVectorToMag(ball2.vel, currentMag, oldMag*collision_scalar/2); } function resolveCollisionRing(ball){      resolveCollisionPin(ball, {_x:ball._x+(ball._x-ringBorder._x),    _y:ball._y+(ball._y-ringBorder._y)}); } function dist(x1,y1,x2,y2){      return Math.sqrt(dist2(x1,y1,x2,y2)); } function dist2(x1,y1,x2,y2){      return Math.pow(x1-x2,2) + Math.pow(y1-y2,2); } function normalize(v){      var d = dist(0, 0, v.x, v.y);      v.x = v.x / d;      v.y = v.y / d; } function scaleVectorToMag(v, origionalMag, targetMag){      var changeFactor = targetMag/origionalMag;      v.x = v.x * changeFactor;      v.y = v.y * changeFactor; } function dotProduct(v1, v2){      return (v1.x * v2.x) + (v1.y * v2.y); } 
Caution  

Make sure that your code has the functions dist, dist2, normalize, scaleVectorToMag, and dotProduct . The code used these functions, but the functions were not explicitly defined in the body of the Pachinko game description.




Macromedia Flash MX 2004 Game Programming
Macromedia Flash MX 2004 Game Programming (Premier Press Game Development)
ISBN: 1592000363
EAN: 2147483647
Year: 2004
Pages: 161

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