ActionScript 3.0: Per-Pixel Collision Detection inna rub-a-dub Peggle Stylee
r3dux | January 22, 2010
I’ve been working on writing a collision detection class, and it’s not as easy as you might think. It’s simple enough to boundary test (i.e. bounding-box overlap test), but to do per-pixel collisions you have to dump your symbol data into a BitmapData object, and you HAVE to use the top-left as the registration point for all your colliding symbol instances, which means you end up doing a lot of offsets & shiz… Ho hum.
Anyways, I knocked this Peggle / Pachinko thing together over the course of yesterday and today. You can place as many “pegs” as you want, wherever you want ‘em, same with the “balls”, and in theory, I should be able to scale both pegs and balls to any size while still keeping the collisions accurate, but I’ve not tested that yet because after debugging this for a while I need a break.
But hey, look at the purty colours
No source-code for this one just yet as it’s not in a fit state (it has waaaay too much debug output per collision that I don’t want to strip out just yet, and enough swearing to make a salty sea-dog blush
). When I’ve knocked it into shape some more I’ll chuck the Peg, Ball and Collision classes here in an update.
Update: Source code and flash files now available after the jump. Some of the code is a bit yucky, so apologies in advance for my bodge-hackery =P
This project uses the FPSCounter class (which can be found here), but it’s not new so I’m not putting the source in this post. It is, of course, included in the zip file at the bottom.
Flash File Source Code:
/* This code is a bit messy. I really should re-write it... */ // Import our custom classes import Peg; import Ball; import CollisionDetection; import FPSCounter; // Number of pegs to create. We actually create more than this due to // the nasty offset-grid code below var pegCount:Number = 6; var pegArray:Array = new Array; // Number of balls to create var ballCount:Number = 8; var ballArray:Array = new Array; function addPegs(pegCount:Number):void { var pegX:Number; var pegY:Number; // Nasty-ass code to set up a grid of pegs. // Loop to draw separate rows of pegs (i.e. controls vertical location) for (var loop:uint = 0; loop < (pegCount - 1); loop++) { // Move our pegY down to draw the next row pegY = (loop * 60) + 100; // Loop to draw individual pegs in a row (i.e. controls horizontal location) for (var loop2:uint = 0; loop2 < pegCount; loop2++) { // Draw 6 pegs on even rows if ( (loop % 2 == 0)) { pegX = (loop2 * 80) + 60; } else // Draw 5 pegs on odd rows { pegX = (loop2 * 80) + 20; } // On every odd row, and for the first peg only... if ((loop % 2 == 1) && (loop2 == 0)) { // Nothing to see here, move along. } else // Slap a peg on the screen... { // Create a new peg and push it to our peg array var newPeg:Peg = new Peg(pegX, pegY); pegArray.push(newPeg); // Add the peg to the stage stage.addChild(newPeg); } } // End of inner (left to right horizontal) peg adding loop } // End of outer (lines of pegs) loop. } // End of addPegs function function addBalls(ballCount:Number):void { var newBallX:Number; var newBallY:Number; var newBallXSpeed:Number; var newBallYSpeed:Number; for (var loop:uint = 0; loop < ballCount; loop++) { newBallX = Math.random() * stage.stageWidth; newBallY = -20 - (Math.random() * 100); newBallXSpeed = (Math.random() * 2) - 1; newBallYSpeed = 0; // Create a new ball and push it to our ball array var newBall:Ball = new Ball(newBallX, newBallY, newBallXSpeed, newBallYSpeed); ballArray.push(newBall); // Add the ball to our canvas movie clip (which we're going to blur) canvas_mc.addChild(newBall); } // End of ball adding loop } // End of addBalls function // Create our bitmapData object for the stage var bmData:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0x00000000); // Create a new bitmap image which we will pass the bitmapData var bitmap:Bitmap = new Bitmap(bmData); // Create the movieclip to draw our objects to be blurred onto var canvas_mc:MovieClip = new MovieClip; addChild(canvas_mc); // Add the r3dux.org banner to the top left by turning it into a // movie clip and drawing it from it's BitmapData (this means we can // have it on the blurred stage without the banner being blurred) var bannerClip_mc:MovieClip = new MovieClip; var myMsg:bannermsg = new bannermsg; myMsg.x = 0; myMsg.y = 0; bannerClip_mc.addChild(myMsg); // Add the banner to the banner movie clip stage.addChild(bannerClip_mc); // Add the banner movie clip to the stage // Add our FPS counter to the top right of the unblurred movie clip bannerClip_mc.addChild(new FPSCounter(510, 0)); // Get bitmap data and make a bitmap of our unblurred movie clip var bannerBitmapData:BitmapData = new BitmapData(bannerClip_mc.width, bannerClip_mc.height, true, 0x00000000); var bannerBitmap = new Bitmap(bannerBitmapData); // Put the actual bitmap of our screengrab onto our canvas object canvas_mc.addChild(bitmap) // Bind an event listener to call the drawStage function on each new frame stage.addEventListener(Event.ENTER_FRAME, drawStage); // Update our bitmap data with whatever's currently on the stage (i.e. this) // This will let make any moving objects move trails & add a blur so the trails fade out function drawStage(e:Event):void { // Actually draw our screengrab to the stage bmData.draw(this); canvas_mc.filters = [new BlurFilter(4,4,1)]; bannerBitmapData.draw(this); } // Create pegs and add to the stage addPegs(pegCount); // These things are a pain... // startMsg = the symbol of (blank) class StartMsg // myStartMsg = an instance of the StartMsg class // startMsg_mc = the movie clip I'll be adding the instance to. // Surely there has to be a cleaner way of doing this? var startMsg_mc:MovieClip = new MovieClip; var myStartMsg:StartMsg = new StartMsg; myStartMsg.x = stage.stageWidth / 2; myStartMsg.y = 50; startMsg_mc.addChild(myStartMsg); // Add the msg to the msg movie clip stage.addChild(startMsg_mc); // Finally add the msg movie clip to the stage // Wait for a clickto begin animation stage.addEventListener(MouseEvent.MOUSE_DOWN, toggleAnimation); var started:Boolean = true; var stopFlag:Boolean = true; function toggleAnimation(e:MouseEvent):void { // Only ever add the balls & remove the instructions once if (started == true) { // Flip the flag started = false; // Remove instructions from stage stage.removeChild(startMsg_mc); // Create balls and add to the stage addBalls(ballCount); // Pass our ball and peg arrays to the CollisionDetection class & let it do its magic addChild(new CollisionDetection(ballArray, pegArray)); } else { // Change the framerate between 0 and 30 fps on subsequent clicks if (stopFlag == true) { stopFlag = false; stage.frameRate = 0; } else { stopFlag = true; stage.frameRate = 30; } } }
Peg Class Source Code:
package { import flash.display.MovieClip; import flash.events.Event; import flash.display.BitmapData; import flash.geom.Rectangle; public class Peg extends MovieClip { // instance variables var beenHit:Boolean; // If a peg's been hit, start it's disappear timer [not used at present, could use to be more peggle-y tho...] var pegBitmapData:BitmapData; // BitmapData object to use with collision detection var centerX:Number; // Peg's center X in screen co-ordinates var centerY:Number; // Peg's center Y in screen co-ordinates // Constructor public function Peg(theXLocation:Number, theYLocation:Number, theScale:Number = 1) { this.x = theXLocation; this.y = theYLocation; this.scaleX = this.scaleY = theScale; this.beenHit = false; // Because we have to assign the top left of our movie clip as the origin // to use per-pixel collision detection, it's worth keeping hold of the center // co-ordinates this.centerX = this.x + (this.width / 2); this.centerY = this.y + (this.height / 2); // Generate our peg BitmapData for per-pixel collision detection this.pegBitmapData = new BitmapData(this.width, this.height, true, 0x00000000); // Draw the peg data into our BitmapData object this.pegBitmapData.draw(this); } // End of constructor } // End of class } // End of package
Ball Class Source Code:
package { import flash.display.MovieClip; import flash.events.Event; import flash.display.BitmapData; import flash.geom.Rectangle; import flash.geom.ColorTransform; public class Ball extends MovieClip { // Instance variables var xSpeed:Number; var ySpeed:Number; var centerX:Number; var centerY:Number; var ballBitmapData:BitmapData; // BitmapData object to use with collision detection // Class-wide variables static var gravity:Number = 0.1; // Constructor public function Ball(theXLocation:Number, theYLocation:Number, theXSpeed:Number = 0, theYSpeed:Number = 0):void { // Set our passed locations and speeds this.x = theXLocation; this.y = theYLocation; this.xSpeed = theXSpeed; this.ySpeed = theYSpeed; // Because we have to assign the top left of our movie clip as the origin // to use per-pixel collision detection, it's worth keeping hold of the center // co-ordinates this.centerX = this.x + (this.width / 2); this.centerY = this.y + (this.height / 2); // Generate our peg BitmapData for per-pixel collision detection this.ballBitmapData = new BitmapData(this.width, this.height, true, 0x00000000); // Draw the ball data into our BitmapData object this.ballBitmapData.draw(this); // Call the updateBall function once per frame for each ball instance this.addEventListener(Event.ENTER_FRAME, updateBall); } private function updateBall(e:Event):void { // Apply gravity this.ySpeed += gravity; // Add our x and y speeds this.x += xSpeed; this.y += ySpeed; // Recalc our center this.centerX = (this.x + this.width) / 2; this.centerY = (this.y + this.height) / 2; // Once the particle has left the stage reset it. // Remember that our registration point is the top left of the object. // We're taking care of particles off the left and right of the stage to minimise // the area of the movie clip the balls are attached to, thus minimising the // area that needs to have the (computationally expensive) blur filter applied to it. if ( (this.y > (400 + this.height)) || (this.x > (550 + this.width)) || (this.x < (0 - this.width)) ) { this.x = Math.random() * 550; this.y = -20 - (Math.random() * 50); this.ySpeed = 0; this.xSpeed = (Math.random() * 2) - 1; // Change ball colour via colorTransform this.randomiseBallColour(); } // End of if ball is off the bottom of the stage condition } // End of updateBall function public function randomiseBallColour():void { var myColourTransform:ColorTransform = this.transform.colorTransform; // This will change the color of all layers and all sub-symbols within this symbol: //myColourTransform.color = 0xff0000; // Shift each colour channel (you can shift in the range: -255 to 255). myColourTransform.redOffset = (Math.random() * 300) - 150; myColourTransform.greenOffset = (Math.random() * 300) - 150; myColourTransform.blueOffset = (Math.random() * 300) - 150; // This number will multiply by the green channel only (decimal value). There is also a redMultiplier and blueMultiplier. // This will essentially saturate/desaturate the color channel. // colorTransform.greenMultiplier = 2; // re-assign the ColorTransform back to this symbol this.transform.colorTransform = myColourTransform; } // End of randomiseBallColour function } // End of class } // End of package
CollisionDetection Class Source Code:
package { import flash.display.MovieClip; import flash.events.Event; import flash.geom.Point; public class CollisionDetection extends MovieClip { // Instance variables var ball:Array; var peg:Array; // Conversion to/from degrees/radians constants public const deg2rad:Number = Math.PI / 180; public const rad2deg:Number = 180 / Math.PI; // Constructor public function CollisionDetection(theBallArray:Array, thePegArray:Array):void { // Create instance copies of the ball and peg arrays this.ball = theBallArray; this.peg = thePegArray; // Check for collisions once per frame this.addEventListener(Event.ENTER_FRAME, CheckForCollisions) } // Function to check for collisions and rebound balls appropriately. // Apologies in advance for the "weak-sauce ball movers", I know I should // really calculate the correct offsets using the rebound angle and the // peg radius, but I keep buggering it up, so I've just gone with a simple // method which seems to work. public function CheckForCollisions(e:Event):void { // For every ball in our ball array... for (var ballLoop:Number = 0; ballLoop < this.ball.length; ballLoop++) { // For every peg in our peg array... for (var pegLoop:Number = 0; pegLoop < this.peg.length; pegLoop++) { // Perform bounds checking first, it's hella quicker than per-pixel... if (this.ball[ballLoop].hitTestObject(this.peg[pegLoop]) ) { //trace("Boundary overlap detected.") // If we've detected a boundary collision, move to the per-pixel detection if(this.ball[ballLoop].ballBitmapData.hitTest(new Point(this.ball[ballLoop].x, this.ball[ballLoop].y), 255, this.peg[pegLoop].pegBitmapData, new Point(this.peg[pegLoop].x, this.peg[pegLoop].y), 255)) { //trace("\nIn per-pixel collision detection, ball number: " + ballLoop + " hit peg number: " + pegLoop); // Calculate offset between the ball and the peg var horizOffset:Number = this.ball[ballLoop].x - this.peg[pegLoop].x; var vertOffset:Number = this.ball[ballLoop].y - this.peg[pegLoop].y; //trace("Horiz offset on collision: " + horizOffset); //trace("Vertical offset on collision: " + vertOffset); // Get the angle the ball will bounce away at. Modified so that 0/360 degrees points up var angleRads = Math.atan2(vertOffset, horizOffset); angleRads += (90 * deg2rad); // Get our angle in degees. // This will be correct without modification because it works off the already modified angle in radians var angleDegrees = angleRads * rad2deg; //trace("Angle on collision: " + angleRads + ", which is: " + angleDegrees + " degrees."); // Get the vector speed of the ball. Mad props 2 Pythagoras =P var vectorSpeed = Math.sqrt( Math.pow(this.ball[ballLoop].xSpeed, 2) + Math.pow(this.ball[ballLoop].ySpeed, 2) ); // Dampen the collision by 10%. Changing this to > 1 gives you super-elastic // collisions (balls rebound faster than they initially hit). Hehe. vectorSpeed *= 0.9; // Resolve horizontal and vertical components of speed vector var newXSpeed:Number = vectorSpeed * Math.sin(angleRads); var newYSpeed:Number = vectorSpeed * Math.cos(angleRads); // Set our new speeds this.ball[ballLoop].xSpeed = newXSpeed; this.ball[ballLoop].ySpeed = newYSpeed; // Handle horizontal component of collision // If the ball is to the right of peg... if (horizOffset > 0) { //trace("Ball is to the right of peg"); // Weak-sauce mover to avoid double-bounces this.ball[ballLoop].x += 1; } else if (horizOffset < 0) // If the ball is to the left of peg... { // Ball is to the left of peg //trace("Ball is to the left of peg"); // Weak-sauce mover to avoid double-bounces this.ball[ballLoop].x -= 1; } // Handle vertical component of collision // If the ball is above the peg var newYLocation:Number; if (vertOffset < 0) { //trace("Ball is above peg"); // Flip the vertical speed if (newYSpeed > 0) { this.ball[ballLoop].ySpeed = newYSpeed * -1; } // Weak-sauce mover to avoid double-bounces this.ball[ballLoop].y -= 1; } else if (vertOffset > 0) // If the ball is below the peg... { //trace("Ball is below peg"); // Flip the vertical speed if (newYSpeed < 0) { this.ball[ballLoop].ySpeed = newYSpeed * -1; } // Weak-sauce mover to avoid double-bounces this.ball[ballLoop].y += 1; // Simple } // End of if ball is below peg condition // Stop balls from staying directly on top of pegs by adding some jitter! if (this.ball[ballLoop].xSpeed == 0) { this.ball[ballLoop].xSpeed = (Math.random() * 0.002) - 0.001; } } // End of per pixel checker } // End of bounding box checker } // End of inner peg loop } // End of outer ball loop } // End of checkCollision function } // End of class } // End of package
All source files can be found: here.
No related posts.











[...] stuck on a canvas below the bats & balls. And all the collission-detection is done through my CollisionDetection class, so I can have as many bats and balls as I want, and it took a whole five minutes to [...]
i want FlA file please
send to email please thank you
Do you see where it says: “All source files can be found: here” at the bottom of the post?
The here is a link to a zip file containing all the source code, which includes the .FLA file…
You’re welcome!