UPDATE: Check out the most recent update to this post.
The Flash Player provides vector-perfect collision detection with the hitTest() method. The drawback of this method is that if you use Bitmaps it sees just the bounding-box of the Bitmap, i.e. it ignores alpha. This is pretty frustrating when it comes to a sprite-based game.
A bit of digging on the web found a quick and simple solution from Andre Michelle [.zip]. A slightly more robust version was written for AS2 by Grant Skinner, then ported to AS3 by Boulevart. I grabbed their version to start with...
First, I cleaned up the code and commented it. It's pretty straightforward: you render two display objects to separate color channels, combine the color channels, then search the resulting image for any overlapping color (the previous two sites have deeper explanations).
I found two problems with the code. First, it unions the bounding boxes of the two display objects. This is unnecessary, we only care about the pixels in the intersecting rectangle. And since performance hit of this technique grows with the size of the overlap, this is a critical optimization (you're allocating significantly less BitmapData each test).
Second, the code doesn't account for a display list of any complexity: it assumes the two display objects have the same parent (and are thus in the same coordinate space), and it assumes the display objects do not have any scale or rotation (it only considers linear transformation, i.e. the objects' positions). I fixed both of these.
So, the new code will determine if two DisplayObjects are visibly overlapping with pixel precision. Enjoy!
-
package
-
{
-
import flash.display.BitmapData;
-
import flash.display.BitmapDataChannel;
-
import flash.display.BlendMode;
-
import flash.display.DisplayObject;
-
import flash.display.DisplayObjectContainer;
-
import flash.geom.Matrix;
-
import flash.geom.Point;
-
import flash.geom.Rectangle;
-
-
public class PixelPerfectCollisionDetection
-
{
-
/** Get the collision rectangle between two display objects. **/
-
public static function getCollisionRect(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Rectangle
-
{
-
// get bounding boxes in common parent's coordinate space
-
var rect1:Rectangle = target1.getBounds(commonParent);
-
var rect2:Rectangle = target2.getBounds(commonParent);
-
-
// find the intersection of the two bounding boxes
-
var intersectionRect:Rectangle = rect1.intersection(rect2);
-
-
if (intersectionRect.size.length> 0)
-
{
-
if (pixelPrecise)
-
{
-
// size of rect needs to integer size for bitmap data
-
intersectionRect.width = Math.ceil(intersectionRect.width);
-
intersectionRect.height = Math.ceil(intersectionRect.height);
-
-
// get the alpha maps for the display objects
-
var alpha1:BitmapData = getAlphaMap(target1, intersectionRect, BitmapDataChannel.RED, commonParent);
-
var alpha2:BitmapData = getAlphaMap(target2, intersectionRect, BitmapDataChannel.GREEN, commonParent);
-
-
// combine the alpha maps
-
alpha1.draw(alpha2, null, null, BlendMode.LIGHTEN);
-
-
// calculate the search color
-
var searchColor:uint;
-
if (tolerance <= 0)
-
{
-
searchColor = 0x010100;
-
}
-
else
-
{
-
if (tolerance> 1) tolerance = 1;
-
var byte:int = Math.round(tolerance * 255);
-
searchColor = (byte <<16) | (byte <<8) | 0;
-
}
-
-
// find color
-
var collisionRect:Rectangle = alpha1.getColorBoundsRect(searchColor, searchColor);
-
collisionRect.x += intersectionRect.x;
-
collisionRect.y += intersectionRect.y;
-
-
return collisionRect;
-
}
-
else
-
{
-
return intersectionRect;
-
}
-
}
-
else
-
{
-
// no intersection
-
return null;
-
}
-
}
-
-
/** Gets the alpha map of the display object and places it in the specified channel. **/
-
private static function getAlphaMap(target:DisplayObject, rect:Rectangle, channel:uint, commonParent:DisplayObjectContainer):BitmapData
-
{
-
// calculate the transform for the display object relative to the common parent
-
var parentXformInvert:Matrix = commonParent.transform.concatenatedMatrix.clone();
-
parentXformInvert.invert();
-
var targetXform:Matrix = target.transform.concatenatedMatrix.clone();
-
targetXform.concat(parentXformInvert);
-
-
// translate the target into the rect's space
-
targetXform.translate(-rect.x, -rect.y);
-
-
// draw the target and extract its alpha channel into a color channel
-
var bitmapData:BitmapData = new BitmapData(rect.width, rect.height, true, 0);
-
bitmapData.draw(target, targetXform);
-
var alphaChannel:BitmapData = new BitmapData(rect.width, rect.height, false, 0);
-
alphaChannel.copyChannel(bitmapData, bitmapData.rect, new Point(0, 0), BitmapDataChannel.ALPHA, channel);
-
-
return alphaChannel;
-
}
-
-
/** Get the center of the collision's bounding box. **/
-
public static function getCollisionPoint(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Point
-
{
-
var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
-
-
if (collisionRect != null && collisionRect.size.length> 0)
-
{
-
var x:Number = (collisionRect.left + collisionRect.right) / 2;
-
var y:Number = (collisionRect.top + collisionRect.bottom) / 2;
-
-
return new Point(x, y);
-
}
-
-
return null;
-
}
-
-
/** Are the two display objects colliding (overlapping)? **/
-
public static function isColliding(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Boolean
-
{
-
var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
-
-
if (collisionRect != null && collisionRect.size.length> 0) return true;
-
else return false;
-
}
-
}
-
}