DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 7.15 Checking for Overlapping Movie Clips (Performing Hit Tests)

7.15.1 Problem

You want to find out if two movie clips are overlapping.

7.15.2 Solution

Use the hitTest( ) method. Or, for testing collisions between two complex shapes, create and utilize a custom hitTestOutline( ) method.

7.15.3 Discussion

The hitTest( ) method enables you to determine one of three things:

  • Whether any point within the bounding box of one movie clip overlaps any point in the bounding box of another movie clip

  • Whether a specific coordinate is within the bounding box of a movie clip

  • Whether a specific coordinate is within the outline of a movie clip's shape

The easiest way to test whether a movie clip overlaps another clip is to call hitTest( ) from one movie clip instance and pass it a reference to the other movie clip instance:

// Returns true if myMovieClipA overlaps myMovieClipB, and false otherwise. This
// tests whether the bounding boxes overlap. If the shapes are nonrectangular, this
// technique may not be appropriate.
myMovieClipA.hitTest(myMovieClipB);

Here is a working example that draws a circle and a rectangle. Then, using the startDrag( ) method, the code causes the circle movie clip to follow the pointer. Using an onEnterFrame( ) method in conjunction with hitTest( ), the code continually checks whether the circle overlaps the rectangle. If you test the example, you can see that there are times when the hit test returns true even though the circle shape does not overlap the rectangle. This is because the circle's bounding box might overlap with the rectangle even though its outline does not.

// Include DrawingMethods.as from Chapter 4 for its custom drawing methods.
#include "DrawingMethods.as"

// Draw a circle.
_root.createEmptyMovieClip("circle_mc", 1);
circle_mc.lineStyle(1, 0x000000, 100);
circle_mc.drawCircle(20);

// Draw a rectangle and position it at (200,200).
_root.createEmptyMovieClip("rectangle_mc", 2);
rectangle_mc.lineStyle(1, 0x000000, 100);
rectangle_mc.drawRectangle(100, 100);
rectangle_mc._x = 200;
rectangle_mc._y = 200;

// Tell the circle to snap to the pointer and follow it around.
circle_mc.startDrag(true);

// Use an onEnterFrame(  ) method to continually check hitTest(  )'s value.
circle_mc.onEnterFrame = function (  ) {
  // Use trace(  ) to output the value of the hit test.
  trace(this.hitTest(_root.rectangle_mc));
};

Another way to use hitTest( ) is to check whether a coordinate is within the bounding box of the movie clip. When you use hitTest( ) in this way, you do not need to specify another movie clip instance, just the x and y coordinates against which you wish to test. Additionally, you should set the third, shapeFlag parameter to false to let hitTest( ) know you want to perform a test on the bounding box of the movie clip and not its outline. Here is an example that checks to see if the coordinates of the mouse pointer are within a rectangle:

// Include DrawingMethods.as from Chapter 4 for its drawRectangle(  ) method.
#include "DrawingMethods.as"

_root.createEmptyMovieClip("rectangle_mc", 1);
rectangle_mc.lineStyle(1, 0x000000, 100);
rectangle_mc.drawRectangle(100, 100);
rectangle_mc._x = 200;
rectangle_mc._y = 200;

rectangle_mc.onEnterFrame = function (  ) {
  // Perform a hit test using the _xmouse and _ymouse properties of _root. This will
  // test true if the mouse pointer is over the rectangle and false otherwise.
  trace(this.hitTest(_root._xmouse, _root._ymouse, false));
};

If you want to test whether coordinates are within a movie clip's shape instead of simply testing for hits within the bounding box, use the same basic technique as shown in the preceding example but set the shapeFlag parameter to true. This is great for testing to see if a coordinate is within a complex (nonrectangular) shape. Here is an example that does just that:

// Include DrawingMethods.as from Chapter 4 for its drawCircle(  ) method.
#include "DrawingMethods.as"

_root.createEmptyMovieClip("circle_mc", 1);
circle_mc.lineStyle(1, 0x000000, 100);
circle_mc.drawCircle(60);
circle_mc._x = 200;
circle_mc._y = 200;

circle_mc.onEnterFrame = function (  ) {
  trace(this.hitTest(_root._xmouse, _root._ymouse, true));
};

All hit tests are performed using global coordinates—i.e., coordinates within the _root object's coordinate system.

Notice that in the preceding example, the mouse pointer coordinates were obtained using the _xmouse and _ymouse properties of _root. You would not get the same results if you used the mouse pointer coordinates relative to the rectangle or circle clips. This is important to understand in situations in which you want to test to see if a coordinate within one movie clip is overlapping another movie clip. In these cases you need to convert the local coordinates to global coordinates using the localToGlobal( ) method and test using the resulting values. The following example demonstrates this:

// Include DrawingMethods.as from Chapter 4 for its drawCircle(  ) method.
#include "DrawingMethods.as"

// Draw a filled circle and position it at (200,200).
_root.createEmptyMovieClip("staticCircle_mc", 2);
staticCircle_mc.lineStyle(1, 0x000000, 100);
staticCircle_mc.beginFill(0xFF, 33);
staticCircle_mc.drawCircle(100);
staticCircle_mc.endFill(  );
staticCircle_mc._x = 200;
staticCircle_mc._y = 200;

// Create a movie clip that will follow the mouse pointer. Inside that movie clip,
// create a nested movie clip and draw a small circle in it.
_root.createEmptyMovieClip("bouncingCircleHolder", 1);
bouncingCircleHolder.createEmptyMovieClip("bouncingCircle_mc", 1);
bouncingCircleHolder.bouncingCircle_mc.lineStyle(1, 0, 100);
bouncingCircleHolder.bouncingCircle_mc.drawCircle(3);

// Tell the bouncingCircle_mc movie clip to move back and forth within the
// bouncingCircleHolder movie clip.
bouncingCircleHolder.bouncingCircle_mc.velocity = 3;
bouncingCircleHolder.bouncingCircle_mc.onEnterFrame = function (  ) {

  // Change the x position of bouncingCircle_mc by the velocity.
  this._x += this.velocity;

  // If the x position is outside of the range of -30 to 30, then reverse the 
  // velocity so that it begins moving back in the opposite direction.
  if (this._x < -30 || this._x > 30) {
    this.velocity *= -1;
  }

  // Create a point object for use with the localToGlobal(  ) method. Assign it the x
  // and y values from bouncingCircle_mc.
  var pnts = {x: this._x, y: this._y};

  // Use localToGlobal(  ) to convert the points' object values 
  // to their global equivalents.
  this._parent.localToGlobal(pnts);

  // Output the hit test results for staticCircle_mc within the coordinates of
  // bouncingCircle_mc (as they have been converted to their global equivalents).
  trace(_root.staticCircle_mc.hitTest(pnts.x, pnts.y, true));
};

// Instruct the boundingCircleHolder movie clip to follow the mouse pointer.
bouncingCircleHolder.startDrag(true);

In the preceding example, you cannot get accurate results by simply performing a hit test between staticCircle_mc and bouncingCircle_mc. This is because such a hit test would be performed between the bounding boxes of the two movie clips, and they are each circles. Notice, however, that while this example works more or less when bouncingCircle_mc is relatively small, a larger version would not produce very accurate results. The reason for this is that the hit test is being performed between staticCircle_mc and the center point of bouncingCircle_mc, not the entire bouncingCircle_mc shape.

ActionScript does not provide a simple way to perform a hit test between two circles. However, you can perform such a test by checking whether the centers of the circles are within a distance that is less than the sum of their radii, as shown in the following example (see Recipe 5.13 for more information):

// Include DrawingMethods.as from Chapter 4 for its drawCircle(  ) method.
#include "DrawingMethods.as"

// Create two circle movie clips: circle0 and circle1.
_root.createEmptyMovieClip("circle0", 1);
circle0.lineStyle(1, 0x000000, 100);
circle0.drawCircle(60);
circle0._x = 200;
circle0._y = 200;

_root.createEmptyMovieClip("circle1", 2);
circle1.lineStyle(1, 0x000000, 100);
circle1.drawCircle(60);
circle1.startDrag(true);

// Continually check to see if the circles overlap.
circle1.onEnterFrame = function (  ) {

  // Find the difference in x and y coordinates between 
  // the centers of the two circles.
  var dx = circle0._x - this._x;
  var dy = circle0._y - this._y;

  // Calculate the distance between the centers of the two circles.
  var dist = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));

  // If the distance between the centers is less than the sum of 
  // the two radii, the circles overlap.
  if (dist < (circle0._width + this._width)/2) {
    trace("circles touching");
  }
};

There is no trivial way to perform hit tests between two complex shapes in ActionScript. However, it can be done with a little bit of work. The premise is that while you cannot use hitTest( ) to test only the shapes of two movie clips (and not their bounding boxes), you can mimic this by performing a shape-sensitive hit test between one of the objects and a series of movie clips that outline the shape of the other. Traditionally, people have accomplished this by creating a series of small, circle movie clips around the outline of one of the shapes at authoring time. However, you can do even better than that. The following code illustrates how to create a series of small circles to outline a shape at runtime and how to use them to perform a hit test between two complex movie clips:

// Include DrawingMethods.as from Chapter 4 for its drawCircle(  ) method.
#include "DrawingMethods.as"

