Project 8-2: Having a Ball

We’re just going to keep right on going with the racquetball court and paddle program you made for the first project in this module. Since this is supposed to be a racquetball game, we need to have a ball to slap around, don’t we?

The goals of this project include

Step-by-Step
  1. Select the filled ellipse tool from the trusty Tool Palette and make a small, ball-shaped sprite on the Stage. Remember, if you shift-drag on the Stage using this tool, your oval will end up being a perfect circle. Make it about 10 pixels wide by ten high, and place it in the center of your Stage for now. Shrink your ball sprite’s span so that it occupies only frame 1 of the Score. You don’t want it extending into frame 2 along with the paddle sprite. Again, I’ll show you why soon.

  2. This is where things will get interesting, because we want to do two different things with the ball. First, we want to get it moving across the screen. Then, we want to make it react appropriately to different conditions as it is moving.

    For instance, if it’s at the top, left, or right edges of the Stage, we want to make it rebound, since those represent the "walls" of our racquetball court. We also want to make it rebound off the paddle. Finally, if it happens to make it all the way to the bottom of the Stage, we want to reset the game, since the player let the ball go past him.

    Vector motion­moving a sprite in a straight line but at some particular angle­is surprisingly hard to create and make smooth on a computer. This is because, at least with Director, motion for a sprite can take place only on the minimum level of one pixel at a time, which means that we’re more or less constrained to strictly horizontal or vertical motion, or motion in diagonals of 45º, because to produce motion along any other angle, we would have to either move fractional pixels, which is impossible, or do some interesting mathematical tricks to virtually "scale up" the size of the Stage.

    We’re in luck, because James Newton (who wrote the Advanced Guide for Director 8.5) created a behavior called Vector Motion, and it’s in your Library Palette right now.

  3. Open your Library Palette and choose Animation | Interactive. Find the behavior there called Vector Motion­it’s probably the very last one in the list­and drag it right on top of your ball sprite. Don’t bother setting anything in the parameters dialog box; just click OK to continue. We don’t need to set any parameters, because we are going to write another behavior of our own that makes our ball act like a real ball, bouncing to and fro across the screen. We’re using James’s Vector Motion behavior to actually cause the ball to move, but we’ll be determining ourselves in which direction it’s going to be moving, and how fast.
  4. Click the Behavior tab of the PI and from the plus pop-up menu, select New Behavior…. Name it something useful such as "ballScript." In the script we are about to write, we’re going to have to do several things. First we’re going to have to choose a random direction we want the ball to begin moving, and then we need to tell the Vector Motion behavior which direction to move the ball.

  5. As the ball is moving, we’ll need to test its location and respond appropriately to what we learn.

    You might surmise that we’ll put this latter set of tests into an enterFrame or exitFrame event script, and that’s correct. However, we need to get the ball rolling, as it were, and so the first script we’ll create is the one that sets the initial parameters for motion. Here it is:
     

      on ChooseVector me, fHMultiplier, fVMultiplier
       
       
        fHorizDelta = float ( random ( 20, 40 ) ) * fHMultiplier
        fVertDelta = float ( random ( 20, 40 ) ) * fVMultiplier

        fHorizSend = fHorizDelta / 10.0
        fVertSend = fVertDelta / 10.0

        sendSprite ( me.spriteNum, #VectorMotion_SetVector, [fHorizSend, fVertSend] )

      END ChooseVector


    Homina homina homina. Don’t panic. I’m right here.

    on ChooseVector me, fHMultiplier, fVMultiplier

    This is a behavior handler definition. The word "me" is something you’ve seen before and might be wondering about, and I’ll tell you all about it in Part 4. For now, just remember that that "me" always has to be there when you’re doing handlers in a behavior. The other two words, fHMultiplier and fVMultiplier, are parameters. You remember parameters from Part 1. We’ve got them there because we’re going to be passing parameters to this handler once we’re all finished with our ball script. You can guess by the simplified Hungarian Notation prefixes on them that they are floating-point, or decimal, parameters that will be passed.

    fHorizDelta = float ( random ( 20, 40 ) ) * fHMultiplier

    Okay, here we’re doing some complicated-looking operations to put a value into some variable called fHorizDelta. Let’s look at what’s happening inside the parentheses first.

    random is another Lingo keyword; it returns a randomly selected integer from within a range that we specify. In this case, we are looking for a random number somewhere between 20 and 40, inclusive. Thus our random call might give us 32 the first time, 27 the second time, 40 the third time, and so on, but the random number we get will never be less than 20 nor greater than 40.

    I chose this range myself after doing some lengthy experimentation with this project, because otherwise you’d’ve had to do it and it would end up being even more stressful than the first part of this module.

    Now that we have the random number, plucked from the range of values from 20 to 40, we pass it through the float keyword. This forces the number we get to be a decimal, or floating-point number. In other words it takes the random number (say 23) and turns it into 23.0.

    After that, you can see we’re multiplying that number by our passed parameter. The next line does almost exactly the same thing, but with different variables:

    fVertDelta = float ( random ( 20, 40 ) ) * fVMultiplier

    Again, we’re taking a random number from 20 to 40, turning it into a decimal, and multiplying it by a passed parameter. Then we’re putting the result into the variable fVertDelta.

    Next we do some division.

    fHorizSend = fHorizDelta / 10.0

    This is just to slip the decimal point over by one place in the number we just calculated. If the final result for the variable fHorizDelta ended up being 36.0, this operation would convert it to 3.60. That’s all. We do the same thing to fVertDelta,

    fVertSend = fVertDelta / 10.0

    plugging the result into another variable. The next line, though, is definitely a little on the odd side:

    sendSprite ( me.spriteNum, #VectorMotion_SetVector, [fHorizSend, fVertSend] )

    What the holy cannoli does that mean? Well, as you can see, we’ve taken the variables fHorizSend and fVertSend, which we just got done calculating, and placed them inside brackets. You recall brackets from the last module. They’re special characters in the world of Lingo, denoting a list.

    But what about the #VectorMotion_SetVector part just before that?

    Believe it or not, this and our little list of variables are also parameters, very special parameters we’re sending to another piece of code in our Director movie. But where is this code to be found?

    Locate the Vector Motion behavior script in your Cast window and double-click it to open it. About two-thirds of the way down in that script, you will see a handler that begins with the line

    on VectorMotion_SetVector me, scaledVector, scaleFactor, theLoc

    Aha, that’s the handler we’re invoking, it seems, with some extra parameters attached. (You don’t really need to worry about those right now.) But this handler does not begin with a pound sign, so why are we using one in our script?

    The reason is that we are invoking this handler using a very special Lingo command, sendSprite. This command is used whenever you want to specifically send a command to a given sprite, a command that invokes a specified handler in a behavior attached to that sprite. The general format for a sendSprite call is

    sendSprite ( spriteNumber, #handlerToRun, parameters )

    Here, spriteNumber is the number of the channel that the sprite occupies in Director’s Score. #handlerToRun is the name of the handler you want the sprite to run; it is preceded by the pound sign to indicate that fact. (Macromedia could have set things up so that you didn’t have to use the pound sign, but having it there is a good thing really, because it serves as a visual reminder that we are doing some interface messaging to a behavior. The reason this distinction matters will become quite clear to you in Part 4. For now, just accept that it’s there and it’s a requirement.) Finally there are parameters, if any, that we send with the rest of the sendSprite message.

    So now when you look at the line

    sendSprite ( me.spriteNum, #VectorMotion_SetVector, [fHorizSend, fVertSend] )

    you can infer that we are sending an interface message to this sprite (me.spriteNum), telling it to run the handler named VectorMotion_SetVector, and passing two variables we calculated in a Director list format ([fHorizSend, fVertSend]). In this fashion, you can see that behaviors attached to sprites can talk to each other. This is a great way for us to be able to have our program talk to itself when we want it to, and to get it to do some really interesting things.

  6. We’re also going to put in a simple little extra handler to the current behavior that gives us a positive or negative number depending on the value of another number sent into it.

  7.  
      on ReturnVectorDirection me, fNumber
       
       
        if fNumber > 0.0 then
         
          return 1.0


        else
         

          return -1.0


        end if

      END ReturnVectorDirection


    This script just takes a parameter passed to it, fNumber, and determines if that number is greater than or less than zero. If it’s more than zero, we return (pass back) the value 1.0; if fNumber is less than zero, we return —1.0. We’ll be making use of this script to ensure that when the ball bounces it continues to head in more or less the same direction it was before. So if it’s coming from the lower left and needs to bounce off the top edge of the Stage, the ball will move toward the lower right after the bounce.

  8. These scripts are just part of the package. We also need to figure out exactly where on the Stage our ball is, and decide what to do next accordingly. For this reason, we enter the following exitFrame event script:

  9.  
      on exitFrame me
       
       
        lVector = sendSprite ( me.spriteNum, #VectorMotion_GetVector )

        if lVector = [ 0, 0 ] then
         

          if random ( 2 ) = 1 then
           
            fHMult = -1.0


          else
           

            fHMult = 1.0


          end if

          me.ChooseVector( fHMult, 1.0 )
           

        end if
           
        if sprite(me.spriteNum).left < the stage.drawRect.left then
         
          fVMult = me.ReturnVectorDirection( lVector[2] )
          me.ChooseVector( 1.0, fVMult )


        else if sprite(me.spriteNum).right > the stage.drawRect.right then
         

          fVMult = me.ReturnVectorDirection( lVector[2] )
          me.ChooseVector( -1.0, fVMult )


        else if sprite(me.spriteNum).top < the stage.drawRect.top then
         

          fHMult = me.ReturnVectorDirection( lVector[1] )
          me.ChooseVector( fHMult, 1.0 )


        else if intersect ( sprite(me.spriteNum).rect, sprite(1).rect ) <> rect ( 0, 0, 0, 0 ) then
         

          fHMult = me.ReturnVectorDirection( lVector[1] )
          me.ChooseVector( fHMult, -1.0 )


        else if sprite(me.spriteNum).bottom > the stage.drawRect.bottom then
         

          go ( the frame + 1 )


        end if

      END exitFrame


    Okay, take a couple deep breaths. This isn’t anywhere near as bad as it looks.

    lVector = sendSprite ( me.spriteNum, #VectorMotion_GetVector )

    You already know, now, what sendSprite does. All we’re doing here is asking the Vector Motion behavior to run the handler called VectorMotion_GetVector. This will return another Director list, this one containing a description of the direction in which the ball sprite has been programmed to move. We put that value into the variable named lVector.

    if lVector = [ 0, 0 ] then

    We’re determining here whether the lVector list we just got from the sendSprite call is [0, 0]. If it is, this means that the ball sprite has not yet been programmed to move. If that turns out to be the case, the next few lines are executed, which pick a random horizontal direction to get the ball moving and then set up the initial motion parameters for the ball. Here’s how it works:

    if random ( 2 ) = 1 then

    fHMult = —1.0

    else

    fHMult = 1.0

    end if

    We take a random value for the number 2, which will always be either 1 or 2, and if the value is 1 we set a variable to be negative 1. Otherwise, the value for random ( 2 ) was 2, and so we set the variable to be positive 1 instead. (As you can see, the positive or negative number is also a decimal.)

    me.ChooseVector( fHMult, 1.0 )

    Here we make a direct internal call to the ChooseVector script we created earlier, and pass along the fHMult variable whose value we just randomly set to either —1.0 or 1.0, along with another hard-set value, 1.0. By passing along a random negative or positive number in the first slot, we allow the ChooseVector script to cause the ball to tend either left or right across the screen as it begins moving. This is because a positive value for the horizontal motion variable will make the ball move more or less rightward, while a negative value will make it move more or less leftward. The positive value in the second position causes the vertical motion parameter to be positive, which will make the ball move generally downward. A negative value here would make the ball move upward.

    We want the game to start with the ball moving downward, since it will be heading toward the paddle. However we also want to let it move randomly to the left or right, so the game never begins with the ball moving in exactly the same direction every time.

    However, everyting in the if…then control structure will be skipped if lVector is not equal to [0, 0], because in that case the ball is already in motion and we need to figure out where it is and what to do next. That’s what the rest of this exitFrame event script does.

    if sprite(me.spriteNum).left < the stage.drawRect.left then

    This you recognize from your paddle script. We’re checking to see if the left edge of the ball sprite is at or beyond the left edge of the Stage. If it is, we need to make it bounce.

    fVMult = me.ReturnVectorDirection( lVector[2] )

    This calls the other little utility script we made. We grab the number at position 2 of the lVector list, which represents the current vertical motion of the ball. We then pass that number to our ReturnVectorDirection script, which determines whether the number we send it is more or less than zero, and kicks back a value based on that determination. This number turns into the vertical multiplier we’ll send along to our ChooseVector script, which will make the ball continue moving in more or less the same vertical direction, but which will reverse its horizontal direction. Thus if it was moving toward the bottom of the Stage when it hit the left wall, it will continue moving more or less downward, but it will bounce toward the right as well.

    me.ChooseVector( 1.0, fVMult )

    This line calls our ChooseVector script with a positive number for the horizontal parameter, which will make the ball move rightwards on the Stage, and with our previously calculated number for the vertical modifier, which will allow it to continue moving in the same vertical direction it was going toward before it hit the left "wall."

    else if sprite(me.spriteNum).right > the stage.drawRect.right then

    fVMult = me.ReturnVectorDirection( lVector[2] )

    me.ChooseVector( —1.0, fVMult )

    This set of lines does exactly the same as those preceding, but for the right edge of the Stage. Note that this time the horizontal modifier is a negative number, which will cause the ball to bounce to the left.

    else if sprite(me.spriteNum).top < the stage.drawRect.top then

    fHMult = me.ReturnVectorDirection( lVector[1] )

    me.ChooseVector( fHMult, 1.0 )

    Here again we’re doing something similar, this time for the top of the Stage. This time we want the ball to keep moving in more or less the same horizontal direction, which is why we determine whether the number in lVector’s first position is positive or negative, but we want its vertical direction to change so that it will bounce.

    else if intersect ( sprite(me.spriteNum).rect, sprite(1).rect ) <> rect ( 0, 0, 0, 0 ) then

    fHMult = me.ReturnVectorDirection( lVector[1] )

    me.ChooseVector( fHMult, —1.0 )

    This one might not look quite as you expected, but we’re just using Lingoesque terminology to determine if the ball has actually hit the player’s paddle. We do this by testing the intersect of the rectangles for the paddle sprite and the ball sprite. The intersect test returns the rectangle defined by the area where the two sprites overlap. If the area is defined by rect ( 0, 0, 0, 0 ), this means the sprites are not overlapping at all; they’re not even touching. If the intersect value is anything else, we know that the ball has hit the paddle, and we do another calculation to make it bounce back upward.

    else if sprite(me.spriteNum).bottom > the stage.drawRect.bottom then

    go ( the frame + 1 )

    end if

    This is the final test in our exitFrame event script; it determines if the bottom of the ball sprite is at or below the bottom of the Stage. If it is, it’s because the player missed the ball, and we need to reset the game. We do this by jumping to frame 2, which is why I had you make the paddle occupy the first two frames of the Score, while the ball occupies only frame 1.

  10. Into the framescript for frame 2, enter the following:

  11.  
      on exitFrame
        go ( the frame - 1 )
      end


    This will cause your playback head to jump back to frame 1 after it’s gone into this second frame. What this does is force a beginSprite event to happen all over again with the ball sprite, which will reset its location onscreen and start the next round of racquetball.

  12. Now go ahead and click the Play button. If all goes well, your ball sprite should start from the center of the Stage and begin moving more or less downward. If you hit it with the paddle, it should bounce back up, and whenever it hits the left, right, or top edges of the Stage, it should rebound appropriately. If you let it get past you and it goes past the bottom of the Stage, the program should reset and start you over with another ball.