Hack 70 Adjust the Animation Complexity Dynamically

figs/expert.gif figs/hack70.gif

Measure the Flash Player's runtime performance in order to adjust the complexity of an effect or animation dynamically to run optimally on both low-end and high-end machines.

In the preceding hacks, we've seen ways to improve performance, regardless of the playback system, by optimizing a Flash movie's assets at authoring time. Optimization of that sort is essentially a lowest common denominator affair. This hack addresses the need to maximize animation and other effects for faster machines without creating a movie that bogs down on slower machines.

Although Flash Player 7 performs better than previous versions, it is still not a speed demon. The underlying goal of Flash is to optimize filesize (i.e., reduce download time) rather than provide breathtaking performance. Although we can improve performance by lowering the render quality [Hack #67], in many cases, turning off antialiasing can result in an unacceptably poor appearance for your site.

A better way to improve performance is to adjust the complexity (i.e., how much is happening per frame) of a graphical effect rather than its quality or frame rate. This approach can yield better results over a wider range of user systems with varying processing power than a one-size-fits-all optimization.

Adjusting the complexity is often an iterative process based on experimentation (with time it will become second nature and require fewer iterations):

  • Start with a target frame rate, such as 18 fps.

  • Measure the achieved frame rate, as covered shortly.

  • If the desired frame rate is not achievable on the test machine, reduce the complexity of the effect.

  • If the effect is rendered quickly enough, you have the leeway to increase the complexity of the effect.

Now that you understand the basics of the approach, let's automate it so that Flash decides dynamically whether to reduce or increase the complexity of the animation. This approach protects against sluggish performance on slower machines and (when done well) enhances the effect on faster machines.

We do not intend to change the frame rate, but rather change the complexity of the effect, in such a way that it always completes just before the next frame starts.

Calculate the Achieved Frame Rate

The following code creates an empty movie clip called perfMon (performance monitor) and attaches an onEnterFrame( ) event handler to it. This event handler calculates the average frame duration (in milliseconds) using the last two timer measurements, creating a rolling average. This rolling average, time, is compared against the expected duration, FRAME_DUR, and a Boolean flag, slow, is set to true or false based on whether the Flash Player is achieving the target frame rate.

function performanceMonitor( ) {

  this.createEmptyMovieClip("perfMon", 10000);

  perfMon.onEnterFrame = function( ) {

    time = (getTimer( ) - lastTime) / 2;

    lastTime = getTimer( );

    // Set slow to a Boolean based on whether the 

    // elapsed time exceeds the allowed frame duration

    slow = time > FRAME_DUR;

  };

}

// Set FRAME_RATE to match the movie's target frame rate

var FRAME_RATE:Number = 18;

var FRAME_DUR:Number = (1 / FRAME_RATE) * 1000;

var time:Number = 0;

var lastTime:Number = getTimer( );

performanceMonitor( );

A slow value of false indicates that we can make the Flash Player do more without compromising performance because it is completing all its tasks before the start of the next frame. Ideally, we would want the Flash Player to finish all tasks for the current frame just as the next frame starts. We know this is happening when our rolling average, time, is equal to the expected frame duration, FRAME_DUR.

Adjust the Complexity Based on Performance

Let's see how to make Flash increase or decrease the animation's complexity based on the performance monitor's calculation. One simple approach is to draw more or fewer movie clips depending on the value of our slow flag. For the sake of example, let's use a star field particle effect [Hack #33] . The following code assumes a default Stage size of 550 400 pixels, with a dark background color. The effect works best with a frame rate of 18-24 fps. To set the background color and frame rate, select ModifyDocument and adjust the properties as desired. The following code assumes a frame rate of 24 fps, and if you choose any other rate, you will need to change the following line to reflect this:

var FRAME_RATE:Number = 24;

Note that you have to do this manually because there is no property or method in Flash that returns the movie's target frame rate (strange but true!).

We've modified the starfield( ) function from [Hack #33] to draw a single star at a time. Instead of setting the slow flag when the animation is running slowly, we use an if statement to adjust the animation's complexity. If the animation is running quickly enough, it draws additional stars by calling starfield( ); otherwise, it deletes stars to maintain performance.

Again, it is important to realize that we are not varying the frame rate (and doing so would not be transparent to the rest of the SWF, in any case), but rather the complexity of the animation so that the time taken to render each frame in the animation is equal to all the available time per frame.

function performanceMonitor( ) {

  var perfMon:MovieClip = this.createEmptyMovieClip("perfMon", 10000);

  perfMon.onEnterFrame = function( ) {

    time = (getTimer( ) - lastTime) / 2;

    if (time < (FRAME_DUR)) {

      // Speed is okay

      stars++;

      starField( );

    } else if (time > (FRAME_DUR + 10)) {

      // Running too slowly

      _root["star" + stars].removeMovieClip( );

      stars--;

    }

    lastTime = getTimer( );

  };

}

function mover( ) {

  this._y += this.speed;

  this._yscale += this.speed;

  if (this._y > 275) {

    this._y = 0;

    this.speed = Math.ceil(Math.random( ) * 10);

    this._yscale = 100;

  }

}

function starField( ) {

  var star:MovieClip = this.createEmptyMovieClip("star" + stars, stars);

  star._rotation = Math.random( )*360;

  star._x = 275;

  star._y = 200;

  var dot:MovieClip = star.createEmptyMovieClip("dot", 0);

  dot.speed = Math.ceil(Math.random( ) * 10);

  dot.lineStyle(1, 0xFFFFE0, 100);

  dot.moveTo(0, 2);

  dot.lineTo(0, 5);

  dot.onEnterFrame = mover;

}

// Set FRAME_RATE to match the movie's target frame rate

var FRAME_RATE:Number = 24;

var FRAME_DUR:Number = (1 / FRAME_RATE) * 1000;

var time:Number = 0;

var lastTime:Number = 0;

var stars:Number = 0;

performanceMonitor( );

If you run the preceding code, you will see the star field build from no stars up to a maximum of several hundred stars, as shown in Figure 9-7. Use the Variables tab in the Debugger panel to view the variables on the main timeline, which displays results similar to those shown in Figure 9-7.

Figure 9-7. The star field and the performance monitor
figs/flhk_0907.gif


The number of stars increases until time is close to FRAME_DUR, at which point the code adds or deletes stars to maintain equilibrium. If you do anything to change this equilibrium (such as run other code or resize the SWF), the change in performance causes a new equilibrium to be reached. Note that the Flash frame rate tends to change spuriously even when equilibrium is reached, so the effect will be almost constantly adding and deleting movie clips. You can build in a buffer if you prefer. For example, you might delete or add clips only if the actual time per frame is more than 15% above or below the target frame rate.

Final Thoughts

Graphical effects that work well on your development machine might bog down on user machines with lesser performance. To make your Flash content run appropriately on as many machines as possible (including mobile devices with significantly slower performance characteristics), you should strive for scalability. Your content should adapt to the user's machine. This hack shows one fairly simple approach: calculate how fast the user's machine is performing and adjust the graphical complexity accordingly. Measuring true performance at runtime is usually easier and more reliable than trying to estimate performance based on, say, the device type or screen resolution.

The many adjustments you can make at runtime to dynamically change the movie's complexity include:

  • Limiting the number of items onscreen (stars in a star field, monsters in a game, users in an avatar-based chat room).

  • Reducing the number of audio channels in use.

  • Reducing the alpha effects (i.e., turn off transparency on slower machines).

  • Limiting the amount or frequency of movements (such as using fewer frames in a walk cycle [Hack #28] ). Digital and 3D artists unfamiliar with performance requirements usually create too many in-between positions. Often 3 or 5 will suffice instead of the typical 15 or 20 positions artists provide.

  • Using smaller screen dimensions or reducing the active area of the screen. You can make the screen size appear larger by adding static graphics around your active area, such as a border to achieve a "picture frame" effect or static gauges to achieve a "dashboard" look.

  • Using still bitmaps or videos with color effects [Hack #8].

You will find that some changes affect performance in more than one way and offer benefits on both low-end and high-end machines. For example, in a bartending game, you might reduce the number of patrons in the bar and the frequency with which they talk. This reduces the number of items to be rendered and the number of sound channels in use. Or you might reduce the size of the patrons so they take up less screen real estate. On higher-performance machines, this allows you to place more patrons at the bar. On lower-end machines, this ensures that the patrons displayed are rendered more quickly.

Runtime performance of the playback machine isn't the only reason to adjust your Flash content. You should also take into consideration the user's personal performance on interactive exercises. For example, you might display directions or play audio help if the user does something wrong or is taking too long to perform the desired operation. Or you might make a video game or educational game easier on the first level in response to the user failing repeatedly.

There are many other ways to affect the perceived performance or user experience. For example, implementing keyboard shortcuts [Hack #96] makes an application feel more responsive than requiring mouseclicks. Likewise, an animation that plays repeatedly becomes boring no matter how well it performs. Shorten the animation if it is played repeatedly, limit the number of times it is played, or allow the user to turn it off or skip it.