All the loops we've looked at so far cause the interpreter to repeatedly execute blocks of code. Most of your loops will be of this "ActionScript-statement" type. But it's also sometimes desirable to create a timeline loop by looping Flash's playhead in the timeline. To do so, attach a series of statements to any frame; on the next frame, attach a gotoAndPlay( ) function whose destination is the previous frame. When the movie plays, the playhead will cycle between the two frames, causing the code on the first frame to be executed repeatedly.
We can make a simple timeline loop by following these steps:
Start a new Flash movie.
On frame 1, attach the following statement:
trace("Hi there! Welcome to frame 1");
On frame 2, attach the following statements:
trace("This is frame 2"); gotoAndPlay(1);
Select Control Test Movie.
When we test our movie, we see an endless stream of the following text:
Hi there! Welcome to frame 1 This is frame 2 Hi there! Welcome to frame 1 This is frame 2
Timeline loops can do two things ordinary loops cannot:
They can execute a block of code an infinite number of times without causing an error.
They can execute a block of code that requires a Stage update between loop iterations.
This second feature of timeline loops requires a little more explanation. When any frame's script is executed, the movie Stage is not updated visually until the end of the script. This means that traditional loop statements cannot be used to perform repetitive visual or audio tasks because the task results aren't rendered between each loop iteration. Repositioning a movie clip, for example, requires a Stage update, so we can't programmatically animate a movie clip with a normal loop statement.
You might assume that the following code would visually slide the ball movie clip horizontally across the Stage:
for (var i = 0; i < 50; i++) { ball._x += 10; }
Conceptually, the loop statement has the right approach -- it repetitively updates the position of ball by small amounts, which should give the illusion of movement. However, in practice, the ball doesn't move each time the _x position of ball is changed because the Stage isn't updated. Instead, we'd see the ball suddenly jump 500 pixels to the right -- 10 pixels for each of the 50 loop iterations -- after the script completes.
To allow the Stage to update after each execution of the ball._x += 10; statement, we can use a timeline loop like this:
// CODE ON FRAME 1 ball._x += 10; // CODE ON FRAME 2 gotoAndPlay(1);
Because Flash updates the Stage between any two frames, the ball will appear to animate. But the timeline loop completely monopolizes the timeline it's on. While it's running, we can't play any normal content on that timeline. A better approach is to put our timeline loop into an empty, two-frame movie clip. We'll get the benefit of a Stage update between loop iterations without freezing a timeline we may need for other animation.
The following steps show how to create an empty-clip timeline loop:
Start a new Flash movie.
Create a movie clip symbol named ball that contains a circle shape.
On the main Stage, rename layer Layer 1 to ball.
On the ball layer, place an instance of the ball symbol.
Name the instance of the ball clip, ball.
Select Insert New Symbol to create a blank movie clip symbol.
Name the clip symbol process.
On frame 1 of the process clip, attach the following code:
_root.ball._x += 10;
On frame 2 of the process clip, add the following code:
gotoAndPlay(1);
Return to the main movie timeline and create a layer called scripts.
On the scripts layer, place an instance of the process symbol.
Name the instance processMoveBall.
Select Control Test Movie.
The processMoveBall instance will now move ball without interfering with the playback of the main timeline upon which ball resides.
Note that step 12 isn't mandatory, but it gives us more control over our loop. By giving our timeline-loop instance a name, we can stop and start our loop by starting and stopping the playback of the instance, like this:
processMoveBall.play( ); processMoveBall.stop( );
Note that in this example processMoveBall and ball must both exist on the main timeline for as long as the loop is supposed to work. If we wanted to make the code more portable, we could use a relative reference to our ball clip in process:
_ parent.ball._x += 10;
And if we wanted to control our ball from any timeline, we'd use an absolute reference to ball:
_root.ball._x += 10;
WARNING
Timeline loops can't loop on a single frame. That is, if we place a gotoAndPlay(5) function on frame 5 of a movie, the function will be ignored. The Player realizes that the playhead is already on frame 5 and simply does nothing.
You'll find the sample timeline loop and empty-clip loop .fla files in the online Code Depot.
Timeline loops are effective but not necessarily elegant. In Flash 5, we can use an event handler on a movie clip to achieve the same results as a timeline loop but with more flexibility (just try to follow along with this example, or see Chapter 10, "Events and Event Handlers", for details on movie clip event handlers).
When placed on a movie clip, an enterFrame event handler causes a block of code to execute every time a frame passes in a movie. We can use an enterFrame event handler on a single-frame empty clip to repetitively execute a block of code while allowing for a Stage update between each repetition (just as a timeline loop does). Follow these steps to try it out:
Follow steps 1 through 7 from the previous section.
On the main Stage, create a new layer called scripts.
On the scripts layer, place an instance of the process clip.
Select the process instance and attach the following code:
onClipEvent(enterFrame) { _root.ball._x += 10; }
Select Control Test Movie.
The ball instance should animate across the Stage.
Clip event loops free us from nesting our code inside a movie clip and don't require a two-frame loop, as timeline loops do. All the action of a clip event loop happens in a single event handler. However, the clip event example we just saw has a potential drawback: there's no way to programmatically start or stop the loop once it's started. The only way to stop the loop is to physically remove the process instance from the timeline with a blank keyframe.
To create an event loop that can be arbitrarily started and stopped, we have to create an empty clip that contains another empty clip that bears an event loop. We can then dynamically attach and remove the whole package whenever we want to start or stop our loop. A little convoluted, yes, but the results are quite flexible. Once again, follow the steps to try it out:
Follow steps 1 through 5 under Section 8.7.1, "Creating an Empty-Clip Timeline Loop".
Select Insert New Symbol twice to create two blank movie clip symbols.
Name one clip symbol process and the other eventLoop.
In the Library, select the process clip, then select Options Linkage. The Symbol Linkage Properties dialog box appears.
Select Export This Symbol.
In the Identifier field, type processMoveBall and then click OK.
On frame 1 of the process clip, drag an instance of eventLoop onto the Stage.
Select the eventLoop instance, and attach the following code:
onClipEvent(enterFrame) { _ parent._ parent.ball._x += 10; }
Return to the main movie timeline and attach the following code to frame 1:
attachMovie("processMoveBall", "processMoveBall", 5000);
Whenever you want to stop the event loop, issue the following statement:
_root.processMoveBall.removeMovieClip( );
Select Control Test Movie.
Once again, the ball instance should animate across the Stage, but this time we can start and stop it whenever we like by using the attachMovie( ) and removeMovieClip( ) functions shown in steps 9 and 10.
There are examples of regular and controllable clip event loops available from the online Code Depot.
Both of the clip event loops we just saw included a line of code that updates the position of the ball instance on the Stage. For example:
onClipEvent(enterFrame) { _ parent._ parent.ball._x += 10; // Updates ball's position }
Although this approach works, it's sloppy. By attaching meaningful code to our clip event, we've decentralized our code base, dispersing logic and behavior throughout our movie. In order to keep our code accessible during authoring and better structured for reuse, from within event loops, we should only call functions. So, instead of actually moving the ball clip in our example, we should call a function that moves the ball clip, like this:
onClipEvent(enterFrame) { _ parent._ parent.moveBall( ); }
The user-defined function moveBall( ) would be defined on the same timeline we attach the processMoveBall clip to, like this:
function moveBall( ) { ball._x += 10; }
We'll talk more about functions and code portability in Chapter 9, "Functions".
If our application is simple, we may wish to forego our empty event-loop clip altogether. In some cases, we can quite legitimately attach an event loop directly to the clip being manipulated. In our ball example, we could avoid the need for separate empty clips by attaching the following code directly to the ball instance:
onClipEvent(enterFrame) { _x += 10; }
This approach is ultraconvenient, but it doesn't scale very easily, and like our first example, it suffers from the inability to start and stop the loop.
Because timeline and clip event loops iterate once per frame, their execution frequency is tied to the frame rate of a movie. If we're moving an object around the screen with a timeline or an event loop, an increase in frame rate can mean an increase in the speed of our animation.
When we programmed the movement of the ball clip in our earlier examples, we implicitly specified the velocity of the ball in relation to the frame rate. Our code says, "With each frame that passes, move ball ten pixels to the right":
_ball += 10;
The speed of ball is, hence, dependent on the frame rate. If our movie plays at 12 frames per second, then our ball clip moves 120 pixels per second. If our movie plays at 30 frames per second, our ball clip moves 300 pixels per second!
When timing scripted animations, it's tempting to calculate the distance to move an item in relation to the movie's frame rate. So, if a movie plays 20 frames per second, and we want an item to move 100 pixels per second, we're tempted to set the velocity of the object to 5 pixels per frame (5 pixels * 20 frames per second = 100 pixels per second). There are two serious flaws in this approach:
By relying on the frame rate to determine the speed of an item, we make it painful to change the frame rate. If we change the frame rate, we have to recalculate our speed and edit our code accordingly.
The Flash Player does not necessarily play movies back at the frame rate set in the Flash authoring tool; it often plays them slower. If the computer running the movie cannot render frames fast enough to keep up with the designated frame rate, the movie slows down. This slowdown can even vary depending on the system load; if other programs are running or if Flash is performing some processor-intensive task, the frame rate may drop for only a short period and then resume its normal pace.
You can test this out yourself using the time-tracker tool available at:
http://www.moock.org/webdesign/flash/actionscript/fps-speedometer
In some cases, an animation that plays back at slightly different speeds could be deemed acceptable. But when visual accuracy matters or when we're concerned with the responsiveness of an action game, it's much more appropriate to calculate the distance to move an object relative to elapsed time instead of the frame rate. Example 8-5 shows a quick-and-dirty sample of time-based animation (i.e., the ball speed is independent of the frame rate). The new movie would have three frames and two layers, one layer with the ball instance and the other with our scripts.
// CODE ON FRAME 1 var distancePerSecond = 50; // Pixels to move per second var now = getTimer( ); // The current time var then = 0; // The time when last frame was rendered var elapsed; // Milliseconds between frame renders var numSeconds; // elapsed expressed in seconds var moveAmount; // Distance to move each frame // CODE ON FRAME 2 then = now; now = getTimer( ); elapsed = now - then; numSeconds = elapsed / 1000; moveAmount = distancePerSecond * numSeconds; ball._x += moveAmount; // CODE ON FRAME 3 gotoAndPlay(2);
Note that our time-based movement might appear jerky if the frame rate suddenly changes. We could smooth things out by using an elapsed-time measurement that averages the time between a series of frames instead of just two.
Copyright © 2002 O'Reilly & Associates. All rights reserved.