Now you'll learn how to use Lingo in order to animate the enemy sprites. Animating a sprite using Lingo is actually very simple. The following behavior, attached to any sprite, would cause the sprite to move two pixels to the right, on every frame:
on exitFrame me sprite(me.spriteNum).locH = sprite(me.spriteNum).locH + 2 end
What this does is take the sprite's current locH, add 2 to it, and reassign it to the sprite, causing it to move two pixels to the right. Simple enough, right? Well, yes and no. The problem is that moving a sprite this way is tied to the movie's frame rate. Not all systems are created equal, so you have no assurance that the frame rate you set will be honored on all computers. To overcome this, you will use time-based animation methods. This is the way all commercial titles are programmed, and something you should definitely become familiar with.
Select the internal cast, then choose the first empty slot: member 1. Press Ctrl/Command+0 to open a script window. Make certain the script type is set to Movie in the Property inspector, then enter the following Lingo:
on startMovie _global.theLevel = 1 end
You'll use the global variable theLevel to keep track of the game level the player is on, and also determine the speed at which the enemies move. Each time the player completes all four levels, theLevel will be incremented by 1.
Right-click on the alien_ship1 sprite in channel 4 within the first level, and select Script from the context menu. Create the following behavior:
property sp, lastTime, moveAmount on beginSprite me sp = sprite(me.spriteNum) moveAmount = (900.0 / (21 - _global.theLevel)) / 1000 lastTime = _system.milliseconds end on enterFrame me currTime = _system.milliseconds timePassed = currTime - lastTime amtToMove = timePassed * moveAmount sp.locH = sp.locH - amtToMove if sp.locH < -50 then sp.locH = 850 lastTime = currTime end
Before we see it in action, let's talk about how this behavior works to move the sprites. First, the variable sp is set to whatever sprite the behavior is attached to. Next, moveAmount is set to a number indicating the number of pixels to move per millisecond. The game board is 800 pixels wide, and you want the ship to disappear off the left edge and then reappear on the right edge, so that it continues to loop until it is shot down. So that the ship doesn't just blink off the left edge and reappear at the right edge, a 50-pixel buffer is used on either side of the Stage, resulting in the ship moving a total of 900 pixels.
A look at the following image will help clarify this:
As you can see, when the enemy ship hits the -50 mark, it is reset to 850 so that it glides in from Stage right. In other words, the sprite moves a total of 900 pixels. That 900 is then divided by the number of seconds in which to move the sprite to get the number of pixels per second. For Level 1, I decided to have the enemies cross the screen in 20 seconds, which is fairly slow:
moveAmount = 900.0 / (21 _global.theLevel)
Because theLevel is set to 1 initially, 900 will be divided by 20, resulting in the sprite moving at a rate of 45 pixels per second. That number is then divided by 1000 to get the number of pixels per millisecond, the unit of time you'll be using:
moveAmount = (900.0 / (21 - _global.theLevel)) / 1000
Using a single floating-point number in the calculation (900.0) guarantees that the result of the calculation will also be a float. Once moveAmount is calculated, the lastTime property is set to the current value of the milliseconds using:
lastTime = _system.milliseconds
The lastTime property will be used to calculate the number of milliseconds that have elapsed since the last frame update. If 80 milliseconds have elapsed and the sprite should be moved .045 pixels per millisecond, the sprite would need to move about 3.5 pixels.
This is taken care of by the enterFrame handler. On each frame it first gets the current value of the milliseconds and then subtracts from that the last time to calculate the time passed since the last frame:
timePassed = currTime lastTime
Next, the amount to move is found by multiplying the time passed by the amount to move per millisecond:
amtToMove = timePassed * moveAmount
The amount to move is then subtracted from the sprite's current locH and reassigned to the sprite, causing the sprite to move to the left:
sp.locH = sp.locH - amtToMove
The locH is then checked to see if it is less than -50. If it is, the sprite is placed at a locH of 850 so that it reappears on the right side of the screen. Finally, the lastTime property is set to the current value of the milliseconds. This is necessary so that the elapsed time can again be calculated on the next frame loop.
Name the script enemy_move and close the script window. Right-click in the Score's frame bar to enable the effects channels if they are not on. Double-click the tempo channel at frame 1 and set the tempo to 60 frames per second.
A frame rate of 60 fps will produce a nice smooth motion on machines fast enough to handle it. On slower machines, the time-based animation will simply produce larger amounts to move per frame, but the sprite will still travel across the screen in the same amount of time.
Turn on the Score's sprite toolbar if it's not on, by again right-clicking in the frame bar and selecting Sprite Toolbar. Select all of the alien ship sprites in Level 1, except the very first one in channel 4.
You don't want to select the first sprite because the movement behavior is already attached to it.
With the sprites selected, choose the enemy_move behavior from the behavior drop-down, as shown:
The move behavior is now attached to all the alien sprites in Level 1. Now you just need to add a simple loop on frame behavior to the last frame of the level and you can see how it works.
Double-click in the Score's behavior channel at frame 25 and create the standard loop on frame behavior:
on exitFrame me _movie.go(_movie.frame) end
Name the script frame_loop and close the script window. Rewind and play the movie.
The enemy ships now travel smoothly from right to left, disappear, and then reappear at Stage right. Although you may sometimes want this kind of formation flying, these aliens are from planet Nebulon 7 and they fly much more randomly. Nice segue, right?
Varying the Speed
Not all ships and not all alien pilots are created equal, so varying the speed at which each ship travels will make their movements more natural. To do this you can use Lingo's random function, which will produce a random number between 1 and whatever number you feed to the function.
The problem with this is the granularity of the functionyou can only get integer numbers. If you vary each ship's motion by too much they will fly too fast and appear to not be "together." What you want to do is add a small fractional random number to the amount to move per millisecond. You do this by first grabbing a random integer and then dividing it by a floating-point number to reduce it.
Stop the movie if it's playing, right-click any one of the enemy ship sprites, and select Script from the context menu. Directly after the line that calculates the moveAmount property, within the beginSprite handler, add the following line:
moveAmount = moveAmount + random(1000) / 20000.0
This will produce an increase in the moveAmount of between .0001 and .05 pixels per millisecond. Although slight, this is enough to have the ships fly at a fairly random speeds but not appear to be too different from one another.
Rewind and play the movie.
Each ship now flies at a slightly different speed, making the motion more believable. To make the ships fly in an even more natural way, you can also alter their vertical position as they travel. If you use a sine wave, for instance, you can make the ships travel with an up-and-down, wave-like motion instead of in a straight line.
Varying the Path
Using a sine, or cosine, function is a popular way to introduce a wave-like motion to an object. Although a complete explanation of how the sine function works isn't possible in the context of this book, examine the following illustration for some insight into how you can achieve periodic motion with it:
As you vary the angle around the circle from points 1 to 8, a corresponding sine of that angle produces a number from -1 to +1 and is shown on the sine graph. As you can see, the sine of 90º, at point 3, is +1, while the sine of 270º, at point 7, is -1. If the angle increases beyond 360º it doesn't matterthe sine of 450º is the same as the sine for 90º: +1. In this manner you can have a variable, call it myAngle, that is initialized to 0. On every update you can increase the angle by some amount and get its sine, which will always vary between -1 and +1. You then take that number and add it to the current locV of the sprite, resulting in a constant and smooth up-and-down motion.
Stop the movie if it's playing and right-click one of the alien ship sprites. Select Script to open the script window. Within the property declarations of the behavior, add the declaration for myAngle:
Within the beginSprite handler of the behavior, add the line to initialize myAngle to 0:
myAngle = 0
You can place the line anywhere within the handler. Next, you'll add a couple of lines to the enterFrame handler that will take the sine of the angle, modify the sprite's locC property, and increment the angle.
Directly after the line that calculates amtToMove, add the following two lines of Lingo:
mySin = sin(myAngle) myAngle = myAngle + .075
First, the sine of the angle is calculated using Lingo's sin() function, and stored in the mySin local variable. Next, myAngle is incremented by .075, which will serve to produce the periodic movement we're after.
The number .075 used to increment the angle was arrived at by trial and error. By playing with different values I arrived at a number that I thought produced a fairly nice motion. Higher values will cause the sprites to move up and down more rapidly, while lower numbers will have the opposite effect. The only thing left now is to modify the sprite's locV parameter to take into account mySin, so that it will move up and down.
Add the following line of Lingo immediately after the two lines you just added:
sp.locV = sp.locV + mySin
Now, if the sine of the angle is positive, the sprite's locV will increase, causing the sprite to move down. If the sine is negative, the locV will decrease and the sprite will move up. As the angle continually increases, the sprite will continually move up and down.
Close the script window, then rewind and play the movie.
As the alien ships move across the screen at somewhat random speed, they also move up and down in a wave-like motion. However, just as with changing the speed, all of the ships now move up and down in unison. Randomizing the motion a little will further add naturalness to the motion. To do this, you can simply randomize the value that is used to increment the angle, which is currently a constant value of .075.
Because .075 produces a nice motion, you want to use values that are fairly close to it. To do this you can use the random function in a way you haven't yet seen. By giving the function two numbers instead of just one, you can determine the range of random numbers that will be determined. To come close to a value of .075 you can grab numbers between 60 and 80 and then divide the result by 1000.
Stop the movie, right-click one of the enemy ship sprites, and select Script from the context menu. Add a property declaration for the angleRand property:
Add the following line of Lingo to the beginSprite handler:
angleRand = random(60, 80) / 1000.0
By limiting the values the random function produces to between 60 and 80, the angleRand property will be set to a fractional number between .06 and .08.
Modify the line within the enterFrame handler that increments myAngle so that it uses the angleRand property instead of the constant value of .075. The line should look like the following when you're finished:
myAngle = myAngle + angleRand
Now, instead of using a constant value of .075, the angle will be incremented with a random number that is fairly close to .075.
Close the script window, and then rewind and play the movie.
The alien ships now move randomly both horizontally and vertically and look much better than when they moved in a simple straight line. Now, let's go ahead and use this same behavior for the UFOs on Level 2.
Stop the movie, and select all of the UFO sprites in Level 2. From the behavior drop-down in the sprite toolbar select the enemy_move behavior.
The UFOs will now move like the ships, with the same behavior attached. Although you could use a different behavior, or add a getPropertyDescriptionList handler and a conditional test to make the UFOs move differently from the other ships, the current behavior works well enough. For the asteroids, however, using a different behavior is appropriate, as they should move in a simple straight line.
Right-click a ship or UFO sprite and select Script from the context menu. Press Ctrl/Command+A to select all of the script, then press Ctrl/Command+C to copy it. Right-click the first asteroid in level 3, select script, then press Ctrl/Command+A to select the default mouseUp. Press Ctrl/Command+V to paste in the copied script.
Because the script is similar, you only need to remove a few lines in order to make the asteroids move the way you want.
Edit the newly pasted script, removing the code to modify the vertical position of the sprite using the sine function. Also, change the travel speed so that the asteroids take longer to cross the screen. When you're finished, the final behavior should look like this:
property sp, lastTime, moveAmount on beginSprite me sp = sprite(me.spriteNum) moveAmount = (900.0 / (30 - _global.theLevel)) / 1000 moveAmount = moveAmount + random(1000) / 20000.0 lastTime = _system.milliseconds end on enterFrame me currTime = _system.milliseconds timePassed = currTime - lastTime amtToMove = timePassed * moveAmount sp.locH = sp.locH - amtToMove if sp.locH < -50 then sp.locH = 850 lastTime = currTime end
As you can see, this is nearly identical to the original script you created for the enemy ships, before adding the random motion.
Name the script move_asteroid, then close the script window. Select the remainder of the asteroids by clicking one and then Shift-clicking the last one. Drag the new move_asteroid behavior from the cast and drop it onto the selection of sprites.
Be sure and not to select the first asteroid sprite or it will have two copies of the same behavior attached to it.
Single-click the behavior channel at frame 45 and hold down Ctrl/Command and single-click frame 65 in order to select just the two frames. From the behavior drop-down in the sprite toolbar, select the frame_loop behavior.
The end frame of levels 2 and 3 now have the standard frame loop behavior attached.
Rewind and play the movie to observe the results.
Because you can't shoot the aliens yet, there's no way to get to Levels 2 and 3. While you could open the Message window and enter: _movie.go("level2") there's an even easier way using the Score.
Press the Next Marker button to advance the playhead to Levels 2 and 3 in order to see the UFOs and asteroids move. When you're finished, stop the movie and save it. Save the movie as 2d_game into your project_three foldercreate the folder now if you haven't already.
The UFOs move like the other ships, while the asteroids travel in a straight line. Now that the enemies on these three levels are animating properly, you can create the behavior that will scroll the ground and mountains.