Last active
January 2, 2016 12:39
-
-
Save gamedevsam/8304760 to your computer and use it in GitHub Desktop.
FlxCollision optimization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package flixel.util; | |
import flash.display.BitmapData; | |
import flash.display.BlendMode; | |
import flash.geom.ColorTransform; | |
import flash.geom.Matrix; | |
import flash.geom.Point; | |
import flash.geom.Rectangle; | |
import flixel.FlxCamera; | |
import flixel.FlxG; | |
import flixel.FlxSprite; | |
import flixel.group.FlxGroup; | |
import flixel.tile.FlxTileblock; | |
/** | |
* FlxCollision | |
* | |
* @link http://www.photonstorm.com | |
* @author Richard Davey / Photon Storm | |
*/ | |
class FlxCollision | |
{ | |
inline static public var CAMERA_WALL_OUTSIDE:Int = 0; | |
inline static public var CAMERA_WALL_INSIDE:Int = 1; | |
// Store tmp images indexed by size, used to perform collision checks | |
static public var imgCache:Map<Int, BitmapData> = Map<Int, BitmapData>(); | |
static public var debug:BitmapData = new BitmapData(1, 1, false); | |
// Optimization: Local static vars to reduce allocations | |
private var pointA:Point = new Point(); | |
private var pointB:Point = new Point(); | |
private var centerA:Point = new Point(); | |
private var centerB:Point = new Point(); | |
private var matrixA:Matrix = new Matrix(); | |
private var matrixB:Matrix = new Matrix(); | |
private var testMatrix:Matrix = new Matrix(); | |
private var boundsA:Rectangle = new Rectangle(); | |
private var boundsB:Rectangle = new Rectangle(); | |
/** | |
* A Pixel Perfect Collision check between two FlxSprites. | |
* It will do a bounds check first, and if that passes it will run a pixel perfect match on the intersecting area. | |
* Works with rotated and animated sprites. | |
* It's extremly slow on cpp targets, so I don't recommend you to use it on them. | |
* Not working on neko target and awfully slows app down | |
* | |
* @param Contact The first FlxSprite to test against | |
* @param Target The second FlxSprite to test again, sprite order is irrelevant | |
* @param AlphaTolerance The tolerance value above which alpha pixels are included. Default to 255 (must be fully opaque for collision). | |
* @param Camera If the collision is taking place in a camera other than FlxG.camera (the default/current) then pass it here | |
* @return Boolean True if the sprites collide, false if not | |
*/ | |
static public function pixelPerfectCheck(Contact:FlxSprite, Target:FlxSprite, AlphaTolerance:Int = 255, ?Camera:FlxCamera):Bool | |
{ | |
//if either of the angles are non-zero, consider the angles of the sprites in the pixel check | |
var considerRotation:Bool = Contact.angle != 0 || Target.angle != 0; | |
if (Camera != null) | |
{ | |
pointA.x = Contact.x - Std.int(Camera.scroll.x * Contact.scrollFactor.x) - Contact.offset.x; | |
pointA.y = Contact.y - Std.int(Camera.scroll.y * Contact.scrollFactor.y) - Contact.offset.y; | |
pointB.x = Target.x - Std.int(Camera.scroll.x * Target.scrollFactor.x) - Target.offset.x; | |
pointB.y = Target.y - Std.int(Camera.scroll.y * Target.scrollFactor.y) - Target.offset.y; | |
} | |
else | |
{ | |
pointA.x = Contact.x - Std.int(FlxG.camera.scroll.x * Contact.scrollFactor.x) - Contact.offset.x; | |
pointA.y = Contact.y - Std.int(FlxG.camera.scroll.y * Contact.scrollFactor.y) - Contact.offset.y; | |
pointB.x = Target.x - Std.int(FlxG.camera.scroll.x * Target.scrollFactor.x) - Target.offset.x; | |
pointB.y = Target.y - Std.int(FlxG.camera.scroll.y * Target.scrollFactor.y) - Target.offset.y; | |
} | |
if (considerRotation) | |
{ | |
// find the center of both sprites | |
centerA.set(Contact.origin.x, Contact.origin.y); | |
centerB.set(Target.origin.x, Target.origin.y); | |
// now make a bounding box that allows for the sprite to be rotated in 360 degrees | |
boundsA.set( | |
(pointA.x + centerA.x - centerA.length), | |
(pointA.y + centerA.y - centerA.length), | |
centerA.length*2, centerA.length*2); | |
boundsB.set( | |
(pointB.x + centerB.x - centerB.length), | |
(pointB.y + centerB.y - centerB.length), | |
centerB.length*2, centerB.length*2); | |
} | |
else | |
{ | |
#if flash | |
boundsA.set(pointA.x, pointA.y, Contact.framePixels.width, Contact.framePixels.height); | |
boundsB.set(pointB.x, pointB.y, Target.framePixels.width, Target.framePixels.height); | |
#else | |
boundsA.set(pointA.x, pointA.y, Contact.frameWidth, Contact.frameHeight); | |
boundsB.set(pointB.x, pointB.y, Target.frameWidth, Target.frameHeight); | |
#end | |
} | |
var intersect:Rectangle = boundsA.intersection(boundsB); | |
if (intersect.isEmpty() || intersect.width == 0 || intersect.height == 0) | |
{ | |
return false; | |
} | |
// Normalise the values or it'll break the BitmapData creation below | |
intersect.x = Math.floor(intersect.x); | |
intersect.y = Math.floor(intersect.y); | |
intersect.width = Math.ceil(intersect.width); | |
intersect.height = Math.ceil(intersect.height); | |
if (intersect.isEmpty()) | |
{ | |
return false; | |
} | |
// Thanks to Chris Underwood for helping with the translate logic :) | |
matrixA.identity(); | |
matrixA.translate(-(intersect.x - boundsA.x), -(intersect.y - boundsA.y)); | |
matrixB.identity(); | |
matrixB.translate(-(intersect.x - boundsB.x), -(intersect.y - boundsB.y)); | |
#if !flash | |
Contact.drawFrame(); //@Beeblerox: why is this necessary? - crazysam 1/2014 | |
Target.drawFrame(); //@Beeblerox: why is this necessary? - crazysam 1/2014 | |
#end | |
var testA:BitmapData = Contact.framePixels; | |
var testB:BitmapData = Target.framePixels; | |
var imgSize = intersect.width * intersect.height; | |
var overlapArea:BitmapData = imgCache.get(imgSize); | |
if(overlapArea == null) | |
{ | |
FlxG.log.add("pixelPerfectCollision: New BitmapData, size = " + imgSize); | |
overlapArea = new BitmapData(intersect.width, intersect.height, false); | |
imgCache.set(imgSize, overlapArea); | |
} | |
// More complicated case, if either of the sprites is rotated | |
if (considerRotation) | |
{ | |
testMatrix.identity(); | |
// translate the matrix to the center of the sprite | |
testMatrix.translate( -Contact.origin.x, -Contact.origin.y); | |
// rotate the matrix according to angle | |
testMatrix.rotate(Contact.angle * 0.017453293 ); // degrees to rad | |
// translate it back! | |
testMatrix.translate(boundsA.width / 2, boundsA.height / 2); | |
// prepare an empty canvas | |
imgSize = Std.int(boundsA.width) * Std.int(boundsA.height); | |
var testA2:BitmapData = imgCache.get(imgSize); | |
if(testA2 == null) | |
{ | |
FlxG.log.add("pixelPerfectCollision: New BitmapData, size = " + imgSize); | |
testA2 = new BitmapData(Math.floor(boundsA.width) , Math.floor(boundsA.height), true, 0x00000000); | |
imgCache.set(imgSize, testA2); | |
} | |
// plot the sprite using the matrix | |
testA2.draw(testA, testMatrix, null, null, null, false); | |
testA = testA2; | |
// (same as above) | |
testMatrix.identity(); | |
testMatrix.translate(-Target.origin.x,-Target.origin.y); | |
testMatrix.rotate(Target.angle * 0.017453293 ); // degrees to rad | |
testMatrix.translate(boundsB.width/2,boundsB.height/2); | |
imgSize = Math.floor(boundsB.width) * Math.floor(boundsB.height); | |
var testB2:BitmapData = imgCache.get(imgSize); | |
if(testB2 == null) | |
{ | |
FlxG.log.add("pixelPerfectCollision: New BitmapData, size = " + imgSize); | |
testB2 = new BitmapData(Math.floor(boundsB.width), Math.floor(boundsB.height), true, 0x00000000); | |
imgCache.set(imgSize, testB2); | |
} | |
testB2.draw(testB, testMatrix, null, null, null, false); | |
testB = testB2; | |
} | |
#if flash | |
overlapArea.draw(testA, matrixA, new ColorTransform(1, 1, 1, 1, 255, -255, -255, AlphaTolerance), BlendMode.NORMAL); | |
overlapArea.draw(testB, matrixB, new ColorTransform(1, 1, 1, 1, 255, 255, 255, AlphaTolerance), BlendMode.DIFFERENCE); | |
#else | |
// TODO: try to fix this method for neko target | |
var overlapWidth:Int = overlapArea.width; | |
var overlapHeight:Int = overlapArea.height; | |
var targetX:Int; | |
var targetY:Int; | |
var pixelColor:Int; | |
var pixelAlpha:Int; | |
var transformedAlpha:Int; | |
var maxX:Int = testA.width + 1; | |
var maxY:Int = testA.height + 1; | |
for (i in 0...maxX) | |
{ | |
targetX = Math.floor(i + matrixA.tx); | |
if (targetX >= 0 && targetX < maxX) | |
{ | |
for (j in 0...maxY) | |
{ | |
targetY = Math.floor(j + matrixA.ty); | |
if (targetY >= 0 && targetY < maxY) | |
{ | |
pixelColor = testA.getPixel32(i, j); | |
pixelAlpha = (pixelColor >> 24) & 0xFF; | |
if (pixelAlpha >= AlphaTolerance) | |
{ | |
overlapArea.setPixel32(targetX, targetY, 0xffff0000); | |
} | |
else | |
{ | |
overlapArea.setPixel32(targetX, targetY, FlxColor.WHITE); | |
} | |
} | |
} | |
} | |
} | |
maxX = testB.width + 1; | |
maxY = testB.height + 1; | |
var secondColor:Int; | |
for (i in 0...maxX) | |
{ | |
targetX = Math.floor(i + matrixB.tx); | |
if (targetX >= 0 && targetX < maxX) | |
{ | |
for (j in 0...maxY) | |
{ | |
targetY = Math.floor(j + matrixB.ty); | |
if (targetY >= 0 && targetY < maxY) | |
{ | |
pixelColor = testB.getPixel32(i, j); | |
pixelAlpha = (pixelColor >> 24) & 0xFF; | |
if (pixelAlpha >= AlphaTolerance) | |
{ | |
secondColor = overlapArea.getPixel32(targetX, targetY); | |
if (secondColor == 0xffff0000) | |
{ | |
overlapArea.setPixel32(targetX, targetY, 0xff00ffff); | |
} | |
else | |
{ | |
overlapArea.setPixel32(targetX, targetY, 0x00000000); | |
} | |
} | |
} | |
} | |
} | |
} | |
#end | |
// Developers: If you'd like to see how this works enable the debugger and display it in your game somewhere. | |
debug = overlapArea; | |
var overlap:Rectangle = overlapArea.getColorBoundsRect(0xffffffff, 0xff00ffff); | |
overlap.offset(intersect.x, intersect.y); | |
return(!overlap.isEmpty()); | |
} | |
/** | |
* A Pixel Perfect Collision check between a given x/y coordinate and an FlxSprite<br> | |
* | |
* @param PointX The x coordinate of the point given in local space (relative to the FlxSprite, not game world coordinates) | |
* @param PointY The y coordinate of the point given in local space (relative to the FlxSprite, not game world coordinates) | |
* @param Target The FlxSprite to check the point against | |
* @param AlphaTolerance The alpha tolerance level above which pixels are counted as colliding. Default to 255 (must be fully transparent for collision) | |
* @return Boolean True if the x/y point collides with the FlxSprite, false if not | |
*/ | |
static public function pixelPerfectPointCheck(PointX:Int, PointY:Int, Target:FlxSprite, AlphaTolerance:Int = 255):Bool | |
{ | |
// Intersect check | |
if (FlxMath.pointInCoordinates(PointX, PointY, Math.floor(Target.x), Math.floor(Target.y), Std.int(Target.width), Std.int(Target.height)) == false) | |
{ | |
return false; | |
} | |
#if flash | |
// How deep is pointX/Y within the rect? | |
var test:BitmapData = Target.framePixels; | |
#else | |
var test:BitmapData = Target.getFlxFrameBitmapData(); | |
#end | |
var pixelAlpha:Int = 0; | |
pixelAlpha = FlxColorUtil.getAlpha(test.getPixel32(Math.floor(PointX - Target.x), Math.floor(PointY - Target.y))); | |
#if !flash | |
pixelAlpha = Std.int(pixelAlpha * Target.alpha); | |
#end | |
// How deep is pointX/Y within the rect? | |
if (pixelAlpha >= AlphaTolerance) | |
{ | |
return true; | |
} | |
else | |
{ | |
return false; | |
} | |
} | |
/** | |
* Creates a "wall" around the given camera which can be used for FlxSprite collision | |
* | |
* @param Camera The FlxCamera to use for the wall bounds (can be FlxG.camera for the current one) | |
* @param Placement CAMERA_WALL_OUTSIDE or CAMERA_WALL_INSIDE | |
* @param Thickness The thickness of the wall in pixels | |
* @param AdjustWorldBounds Adjust the FlxG.worldBounds based on the wall (true) or leave alone (false) | |
* @return FlxGroup The 4 FlxTileblocks that are created are placed into this FlxGroup which should be added to your State | |
*/ | |
static public function createCameraWall(Camera:FlxCamera, Placement:Int, Thickness:Int, AdjustWorldBounds:Bool = false):FlxGroup | |
{ | |
var left:FlxTileblock = null; | |
var right:FlxTileblock = null; | |
var top:FlxTileblock = null; | |
var bottom:FlxTileblock = null; | |
switch (Placement) | |
{ | |
case FlxCollision.CAMERA_WALL_OUTSIDE: | |
left = new FlxTileblock(Math.floor(Camera.x - Thickness), Math.floor(Camera.y + Thickness), Thickness, Camera.height - (Thickness * 2)); | |
right = new FlxTileblock(Math.floor(Camera.x + Camera.width), Math.floor(Camera.y + Thickness), Thickness, Camera.height - (Thickness * 2)); | |
top = new FlxTileblock(Math.floor(Camera.x - Thickness), Math.floor(Camera.y - Thickness), Camera.width + Thickness * 2, Thickness); | |
bottom = new FlxTileblock(Math.floor(Camera.x - Thickness), Camera.height, Camera.width + Thickness * 2, Thickness); | |
if (AdjustWorldBounds) | |
{ | |
FlxG.worldBounds.set(Camera.x - Thickness, Camera.y - Thickness, Camera.width + Thickness * 2, Camera.height + Thickness * 2); | |
} | |
case FlxCollision.CAMERA_WALL_INSIDE: | |
left = new FlxTileblock(Math.floor(Camera.x), Math.floor(Camera.y + Thickness), Thickness, Camera.height - (Thickness * 2)); | |
right = new FlxTileblock(Math.floor(Camera.x + Camera.width - Thickness), Math.floor(Camera.y + Thickness), Thickness, Camera.height - (Thickness * 2)); | |
top = new FlxTileblock(Math.floor(Camera.x), Math.floor(Camera.y), Camera.width, Thickness); | |
bottom = new FlxTileblock(Math.floor(Camera.x), Camera.height - Thickness, Camera.width, Thickness); | |
if (AdjustWorldBounds) | |
{ | |
FlxG.worldBounds.set(Camera.x, Camera.y, Camera.width, Camera.height); | |
} | |
} | |
var result:FlxGroup = new FlxGroup(4); | |
result.add(left); | |
result.add(right); | |
result.add(top); | |
result.add(bottom); | |
return result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment