How to Rotate PixelMask for collision detection


(Eli Priest) #1

I am using a sprite with a pixelmask for collision detection. I want to rotate the sprite using sprite.angle, but the problem is that the pixelmask does not rotate along with the sprite as the pixelmask is still based on the original non-rotated sprite image.

How would I rotate the pixelmask along with the sprite?


(Joseph Sweeney) #2

As far as I can tell, this cannot be done in Flashpunk. The Pixelmask class uses the function BitmapData.hitTest to test for collisions when using pixelmasks, a test that can only be done with a given bitmap.

The Image class also seems to draw rotated images on the fly using a matrix.

Perhaps you could prerender each rotation of the sprite? This wouldn’t work with the built in angle attribute unless you created a new class and overrode some features, but it would allow you to create a pixelmask for each rotation. This might take too much memory though.

Your best bet is probably to forego using pixel collision unless it is absolutely necessary. A set of shapes that generally fit the hitbox of your entity would be much easier to rotate. I’ve never used anything but rectangles, though, so I don’t know how difficult that would be to implement.


(Wayne Makoto Sturdy) #3

I’ve actually created a class that does this. It’s not very polished yet but it should work with what you need.

The graphic of this class must be of type Image or anything that extends Image.

The class will generate the pixelMask based off the Image you pass in with the constructor, so you will want to do any positioning of the Image before you call it.

Alternatively, you can pass in a Rectangle if you know how big your pixelMask needs to be as well as its x/y offset.

The class has a rotation method that will rotate the Entity’s graphic or all the graphics if the graphic is a Graphiclist.

The class has other options and methods, check the source, it’s fairly well commented.

**DynamicMaskEntity.as**

I would extend this class for your own purposes but I’ll give you an example of instantiating the class itself:

var img:Image = new Image(IMG_ASSET);
img.centerOO();
img.smooth = true;
var DME:DynamicMaskEntity = new DynamicMaskEntity(300, 300, img);
add(DME);
//...later
DMG.rotation = 45;

(Alex Larioza) #4

I think the better answer is another question: do you really need the pixel mask? How are you using the pixel mask in your game? In most situations, you don’t really need such precision and performance would benefit from using a simple sphere or box mask.


(Wayne Makoto Sturdy) #5

That’s a good point too, but the pixelMask is part of the core for a reason.

The class I posted is part of a collection that I am working on and serves as a grandparent to a button class. When originally working on the button class, I was using hitboxes and the thought came up, what if I wanted to rotate a button, or if that button is oddly shaped? Hence the DynamicMaskEntity class.


(LoneStranger) #6

There was a TransformPixel mask that floated around the old forums. I have used it in my games a couple of times. If I remember correctly, you just call sync() whenever you change the entity’s image. Hope it helps.