// The outline(  ) method creates a series of small circles within the movie clip that
// outline the shape. The optional circleRadius parameter defines the radii of the
// circles. The smaller the circles, the more accurate the hit test, but it is also
// more processor-intensive. The optional show parameter allows you to show the
// circles for testing purposes.
MovieClip.prototype.outline = function (circleRadius, show) {
  // Use a default radius of 3 pixels.
  if (circleRadius == undefined) {
    circleRadius = 3;
  }

  // Create an array for holding the references to the outline circles.
  this.outlines = new Array(  );

  // Get the coordinates of the bounding box and set the x and y variables
  // accordingly. The x variable must be more than the minimum x boundary because
  // otherwise the method will not be able to locate the shape.
  var bounds = this.getBounds(this);
  var x = bounds.xMin + circleRadius;
  var y = bounds.yMin;

  // Begin by outlining the shape from the top-left corner 
  // and moving to the right as x increases.
  var dir = "incX";
  goodToGo = true;
  var pnts;
  var i = 0;

  // The goodToGo variable is true until the last circle is drawn.
  while (goodToGo) {
    i++;

    // Create the new circle outline movie clip and draw a circle in it.
    this.createEmptyMovieClip("outline" + i, i);
    var mc = this["outline" + i];
    mc.lineStyle(0, 0x0000FF, 100);
    mc.drawCircle(circleRadius);

    // Set the circle visibility to false unless show is true.
    mc._visible = show ? true : false;

    // Add the circle movie clip to the outlines array for use during the hit test.
    this.outlines.push(mc);

    // Check to see in which direction the outline is being drawn.
    switch (dir) {
      case "incX":

        // Increment the x value by the width of one of the circles to move the next
        // circle just to the right of the previous one.
        x += mc._width;

        // Create a point object and call localToGlobal(  ) to convert the values to
        // the global equivalents.
        pnts = {x: x, y: y};
        this.localToGlobal(pnts);

        // If the center of the circle does not touch the shape within the movie
        // clip, increment y and calculate the new global equivalents for another hit
        // test. This moves the circle down until it touches the shape.
        while (!this.hitTest(pnts.x, pnts.y, true)) {
          y += mc._width;
          pnts = {x: x, y: y};
          this.localToGlobal(pnts);
        }

        // If the maximum x boundary has been reached, set the new direction to begin
        // moving in the increasing y direction.
        if (x >= bounds.xMax - (mc._width)) {
          dir = "incY";
        }

        // Set the coordinates of the circle movie clip.
        mc._x = x;
        mc._y = y;

        // Reset y to the minimum y boundary so that you can move the next circle
        // down from the top until it touches the shape.
        y = bounds.yMin;
        break;

      case "incY":
        // The remaining cases are much like the first, but they move in different
        // directions.
        y += mc._width;
        pnts = {x: x, y: y};
        this.localToGlobal(pnts);
        while (!this.hitTest(pnts.x, pnts.y, true)) {
          x -= mc._width;
          pnts = {x: x, y: y};
          this.localToGlobal(pnts);
        }
        if (y >= bounds.yMax - (mc._width)) {
          dir = "decX";
        }
        mc._x = x;
        mc._y = y;
        x = bounds.xMax;
        break;

      case "decX":
        x -= mc._width;
        pnts = {x: x, y: y};
        this.localToGlobal(pnts);
        while (!this.hitTest(pnts.x, pnts.y, true)) {
          y -= mc._width;
          pnts = {x: x, y: y};
          this.localToGlobal(pnts);
        }
        if (x <= bounds.xMin + (mc._width)) {
          dir = "decY";
        }
        mc._x = x;
        mc._y = y;
        y = bounds.yMax;
        break;

      case "decY":
        y -= mc._width;
        pnts = {x: x, y: y};
        this.localToGlobal(pnts);
        while (!this.hitTest(pnts.x, pnts.y, true)) {
          x += mc._width;
          pnts = {x: x, y: y};
          this.localToGlobal(pnts);
        }
        if (y <= bounds.yMin + (mc._width)) {
          goodToGo = false;
        }
        mc._x = x;
        mc._y = y;
        x = bounds.xMin;
        break;
    }
  }
};

// Perform a hit test between a movie clip with outline circles and another movie
// clip you specify in the parameter.
MovieClip.prototype.hitTestOutline = function (mc) {

  // Loop through all the elements of the outlines array.
  for (var i = 0; i < this.outlines.length; i++) {

    // Create a point object and get the global equivalents.
    var pnts = {x:this.outlines[i]._x, y:this.outlines[i]._y};
    this.localToGlobal(pnts);

    // If the mc movie clip tests true for overlapping with any of the outline 
    // circles, then return true. Otherwise, the method returns false.
    if (mc.hitTest(pnts.x, pnts.y, true)) {
      return true;
    }
  }
  return false;
};

// In this example, create two movie clips with complex shapes. Create instances
// named mc1 and mc2. This code creates an outline on mc1 (and for testing purposes
// it shows you the circles).
mc1.outline(3, true);

// Continually perform a hit test between the two shapes. The hitTestOutline(  ) method
// must be invoked from the movie clip with the outlines.
mc1.onEnterFrame = function (  ) {
  trace(this.hitTestOutline(mc2));
};

// Tell mc2 to follow the mouse pointer.
mc2.startDrag(true);

7.15.4 See Also

Recipe 7.6 and Recipe 7.17

    [ Team LiB ] Previous Section Next Section