Pixel-Perfect Collision Detection in ActionScript3

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!

Actionscript:
  1. package
  2. {
  3.     import flash.display.BitmapData;
  4.     import flash.display.BitmapDataChannel;
  5.     import flash.display.BlendMode;
  6.     import flash.display.DisplayObject;
  7.     import flash.display.DisplayObjectContainer;
  8.     import flash.geom.Matrix;
  9.     import flash.geom.Point;
  10.     import flash.geom.Rectangle;
  11.    
  12.     public class PixelPerfectCollisionDetection
  13.     {
  14.         /** Get the collision rectangle between two display objects. **/
  15.         public static function getCollisionRect(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Rectangle
  16.         {
  17.             // get bounding boxes in common parent's coordinate space
  18.             var rect1:Rectangle = target1.getBounds(commonParent);
  19.             var rect2:Rectangle = target2.getBounds(commonParent);
  20.            
  21.             // find the intersection of the two bounding boxes
  22.             var intersectionRect:Rectangle = rect1.intersection(rect2);
  23.            
  24.             if (intersectionRect.size.length> 0)
  25.             {
  26.                 if (pixelPrecise)
  27.                 {
  28.                     // size of rect needs to integer size for bitmap data
  29.                     intersectionRect.width = Math.ceil(intersectionRect.width);
  30.                     intersectionRect.height = Math.ceil(intersectionRect.height);
  31.                    
  32.                     // get the alpha maps for the display objects
  33.                     var alpha1:BitmapData = getAlphaMap(target1, intersectionRect, BitmapDataChannel.RED, commonParent);
  34.                     var alpha2:BitmapData = getAlphaMap(target2, intersectionRect, BitmapDataChannel.GREEN, commonParent);
  35.                    
  36.                     // combine the alpha maps
  37.                     alpha1.draw(alpha2, null, null, BlendMode.LIGHTEN);
  38.                    
  39.                     // calculate the search color
  40.                     var searchColor:uint;
  41.                     if (tolerance <= 0)
  42.                     {
  43.                         searchColor = 0x010100;
  44.                     }
  45.                     else
  46.                     {
  47.                         if (tolerance> 1) tolerance = 1;
  48.                         var byte:int = Math.round(tolerance * 255);
  49.                         searchColor = (byte <<16) | (byte <<8) | 0;
  50.                     }
  51.  
  52.                     // find color
  53.                     var collisionRect:Rectangle = alpha1.getColorBoundsRect(searchColor, searchColor);
  54.                     collisionRect.x += intersectionRect.x;
  55.                     collisionRect.y += intersectionRect.y;
  56.                    
  57.                     return collisionRect;
  58.                 }
  59.                 else
  60.                 {
  61.                     return intersectionRect;
  62.                 }
  63.             }
  64.             else
  65.             {
  66.                 // no intersection
  67.                 return null;
  68.             }
  69.         }
  70.        
  71.         /** Gets the alpha map of the display object and places it in the specified channel. **/
  72.         private static function getAlphaMap(target:DisplayObject, rect:Rectangle, channel:uint, commonParent:DisplayObjectContainer):BitmapData
  73.         {
  74.             // calculate the transform for the display object relative to the common parent
  75.             var parentXformInvert:Matrix = commonParent.transform.concatenatedMatrix.clone();
  76.             parentXformInvert.invert();
  77.             var targetXform:Matrix = target.transform.concatenatedMatrix.clone();
  78.             targetXform.concat(parentXformInvert);
  79.            
  80.             // translate the target into the rect's space
  81.             targetXform.translate(-rect.x, -rect.y);
  82.            
  83.             // draw the target and extract its alpha channel into a color channel
  84.             var bitmapData:BitmapData = new BitmapData(rect.width, rect.height, true, 0);
  85.             bitmapData.draw(target, targetXform);
  86.             var alphaChannel:BitmapData = new BitmapData(rect.width, rect.height, false, 0);
  87.             alphaChannel.copyChannel(bitmapData, bitmapData.rect, new Point(0, 0), BitmapDataChannel.ALPHA, channel);
  88.            
  89.             return alphaChannel;
  90.         }
  91.  
  92.         /** Get the center of the collision's bounding box. **/
  93.         public static function getCollisionPoint(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Point
  94.         {
  95.             var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
  96.        
  97.             if (collisionRect != null && collisionRect.size.length> 0)
  98.             {
  99.                 var x:Number = (collisionRect.left + collisionRect.right) / 2;
  100.                 var y:Number = (collisionRect.top + collisionRect.bottom) / 2;
  101.        
  102.                 return new Point(x, y);
  103.             }
  104.        
  105.             return null;
  106.         }
  107.        
  108.         /** Are the two display objects colliding (overlapping)? **/
  109.         public static function isColliding(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Boolean
  110.         {
  111.             var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
  112.        
  113.             if (collisionRect != null && collisionRect.size.length> 0) return true;
  114.             else return false;
  115.         }
  116.     }
  117. }

28 Comments so far

  1. Adam Billyard on July 28th, 2007

    If the rendering is using h/w, have you tried folding the intersection rect epeatedly on itself so you can do a smaller getColorBoundsRect() test? Assuming this is the gating function wrt speed.

  2. Troy on July 29th, 2007

    Rendering is not using hardware, but it is native to the Flash Player, which is the equivalent of hardware-accelerated in the Flash world… ;)

    What do you mean by folding the intersection rect in on itself?

  3. Adam Billyard on August 7th, 2007

    I mean that if you want to reduce the number of pixels tested by getColorBoundsRect(), you do additive copy of the right half of the intersection rect to the left, the bottom to the top half and repeat thus halving the dminesions each time, you’ll end up with approx 1×1 rect to test - it probably won’t get down to 1×1 by you get the idea.

    That fine RenderWare product used this technique to render lightmaps on a PS2 but on xfer back a small amount of data.

  4. Troy on August 7th, 2007

    Interesting… I’ll have to do some timing tests to see if the cost of the additive copies outweighs the cost of the getColorBoundsRect() on the smaller bitmap…

  5. Martin on February 24th, 2008

    Hi,

    I tried using the method isColliding. It didn’t work at all. I have two display objects and every time the x and y position of the first object is changed I’m checking if it collides.

    Can you give an example of how to use it properly?

    Thanks!

  6. Troy on February 24th, 2008

    Are you providing a DisplayObject that’s common to both of the DisplayObjects? I’ve not used this source in a while (we had to go back to bounding-box collision tests because of the number of the objects) but it was copied nearly verbatim from our project, and it was definitely working.

    You’d use isColliding like this:

    isColliding(myObject1, myObject2, stage, true, 127);

    That will test if myObject1 and myObject2 are colliding, checking at the pixel-level (as opposed to just checking bounding boxes) and considering a collision only if the pixels have greater than 50% alpha.

    The reason I have you specify the parent instead of assuming the stage is to handle off-stage display lists.

  7. sooon on February 24th, 2008

    hi Troy,

    first of all i must compliment you for your work. nice one!

    but, I do face a problem:

    i have a movieclip(A) hiding within movieclip(B) :

    B.A.visible = false;

    if I have movieclip(C) that is same level with B, It will still detected when C hit A even A.visible is set to false. whereas, in Grant Skinner version, it works fine.

    any idea?

  8. Martin on February 25th, 2008

    Hi Troy,

    thanks for you reply. I forgot to tell you something .. both Objects are extending from DisplayObjects (they are images actually) however they are rotated using the mx:Rotate flex thing.

    Would it still suppose to work?

    Thanks!

  9. TheActionscriptMan on March 18th, 2008

    Hiya,

    I needed a pixel perfect collision detection routine for a game i’m making, and i found grant skinners and your version, and wrote a test app - and managed to make a tweaked version which combines a bit from each and gained a speed up. It’s my first ever released bit of Actionscript, let me know if it’s ok to share the modified code on my blog :).

    Did you ever try the additive copy folding method someone mentioned above?

  10. Troy on March 18th, 2008

    @TheActionscriptMan: Looks good. I haven’t spent too much time studying the code, but I guess the big difference from mine is that instead of extracting alpha maps from each image and combining those, you use a ColorTransform to get a similar result (different bit ops). So, instead of 3 draws and 2 copy channels (mine) you’ve got just 2 draws, which should account for most of the speedup you’re seeing.

    As far as I can tell, they’re equivalent. I want to take a few minutes and do the math on the combining ops to ensure they’d get the same results in all situations…

    The code is totally okay to share. I didn’t stick a license on it, but you can consider it equivalent to MIT or LPGL… free to do with it what you want, commercial or personal, just don’t claim a verboten version as your own. Attribution is always appreciated but not required (what you’ve got on your blog is more than enough).

    And if you don’t mind, I’ll probably include your changes into my code! ;)
    I haven’t tried the additive copy folding Adam mentioned. Now that I’ve got Flex 3 with its profiler I may be able to get some detailed profiling info to work off of. It depends on whether the extra cost of additional draw calls is more than the savings of a smaller getColorBoundsRect call. My gut tells me it’d break even at best, but I don’t have any numbers to back that up.

  11. lee on March 22nd, 2008

    Thanks for this. It’s just what I needed.

    Does anyone know why flash doesn’t have a proper pixel perfect hit test? It strikes me as odd.

  12. Anonymous on April 3rd, 2008

    Okay, everything seems to be working fine, except no matter if pixelperfect is turned on or off when I call isColliding, it uses the bounding boxes to determine the collision.

    So I went hunting and found out that it’s because the width and height of the collisionRect always comes back as 0. I have no idea why this is - some help? Just so you know, I have a maze type thing (walls) and just a simple little 3 pixel ball. I tried it out with a more solid movieclip as well (a large circle) and it does the same. Here’s my code:

    //
    ball.addEventListener(Event.ENTER_FRAME, hitTest);

    function hitTest(e:Event):void{
    if(PixelPerfectCollisionDetection.isColliding(walls, ball, stage, true, 255)){
    hitCounter
    trace(hitCounter);
    }
    }
    //

  13. Troy on April 3rd, 2008

    If collisionRect comes back with zero size, then no overlap was found between the two display objects. I’d need to step through the code while executing on your actual data set to figure out exactly why there’s no overlap. Some questions:

    - Is a valid intersectionRect being calculated? What are its dimensions?

    - Are valid alphaMap’s being generated? What are their dimensions?

    - What path through the branches does the code take, i.e. where does the code return from each of the main functions?

    It should be pretty quick to answer these questions if you’re using an interactive debugger like Flex Builder.

  14. Robert on April 5th, 2008

    Troy -

    Is the above class the newer version (handling rotation, etc.) that you mentioned on 2.25.08 on the Boulevart blog? I am putting together an open-source piece that uses your class, and would like to make sure I have the latest version. Thanks.

  15. Troy on April 6th, 2008

    No, it’s not the latest version, I haven’t had time to finish/test it and put together the post (but I do have a draft I’m working on). The next few weeks are going to be pretty crazy for me, so it’ll probably be late April before I’m able to get it posted.

    The above source *does* handle rotation and all transforms. What it doesn’t account for is the visible flags or alpha values of of DisplayObjects higher up the display list… it’s not intended to, but some users have requested it.

  16. Robert on April 8th, 2008

    Troy -

    I just wanted to follow up and let you know I have officially released the open-source piece I alluded to in my last post. A link to the site can be found below. Thanks again for all your hard work.

    https://netfiles.umn.edu/users/frah0005/www/dynamicSpace/dynamicSpace.html

    I’d really appreciate reading your thoughts on the blog (a link to the blog can be found on the page).

  17. charli_e on April 21st, 2008

    HI ,
    Cool project! I’ve added to my favorites for future projects.

    Please, I’ve been testing perfectCollision with two movie clips, and I haven’t got the collision (similar case as Martin)

    If I use only BoundingBox(pixelPrecise as false)no problem.

    But using pixelPrecise as True then no collision appear. What I’m doing wrong? Is necessary to convert movie clips to Bitmaps? I’m lose.

    line of code:
    (PixelPerfectCollisionDetection.isColliding(siluetaMC, objMC, stage, true, 120))

    Best Regards

  18. Ben on May 13th, 2008

    I am having trouble properly importing the package into my actionscript file.
    I am using “import com.mysite.game.PixelPerfectCollisionDetection;” which is where I have placed the file in the directory structure. When I reference the isColliding function, I get a compiler error saying
    1180: call to a possibly undefined method isColliding.
    Any help you can offer is greatly appreciated.
    Thanks,
    Ben

  19. Hip-Hop on May 24th, 2008

    Thanks for the share!!!!!!!!!!!!

  20. CISNKY » links for 2008-06-23 on June 22nd, 2008

    [...] Pixel-Perfect Collision Detection in ActionScript3 Currently working on a site that requires collision detection. (tags: as3 collision) [...]

  21. Tink on June 24th, 2008

    we wrote a version here too http://www.tink.ws/blog/as-30-hittest/

  22. Steffen on July 8th, 2008

    Looks very promising — but I can’t get the pixelPrecise detection to work.

    Everythings seems fine in the debugger. intersectionRect, alpha1 and alpha2 have a dimension of e.g. 69×63, but collisionRect is 0×0.

    I tried with MovieClip and Bitmap objects.

    Any idea how to debug further?

    Would it make sense to have a look at the alpha bitmaps? Is there an easy way to do it?

  23. Steffen on July 8th, 2008

    Solved! *g*

    I used the main stage object as parent. Using obj.parent instead it works!

    But I can’t figure out, which object is being used now. The debugger says: exception thrown by getter! A little confusing…

    Anyway, it works… :)

  24. Daniel on August 3rd, 2008

    The tolerance needs to be a number between 0 and 1. See lines 41 - 47.

  25. Anonymous on August 9th, 2008

    How could I modify this code or Skinners code to subtract the intersecting pixels from one of the movie clips? I want to create some kind of deformable terrain.

  26. Eric on September 18th, 2008

    This may be a simple question, but I’m fairly new to Flash. So here’s the situation, I have imported the package declared on this page, but when I try and use the isColliding() function, I get the error: Access of undefined property PixelPerfectCollisionDetection.

    I have imported the package using:

    import classes.Collision;

    What am I doing wrong? Thanks for the help.

  27. Ronan Super on September 23rd, 2008

    Hi,
    I’ve tried different pixelperfect sollutions and I also encoutered the display list problem. Your function works fine for me. Thanks.

    Though, it would be easier for people if you posted an example in the blog post instead of a reply :) (I needed that example cos it didn’t work first time)

    @Eric
    I think you should give the actinoscript file the same name as the classname.
    So that would be PixelPerfectCollisionDetection.as, or rename the class.

  28. Someone on October 13th, 2008

    Hi there!

    Your collision class is not working really well. My simple random vector drawing gets detected inappropriately. I think it has something to do with the bounding rectangles, not very sure though.

    I used

    if (PixelPerfectCollisionDetection.getCollisionRect(blue, red, stage, true, 255) != null)
    trace(”Hit!”);

    Pure red and pure blue.
    So I can’t be wrong unless there’s something wrong with your functions. =)

    Best Regards,
    Someone

Leave a Reply