package com.lonestranger.fpext.masks
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.geom.Matrix;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import net.flashpunk.graphics.Image;
	import net.flashpunk.masks.Pixelmask;
	import net.flashpunk.FP;
	/**
	* The purpose of this class is to extend Pixelmask and allow for the ability to rotate/scale the pixelmask.
	* It uses the same principles as ImageBuffer and SpritemapBuffer. Note that the extended world class will need to be
	* used to have this mask play nice with the other mask types as without it, it won't update on its own.
	* 
	* @author Blackstream at flashpunk.net forums
	*/
	public class TransformPixelmask extends Pixelmask
	{
		/**
		*
		* @param	source	The image to use as a mask.
		* @param	x	X offset of the mask.
		* @param	y	Y offset of the mask.
		* @param	i	Graphic of base type Image to link to. Null means sync will attempt to use the parent's base graphic
		*/
		public function TransformPixelmask(source:*, x:int = 0, y:int = 0, i:Image = null)
		{
			//For now create our source, but just pass it along to the super function to be our mask
			//We'll only create a buffer and do all the fancy stuff if we need to
			if (source is BitmapData) _sourceData = source;
			if (source is Class) _sourceData = FP.getBitmap(source);
			_source.bitmapData = _sourceData;
			super(_sourceData, x, y);
			_check[Pixelmask] = collideTransformPixelmask;
			_check[TransformPixelmask] = collideTransformPixelmask;
			link(i);
		}

		private function collideTransformPixelmask(other:Pixelmask):Boolean
		{
			_point.x = parent.x + x;
			_point.y = parent.y + y;
			_point2.x = other.parent.x + other.x;
			_point2.y = other.parent.y + other.y;
			return data.hitTest(_point, threshold, other.data, _point2, other.threshold);
		}

		protected function createBuffer():void {
			//newData will eventually be what we set data to. tempData is what we do all our work on first
			var newData:BitmapData;
			var tempData:BitmapData;

			//Calculate the maxlength, and use that to create our new buffer
			//The effectiveWidth and Height are twice the distance from the origin point of the picture to furthest relevent side.
			//i.e., if the origin point is 0,0, then the effective width and height are doubled because it's rotating on the corner
			var effectiveWidth:Number = 0;
			var effectiveHeight:Number = 0;

			if (this.originX >= (_sourceData.width / 2)) {
			effectiveWidth = this.originX * 2;
			} else if (this.originX < _sourceData.width / 2) {
			effectiveWidth = (_sourceData.width - this.originX) * 2;
			}
			effectiveWidth *= this.scaleX;

			if (this.originY >= (_sourceData.height / 2)) {
			effectiveHeight = this.originY * 2;
			} else if (this.originY < _sourceData.height / 2) {
			effectiveHeight = (_sourceData.height - this.originY) * 2;
			}
			effectiveHeight *= this.scaleY;

			//Calculate new width and height values, and account for scale
			var maxLength:int = Math.ceil(Math.sqrt(effectiveWidth * effectiveWidth + effectiveHeight * effectiveHeight));
			maxLength = Math.ceil(maxLength * this.scale);

			//Create the buffer
			tempData = new BitmapData(maxLength, maxLength, true, 0);
			var newOrigin:int = maxLength / 2;

			//Calculate offsets
			_offsetX = newOrigin - originX;
			_offsetY = newOrigin - originY;

			//Draw our buffer
			_matrix.b = _matrix.c = 0;
			_matrix.a = scaleX * scale;
			_matrix.d = scaleY * scale;
			_matrix.tx = -originX * _matrix.a;
			_matrix.ty = -originY * _matrix.d;
			if (angle != 0) _matrix.rotate(angle * FP.RAD);
			_matrix.tx += newOrigin;
			_matrix.ty += newOrigin;
			tempData.draw(_source, _matrix, null, null, null, false);

			//Get the bounds of our non-transparent pixels
			var crop:Rectangle = tempData.getColorBoundsRect(0xFF000000, 0, false);

			//Now create our actual data buffer, copy over the pixels, and adjust our offsets based on how much in front got cut off
			newData = new BitmapData(crop.width, crop.height, true, 0);
			newData.copyPixels(tempData, crop, new Point(0, 0), null, null, true);
			tempData.dispose();
			_offsetX -= crop.x;
			_offsetY -= crop.y;

			//Set our internal offsets to match our transform offsets. Note that our overloaded set x and y functions automatically
			//take into account offset.
			this.x = _sourceX;
			this.y = _sourceY;

			//Finally, set our data variable which will call the Pixelmask's set fucntion which will also update our entity's bounds
			if (data && data != _sourceData) data.dispose();
			data = newData;
		}

		/**
		* Purpose of this function is to 'sync' the mask with its parent graphic. This is called only by the BSWorld class as part of the overridden update() function.
		* If not using BSWorld, this will have to be manually called as FlashPunk doesn't normally have an update system for masks.
		*/
		public function sync():void {
			//If we have a linked image, use that. Otherwise check to see if our parent's graphic is an image and link to that and use it
			
			//trace(_link.scale + ":" + _link.scaleX + "," + _link.scaleY);
			if (!_link) {
				if (parent && parent.graphic && parent.graphic is Image) link((Image)(parent.graphic));
			}

			//Only worry about syncing if are linked to an Image class
			if (_link) {
				//Check if something has changed. Note that as long as long as the linked image stays at angle 0 and scale 1, this class will perform like
				//a normal pixelmask. Once it starts changing, then this class will start manipulating _data
				if (angle != _link.angle || scale != _link.scale || scaleX != _link.scaleX || scaleY != _link.scaleY || originX != _link.originX || originY != _link.originY) {
					//Sync our values and then create the buffer
					//Unlike ImageBuffer and the like, there is only create buffer because we always crop the buffer after drawing to maintain a more accurate hitbox
					//and ensure faster hitdetection.
					angle = _link.angle;
					scale = _link.scale;
					scaleX = _link.scaleX;
					scaleY = _link.scaleY;
					originX = _link.originX;
					originY = _link.originY;
					angle = _link.angle;
					createBuffer();
				}
			}
		}

		public function link(i:Image = null):void {
			if (_link == i) return;
			//Remake our buffer in addition to linking
			_link = i;
			if(_link) {
				angle = _link.angle;
				scale = _link.scale;
				scaleX = _link.scaleX;
				scaleY = _link.scaleY;
				originX = _link.originX;
				originY = _link.originY;
				angle = _link.angle;
				createBuffer();	
			}
		}

		public function release(): void {
			_link = null;
			if(data && data != _sourceData) data.dispose();
		}

		override protected function update():void
		{
			if(parent) super.update(); //No point in updating our parent if we don't have one
		}

		override public function set x(value:int):void
		{
			_sourceX = value;
			super.x = _sourceX - _offsetX;
		}

		override public function set y(value:int):void
		{
			_sourceY = value;
			super.y = _sourceY - _offsetY;
		}

		public var originX:int = 0;
		public var originY:int = 0;
		public var angle:Number = 0;
		public var scale:Number = 1;
		public var scaleX:Number = 1;
		public var scaleY:Number = 1;

		public var _sourceData:BitmapData;
		protected var _source:Bitmap = new Bitmap;

		/**
		* The followed 4 variables are all positional related variables
		* _sourceX	Original X offset of this hitbox
		* _sourceY	Original Y offset of this hitbox
		* _offsetX	X Offset of the transformed hitbox from the original hitbox
		* _offsetY	Y Offset of the transformed hitbox from the original hitbox
		* _x	Combined X offset from the above. Defined in class Hitbox.
		* _y	Combined Y offset from the above. Defined in the class Hitbox.
		*
		* Since all the hitchecking functions reference _x and _y directly, those become our true offset vars
		* and when createBuffer is called, it sets these values.
		*/

		protected var _sourceX:int = 0;
		protected var _sourceY:int = 0;
		protected var _offsetX:int = 0;
		protected var _offsetY:int = 0;

		protected var _link:Image = null;

		protected var _matrix:Matrix = FP.matrix;

		/** @private */ protected var _point:Point = FP.point;
		/** @private */ protected var _point2:Point = FP.point2;
	}

}