ActionScript 3.0: Per-Pixel Collision Detection inna rub-a-dub Peggle Stylee

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.

4 thoughts on “ActionScript 3.0: Per-Pixel Collision Detection inna rub-a-dub Peggle Stylee”

  1. 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!

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.