diff --git a/net/flashpunk/Engine.as b/net/flashpunk/Engine.as index 1d6c900..7be8b1a 100644 --- a/net/flashpunk/Engine.as +++ b/net/flashpunk/Engine.as @@ -204,12 +204,12 @@ FP.elapsed *= FP.rate; _last = _time; - // update console - if (FP._console) FP._console.update(); - // update loop if (!paused) update(); + // update console + if (FP._console) FP._console.update(); + // update input Input.update(); diff --git a/net/flashpunk/Mask.as b/net/flashpunk/Mask.as index fbb9693..dfd59ec 100644 --- a/net/flashpunk/Mask.as +++ b/net/flashpunk/Mask.as @@ -1,6 +1,7 @@ package net.flashpunk { import flash.display.Graphics; + import flash.geom.Point; import flash.utils.Dictionary; import flash.utils.getDefinitionByName; import flash.utils.getQualifiedClassName; @@ -45,7 +46,7 @@ } /** @private Collide against an Entity. */ - private function collideMask(other:Mask):Boolean + protected function collideMask(other:Mask):Boolean { return parent.x - parent.originX + parent.width > other.parent.x - other.parent.originX && parent.y - parent.originY + parent.height > other.parent.y - other.parent.originY @@ -78,6 +79,33 @@ } + /** @private Projects this mask points on axis and returns min and max values in projection object. */ + public function project(axis:Point, projection:Object):void + { + var cur:Number, + max:Number = Number.NEGATIVE_INFINITY, + min:Number = Number.POSITIVE_INFINITY; + + cur = -parent.originX * axis.x - parent.originY * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = (-parent.originX + parent.width) * axis.x - parent.originY * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = -parent.originX * axis.x + (-parent.originY + parent.height) * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = (-parent.originX + parent.width) * axis.x + (-parent.originY + parent.height)* axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + projection.min = min; + projection.max = max; + } + // Mask information. /** @private */ private var _class:Class; /** @private */ protected var _check:Dictionary = new Dictionary; diff --git a/net/flashpunk/graphics/Image.as b/net/flashpunk/graphics/Image.as index 67eab97..0ac363e 100644 --- a/net/flashpunk/graphics/Image.as +++ b/net/flashpunk/graphics/Image.as @@ -2,6 +2,7 @@ package net.flashpunk.graphics { import flash.display.*; import flash.geom.*; + import net.flashpunk.masks.Polygon; import net.flashpunk.*; @@ -137,17 +138,38 @@ package net.flashpunk.graphics * @param width Width of the rectangle. * @param height Height of the rectangle. * @param color Color of the rectangle. + * @param alpha Alpha of the rectangle. + * @param fill If the rectangle should be filled with the color (true) or just an outline (false). + * @param thick How thick the outline should be (only applicable when fill = false). + * @param radius Round rectangle corners by this amount. * @return A new Image object. */ - public static function createRect(width:uint, height:uint, color:uint = 0xFFFFFF, alpha:Number = 1):Image + public static function createRect(width:uint, height:uint, color:uint = 0xFFFFFF, alpha:Number = 1, fill:Boolean = true, thick:Number = 1, radius:Number = 0):Image { - var source:BitmapData = new BitmapData(width, height, true, 0xFFFFFFFF); + var graphics:Graphics = FP.sprite.graphics; - var image:Image = new Image(source); + if (color > 0xFFFFFF) color = 0xFFFFFF & color; + graphics.clear(); - image.color = color; - image.alpha = alpha; + var thickOffset:Number = 0; + if (fill) { + graphics.beginFill(color, alpha); + } else { + thickOffset = thick * .5; + graphics.lineStyle(thick, color, alpha, false, LineScaleMode.NORMAL, null, JointStyle.MITER); + } + if (radius <= 0) { + graphics.drawRect(0 + thickOffset, 0 + thickOffset, width - thickOffset * 2, height - thickOffset * 2); + } else { + graphics.drawRoundRect(0 + thickOffset, 0 + thickOffset, width - thickOffset * 2, height - thickOffset * 2, radius); + } + graphics.endFill(); + + var data:BitmapData = new BitmapData(width, height, true, 0); + data.draw(FP.sprite); + + var image:Image = new Image(data); return image; } @@ -156,21 +178,27 @@ package net.flashpunk.graphics * @param radius Radius of the circle. * @param color Color of the circle. * @param alpha Alpha of the circle. + * @param fill If the circle should be filled with the color (true) or just an outline (false). + * @param thick How thick the outline should be (only applicable when fill = false). * @return A new Image object. */ - public static function createCircle(radius:uint, color:uint = 0xFFFFFF, alpha:Number = 1):Image + public static function createCircle(radius:uint, color:uint = 0xFFFFFF, alpha:Number = 1, fill:Boolean = true, thick:Number = 1):Image { - FP.sprite.graphics.clear(); - FP.sprite.graphics.beginFill(0xFFFFFF); - FP.sprite.graphics.drawCircle(radius, radius, radius); + var graphics:Graphics = FP.sprite.graphics; + + graphics.clear(); + if (fill) { + graphics.beginFill(color & 0xFFFFFF, alpha); + graphics.drawCircle(radius, radius, radius); + graphics.endFill(); + } else { + graphics.lineStyle(thick, color & 0xFFFFFF, alpha); + graphics.drawCircle(radius, radius, radius - thick * .5); + } var data:BitmapData = new BitmapData(radius * 2, radius * 2, true, 0); data.draw(FP.sprite); var image:Image = new Image(data); - - image.color = color; - image.alpha = alpha; - return image; } @@ -188,7 +216,7 @@ package net.flashpunk.graphics * @param toAlpha Alpha at end of gradient. * @return A new Image object. */ - public static function createGradient (width:uint, height:uint, fromX:Number, fromY:Number, toX:Number, toY:Number, fromColor:uint, toColor:uint, fromAlpha:Number = 1, toAlpha:Number = 1):Image + public static function createGradient(width:uint, height:uint, fromX:Number, fromY:Number, toX:Number, toY:Number, fromColor:uint, toColor:uint, fromAlpha:Number = 1, toAlpha:Number = 1):Image { var bitmap:BitmapData = new BitmapData(width, height, true, 0x0); @@ -227,6 +255,52 @@ package net.flashpunk.graphics return new Image(bitmap); } + /** + * Creates a new polygon Image from an array of points. + * @param points An array of coordinates (must be positive) that define the polygon. + * @param color Color of the polygon. + * @param alpha Alpha of the polygon. + * @param fill If the polygon should be filled with the color (true) or just an outline (false). + * @param thick How thick the outline should be (only applicable when fill = false). + * @return A new Image object. + */ + public static function createPolygon(points:Vector., color:uint = 0xFFFFFF, alpha:Number = 1, fill:Boolean = true, thick:Number = 1):Image + { + var graphics:Graphics = FP.sprite.graphics; + var p:Point; + + var maxX:Number = Number.NEGATIVE_INFINITY; + var maxY:Number = Number.NEGATIVE_INFINITY; + + // find max x and y coords + for (var i:int = 0; i < points.length; i++) { + p = points[i]; + if (p.x > maxX) maxX = p.x; + if (p.y > maxY) maxY = p.y; + } + + if (color > 0xFFFFFF) color = 0xFFFFFF & color; + graphics.clear(); + + if (fill) { + graphics.beginFill(color, alpha); + } else { + graphics.lineStyle(thick, color, alpha, false, LineScaleMode.NORMAL, null, JointStyle.MITER); + } + + graphics.moveTo(points[points.length - 1].x, points[points.length - 1].y); + for (var j:int = 0; j < points.length; j++) { + p = points[j]; + graphics.lineTo(p.x, p.y); + } + graphics.endFill(); + + var data:BitmapData = new BitmapData(maxX, maxY, true, 0); + data.draw(FP.sprite); + + return new Image(data); + } + /** * Updates the image buffer. */ diff --git a/net/flashpunk/masks/Circle.as b/net/flashpunk/masks/Circle.as new file mode 100644 index 0000000..5f6a95c --- /dev/null +++ b/net/flashpunk/masks/Circle.as @@ -0,0 +1,242 @@ +package net.flashpunk.masks +{ + + import flash.display.BitmapData; + import net.flashpunk.FP; + import net.flashpunk.Graphic; + import net.flashpunk.Mask; + import net.flashpunk.masks.Grid; + import flash.display.Graphics; + import flash.geom.Point; + + /** + * Uses circular area to determine collision. + */ + public class Circle extends Hitbox + { + /** + * Constructor. + * @param radius Radius of the circle. + * @param x X offset of the circle. + * @param y Y offset of the circle. + */ + public function Circle(radius:int, x:int = 0, y:int = 0) + { + this.radius = radius; + _x = x + radius; + _y = y + radius; + _fakePixelmask = new Pixelmask(new BitmapData(1, 1)); + + _check[Mask] = collideMask; + _check[Hitbox] = collideHitbox; + _check[Grid] = collideGrid; + _check[Pixelmask] = collidePixelmask; + _check[Circle] = collideCircle; + } + + /** @private Collides against an Entity. */ + override protected function collideMask(other:Mask):Boolean + { + var _otherHalfWidth:Number = other.parent.width * 0.5; + var _otherHalfHeight:Number = other.parent.height * 0.5; + + var distanceX:Number = Math.abs(parent.x + _x - other.parent.x - _otherHalfWidth), + distanceY:Number = Math.abs(parent.y + _y - other.parent.y - _otherHalfHeight); + + if (distanceX > _otherHalfWidth + radius || distanceY > _otherHalfHeight + radius) + { + return false; // the hitbox/mask is too far away so return false + } + if (distanceX <= _otherHalfWidth || distanceY <= _otherHalfHeight) + { + return true; + } + var distanceToCorner:Number = (distanceX - _otherHalfWidth) * (distanceX - _otherHalfWidth) + + (distanceY - _otherHalfHeight) * (distanceY - _otherHalfHeight); + + return distanceToCorner <= _squaredRadius; + } + + /** @private Collides against a Hitbox. */ + override protected function collideHitbox(other:Hitbox):Boolean + { + var _otherHalfWidth:Number = other._width * 0.5; + var _otherHalfHeight:Number = other._height * 0.5; + + var distanceX:Number = Math.abs(parent.x + _x - other.parent.x - other._x - _otherHalfWidth), + distanceY:Number = Math.abs(parent.y + _y - other.parent.y - other._y - _otherHalfHeight); + + if (distanceX > _otherHalfWidth + radius || distanceY > _otherHalfHeight + radius) + { + return false; // the hitbox is too far away so return false + } + if (distanceX <= _otherHalfWidth || distanceY <= _otherHalfHeight) + { + return true; + } + var distanceToCorner:Number = (distanceX - _otherHalfWidth) * (distanceX - _otherHalfWidth) + + (distanceY - _otherHalfHeight) * (distanceY - _otherHalfHeight); + + return distanceToCorner <= _squaredRadius; + } + + /** @private Collides against a Grid. */ + private function collideGrid(other:Grid):Boolean + { + var thisX:Number = parent.x + _x, + thisY:Number = parent.y + _y, + otherX:Number = other.parent.x + other.x, + otherY:Number = other.parent.y + other.y, + entityDistX:Number = thisX - otherX, + entityDistY:Number = thisY - otherY; + + var minx:int = Math.floor((entityDistX - radius) / other.tileWidth), + miny:int = Math.floor((entityDistY - radius) / other.tileHeight), + maxx:int = Math.ceil((entityDistX + radius) / other.tileWidth), + maxy:int = Math.ceil((entityDistY + radius) / other.tileHeight); + + if (minx < 0) minx = 0; + if (miny < 0) miny = 0; + if (maxx > other.columns) maxx = other.columns; + if (maxy > other.rows) maxy = other.rows; + + var hTileWidth:Number = other.tileWidth * 0.5, + hTileHeight:Number = other.tileHeight * 0.5, + dx:Number, dy:Number; + + for (var xx:int = minx; xx < maxx; xx++) + { + for (var yy:int = miny; yy < maxy; yy++) + { + if (other.getTile(xx, yy)) + { + var mx:Number = otherX + xx*other.tileWidth + hTileWidth, + my:Number = otherY + yy*other.tileHeight + hTileHeight; + + dx = Math.abs(thisX - mx); + + if (dx > hTileWidth + radius) + continue; + + dy = Math.abs(thisY - my); + + if (dy > hTileHeight + radius) + continue; + + if (dx <= hTileWidth || dy <= hTileHeight) + return true; + + var xCornerDist:Number = dx - hTileWidth; + var yCornerDist:Number = dy - hTileHeight; + + if (xCornerDist * xCornerDist + yCornerDist * yCornerDist <= _squaredRadius) + return true; + } + } + } + + return false; + } + + /** + * Checks for collision with a Pixelmask. + * May be slow (especially with big polygons), added for completeness sake. + * + * Internally sets up a Pixelmask and uses that for collision check. + */ + private function collidePixelmask(other:Pixelmask):Boolean + { + var data:BitmapData = _fakePixelmask._data; + + _fakePixelmask._x = _x - _radius; + _fakePixelmask._y = _y - _radius; + _fakePixelmask.parent = parent; + + _width = _height = _radius * 2; + + if (data == null || (data.width < _width || data.height < _height)) { + data = new BitmapData(_width, height, true, 0); + } else { + data.fillRect(data.rect, 0); + } + + var graphics:Graphics = FP.sprite.graphics; + graphics.clear(); + + graphics.beginFill(0xFFFFFF, 1); + graphics.lineStyle(1, 0xFFFFFF, 1); + + graphics.drawCircle(_x + parent.originX, _y + parent.originY, _radius); + + graphics.endFill(); + + data.draw(FP.sprite); + + _fakePixelmask.data = data; + + return other.collide(_fakePixelmask); + } + + /** @private Collides against a Circle. */ + private function collideCircle(other:Circle):Boolean + { + var dx:Number = (parent.x + _x) - (other.parent.x + other._x); + var dy:Number = (parent.y + _y) - (other.parent.y + other._y); + return (dx * dx + dy * dy) < Math.pow(_radius + other._radius, 2); + } + + /** @private */ + override public function project(axis:Point, projection:Object):void + { + projection.min = -_radius; + projection.max = _radius; + } + + override public function renderDebug(graphics:Graphics):void + { + var sx:Number = FP.screen.scaleX * FP.screen.scale; + var sy:Number = FP.screen.scaleY * FP.screen.scale; + + graphics.lineStyle(1, 0xFFFFFF, 0.25); + graphics.drawCircle((parent.x + _x - FP.camera.x) * sx, (parent.y + _y - FP.camera.y) * sy, radius * sx); + } + + override public function get x():int { return _x - _radius; } + override public function get y():int { return _y - _radius; } + + /** + * Radius. + */ + public function get radius():int { return _radius; } + public function set radius(value:int):void + { + if (_radius == value) return; + _radius = value; + _squaredRadius = value * value; + height = width = _radius + _radius; + if (list != null) list.update(); + else if (parent != null) update(); + } + + /** Updates the parent's bounds for this mask. */ + override public function update():void + { + if (parent != null) + { + // update entity bounds + parent.originX = -_x + radius; + parent.originY = -_y + radius; + parent.height = parent.width = radius + radius; + + // update parent list + if (list != null) + list.update(); + } + } + + // Hitbox information. + protected var _radius:int; + protected var _squaredRadius:int; // set automatically through the setter for radius + private var _fakePixelmask:Pixelmask; // used for Pixelmask collision + } +} \ No newline at end of file diff --git a/net/flashpunk/masks/Grid.as b/net/flashpunk/masks/Grid.as index f473e74..e54d86a 100644 --- a/net/flashpunk/masks/Grid.as +++ b/net/flashpunk/masks/Grid.as @@ -196,7 +196,7 @@ public function get data():BitmapData { return _data; } /** @private Collides against an Entity. */ - private function collideMask(other:Mask):Boolean + override protected function collideMask(other:Mask):Boolean { _rect.x = other.parent.x - other.parent.originX - parent.x + parent.originX; _rect.y = other.parent.y - other.parent.originY - parent.y + parent.originY; @@ -210,7 +210,7 @@ } /** @private Collides against a Hitbox. */ - private function collideHitbox(other:Hitbox):Boolean + override protected function collideHitbox(other:Hitbox):Boolean { _rect.x = other.parent.x + other._x - parent.x - _x; _rect.y = other.parent.y + other._y - parent.y - _y; @@ -349,6 +349,7 @@ var x:int, y:int; + g.beginFill(0xFFFFFF, .15); g.lineStyle(1, 0xFFFFFF, 0.25); for (y = 0; y < _rows; y ++) @@ -361,6 +362,8 @@ } } } + + g.endFill(); } // Grid information. diff --git a/net/flashpunk/masks/Hitbox.as b/net/flashpunk/masks/Hitbox.as index 8572aae..a36c7bd 100644 --- a/net/flashpunk/masks/Hitbox.as +++ b/net/flashpunk/masks/Hitbox.as @@ -1,6 +1,7 @@ package net.flashpunk.masks { import flash.display.Graphics; + import flash.geom.Point; import net.flashpunk.*; /** @@ -28,7 +29,7 @@ } /** @private Collides against an Entity. */ - private function collideMask(other:Mask):Boolean + override protected function collideMask(other:Mask):Boolean { return parent.x + _x + _width > other.parent.x - other.parent.originX && parent.y + _y + _height > other.parent.y - other.parent.originY @@ -37,7 +38,7 @@ } /** @private Collides against a Hitbox. */ - private function collideHitbox(other:Hitbox):Boolean + protected function collideHitbox(other:Hitbox):Boolean { return parent.x + _x + _width > other.parent.x + other._x && parent.y + _y + _height > other.parent.y + other._y @@ -53,8 +54,7 @@ { if (_x == value) return; _x = value; - if (list) list.update(); - else if (parent) update(); + update(); } /** @@ -65,8 +65,7 @@ { if (_y == value) return; _y = value; - if (list) list.update(); - else if (parent) update(); + update(); } /** @@ -77,8 +76,7 @@ { if (_width == value) return; _width = value; - if (list) list.update(); - else if (parent) update(); + update(); } /** @@ -89,8 +87,7 @@ { if (_height == value) return; _height = value; - if (list) list.update(); - else if (parent) update(); + update(); } /** @public Updates the parent's bounds for this mask. */ @@ -111,15 +108,45 @@ } } - public override function renderDebug(g:Graphics):void + override public function renderDebug(g:Graphics):void + { + // draw only if we're part of a Masklist + if (!list || list.count <= 1) return; + + var sx:Number = FP.screen.scaleX * FP.screen.scale; + var sy:Number = FP.screen.scaleY * FP.screen.scale; + + g.lineStyle(1, 0xFFFFFF, 0.25); + g.drawRect((parent.x - FP.camera.x + x) * sx, (parent.y - FP.camera.y + y) * sy, width * sx, height * sy); + } + + /** @private */ + override public function project(axis:Point, projection:Object):void { - // draw only if we're part of a Masklist - if (!list) return; - - var sx:Number = FP.screen.scaleX * FP.screen.scale; - var sy:Number = FP.screen.scaleY * FP.screen.scale; - - g.drawRect((parent.x - FP.camera.x + x) * sx, (parent.y - FP.camera.y + y) * sy, width * sx, height * sy); + var px:Number = _x, + py:Number = _y, + cur:Number, + max:Number = Number.NEGATIVE_INFINITY, + min:Number = Number.POSITIVE_INFINITY; + + cur = px * axis.x + py * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = (px + _width) * axis.x + py * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = px * axis.x + (py + _height) * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + cur = (px + _width) * axis.x + (py + _height) * axis.y; + if (cur < min) min = cur; + if (cur > max) max = cur; + + projection.min = min; + projection.max = max; } // Hitbox information. diff --git a/net/flashpunk/masks/Masklist.as b/net/flashpunk/masks/Masklist.as index b962cd6..737e6a6 100644 --- a/net/flashpunk/masks/Masklist.as +++ b/net/flashpunk/masks/Masklist.as @@ -48,7 +48,7 @@ */ public function add(mask:Mask):Mask { - _masks[_count ++] = mask; + _masks[_count++] = mask; mask.list = this; mask.parent = parent; update(); @@ -70,7 +70,7 @@ { mask.list = null; mask.parent = null; - _count --; + _count--; update(); } else _temp[_temp.length] = m; @@ -90,12 +90,12 @@ _temp.length = 0; var i:int = _masks.length; index %= i; - while (i --) + while (i--) { if (i == index) { _masks[index].list = null; - _count --; + _count--; update(); } else _temp[_temp.length] = _masks[index]; @@ -134,17 +134,28 @@ override public function update():void { // find bounds of the contained masks - var t:int, l:int, r:int, b:int, h:Hitbox, i:int = _count; + var t:int, l:int, r:int, b:int, i:int = _count; l = t = int.MAX_VALUE; r = b = int.MIN_VALUE; - while (i --) + + var hitbox:Hitbox; + var poly:Polygon; + + while (i--) { - if ((h = _masks[i] as Hitbox)) + if ((poly = _masks[i] as Polygon)) + { + if (poly.minX < l) l = poly.minX; + if (poly.minY < t) t = poly.minY; + if (poly.maxX > r) r = poly.maxX; + if (poly.maxY > b) b = poly.maxY; + } + else if ((hitbox = _masks[i] as Hitbox)) { - if (h._x < l) l = h._x; - if (h._y < t) t = h._y; - if (h._x + h._width > r) r = h._x + h._width; - if (h._y + h._height > b) b = h._y + h._height; + if (hitbox._x < l) l = hitbox._x; + if (hitbox._y < t) t = hitbox._y; + if (hitbox._x + hitbox._width > r) r = hitbox._x + hitbox._width; + if (hitbox._y + hitbox._height > b) b = hitbox._y + hitbox._height; } } diff --git a/net/flashpunk/masks/Pixelmask.as b/net/flashpunk/masks/Pixelmask.as index 4c572a9..de59e7a 100644 --- a/net/flashpunk/masks/Pixelmask.as +++ b/net/flashpunk/masks/Pixelmask.as @@ -40,7 +40,7 @@ } /** @private Collide against an Entity. */ - private function collideMask(other:Mask):Boolean + override protected function collideMask(other:Mask):Boolean { _point.x = parent.x + _x; _point.y = parent.y + _y; @@ -52,7 +52,7 @@ } /** @private Collide against a Hitbox. */ - private function collideHitbox(other:Hitbox):Boolean + override protected function collideHitbox(other:Hitbox):Boolean { _point.x = parent.x + _x; _point.y = parent.y + _y; diff --git a/net/flashpunk/masks/Polygon.as b/net/flashpunk/masks/Polygon.as new file mode 100644 index 0000000..acba9d8 --- /dev/null +++ b/net/flashpunk/masks/Polygon.as @@ -0,0 +1,687 @@ +package net.flashpunk.masks +{ + + import flash.display.BitmapData; + import flash.display.Graphics; + import flash.geom.Point; + import net.flashpunk.Entity; + import net.flashpunk.FP; + import net.flashpunk.Mask; + + + /** + * Uses polygon edges to check for collisions (only works with convex polygons). + */ + public class Polygon extends Hitbox + { + /** + * Constructor. + * + * @param points An array of coordinates that define the convex polygon (must have at least 3). + * @param pivotX X pivot for rotations. + * @param pivotY Y pivot for rotations. + */ + public function Polygon(points:Vector., pivotX:Number = 0, pivotY:Number = 0) + { + if (points.length < 3) throw "The polygon needs at least 3 sides"; + _points = points; + _transformedPoints = new Vector.(); + for (var i:int = 0; i < points.length; i++) _transformedPoints[i] = points[i].clone(); + + _fakeEntity = new Entity(); + _fakeTileHitbox = new Hitbox(); + _fakePixelmask = new Pixelmask(new BitmapData(1, 1)); + + _check[Mask] = collideMask; + _check[Hitbox] = collideHitbox; + _check[Grid] = collideGrid; + _check[Pixelmask] = collidePixelmask; + _check[Circle] = collideCircle; + _check[Polygon] = collidePolygon; + + this.pivotX = pivotX; + this.pivotY = pivotY; + _angle = 0; + _lastAngle = 0; + + updateAxes(); + } + + /** + * Checks for collisions with an Entity. + */ + override protected function collideMask(other:Mask):Boolean + { + var offset:Number, + offsetX:Number = parent.x + _x - other.parent.x, + offsetY:Number = parent.y + _y - other.parent.y; + + // project on the vertical axis of the hitbox/mask + project(verticalAxis, firstProj); + other.project(verticalAxis, secondProj); + + firstProj.min += offsetY; + firstProj.max += offsetY; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + + // project on the horizontal axis of the hitbox/mask + project(horizontalAxis, firstProj); + other.project(horizontalAxis, secondProj); + + firstProj.min += offsetX; + firstProj.max += offsetX; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + + var a:Point; + + // project hitbox/mask on polygon axes + // for a collision to be present all projections must overlap + for (var i:int = 0; i < _axes.length; i++) + { + a = _axes[i]; + project(a, firstProj); + other.project(a, secondProj); + + offset = offsetX * a.x + offsetY * a.y; + firstProj.min += offset; + firstProj.max += offset; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + } + return true; + } + + /** + * Checks for collisions with a Hitbox. + */ + override protected function collideHitbox(other:Hitbox):Boolean + { + var offset:Number, + offsetX:Number = parent.x + _x - other.parent.x, + offsetY:Number = parent.y + _y - other.parent.y; + + // project on the vertical axis of the hitbox + project(verticalAxis, firstProj); + other.project(verticalAxis, secondProj); + + firstProj.min += offsetY; + firstProj.max += offsetY; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + + // project on the horizontal axis of the hitbox + project(horizontalAxis, firstProj); + other.project(horizontalAxis, secondProj); + + firstProj.min += offsetX; + firstProj.max += offsetX; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + + var a:Point; + + // project hitbox on polygon axes + // for a collision to be present all projections must overlap + for (var i:int = 0; i < _axes.length; i++) + { + a = _axes[i]; + project(a, firstProj); + other.project(a, secondProj); + + offset = offsetX * a.x + offsetY * a.y; + firstProj.min += offset; + firstProj.max += offset; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + } + return true; + } + + /** + * Checks for collisions with a Grid. + * May be slow, added for completeness sake. + * + * Internally sets up an Hitbox out of each solid Grid tile and uses that for collision check. + */ + protected function collideGrid(other:Grid):Boolean + { + var tileW:uint = other.tileWidth; + var tileH:uint = other.tileHeight; + var solidTile:Boolean; + + _fakeEntity.width = tileW; + _fakeEntity.height = tileH; + _fakeEntity.x = parent.x; + _fakeEntity.y = parent.y; + _fakeEntity.originX = other.parent.originX + other._x; + _fakeEntity.originY = other.parent.originY + other._y; + + _fakeTileHitbox._width = tileW; + _fakeTileHitbox._height = tileH; + _fakeTileHitbox.parent = _fakeEntity; + + for (var r:int = 0; r < other.rows; r++ ) { + for (var c:int = 0; c < other.columns; c++) { + _fakeEntity.x = other.parent.x + other._x + c * tileW; + _fakeEntity.y = other.parent.y + other._y + r * tileH; + solidTile = other.getTile(c, r); + + if (solidTile && collideHitbox(_fakeTileHitbox)) return true; + } + } + return false; + } + + /** + * Checks for collision with a Pixelmask. + * May be slow (especially with big polygons), added for completeness sake. + * + * Internally sets up a Pixelmask using the polygon representation and uses that for collision check. + */ + protected function collidePixelmask(other:Pixelmask):Boolean + { + var data:BitmapData = _fakePixelmask._data; + + _fakeEntity.width = parent.width; + _fakeEntity.height = parent.height; + _fakeEntity.x = parent.x - _x; + _fakeEntity.y = parent.y - _y; + _fakeEntity.originX = parent.originX; + _fakeEntity.originY = parent.originY; + + _fakePixelmask._x = _x - parent.originX; + _fakePixelmask._y = _y - parent.originY; + _fakePixelmask.parent = _fakeEntity; + + if (data == null || (data.width < parent.width || data.height < parent.height)) { + data = new BitmapData(parent.width, parent.height, true, 0); + } else { + data.fillRect(data.rect, 0); + } + + var graphics:Graphics = FP.sprite.graphics; + graphics.clear(); + + graphics.beginFill(0xFFFFFF, 1); + graphics.lineStyle(1, 0xFFFFFF, 1); + + var offsetX:Number = _x + parent.originX; + var offsetY:Number = _y + parent.originY; + + graphics.moveTo(points[_transformedPoints.length - 1].x + offsetX, _transformedPoints[_transformedPoints.length - 1].y + offsetY); + for (var i:int = 0; i < _transformedPoints.length; i++) + { + graphics.lineTo(_transformedPoints[i].x + offsetX, _transformedPoints[i].y + offsetY); + } + + graphics.endFill(); + + data.draw(FP.sprite); + + _fakePixelmask.data = data; + + return other.collide(_fakePixelmask); + } + + /** + * Checks for collision with a circle. + */ + protected function collideCircle(other:Circle):Boolean + { + var edgesCrossed:int = 0; + var p1:Point, p2:Point; + var i:int, j:int; + var nPoints:int = _transformedPoints.length; + var offsetX:Number = parent.x + _x; + var offsetY:Number = parent.y + _y; + + + // check if circle center is inside the polygon + for (i = 0, j = nPoints - 1; i < nPoints; j = i, i++) { + p1 = _transformedPoints[i]; + p2 = _transformedPoints[j]; + + var distFromCenter:Number = (p2.x - p1.x) * (other._y + other.parent.y - p1.y - offsetY) / (p2.y - p1.y) + p1.x + offsetX; + + if ((p1.y + offsetY > other._y + other.parent.y) != (p2.y + offsetY > other._y + other.parent.y) + && (other._x + other.parent.x < distFromCenter)) + { + edgesCrossed++; + } + } + + if (edgesCrossed & 1) return true; + + // check if minimum distance from circle center to each polygon side is less than radius + var radiusSqr:Number = other.radius * other.radius; + var cx:Number = other._x + other.parent.x; + var cy:Number = other._y + other.parent.y; + var minDistanceSqr:Number = 0; + var closestX:Number; + var closestY:Number; + + for (i = 0, j = nPoints - 1; i < nPoints; j = i, i++) { + p1 = _transformedPoints[i]; + p2 = _transformedPoints[j]; + + var segmentLenSqr:Number = (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y); + + // find projection of center onto line (extended segment) + var t:Number = ((cx - p1.x - offsetX) * (p2.x - p1.x) + (cy - p1.y - offsetY) * (p2.y - p1.y)) / segmentLenSqr; + + if (t < 0) { + closestX = p1.x; + closestY = p1.y; + } else if (t > 1) { + closestX = p2.x; + closestY = p2.y; + } else { + closestX = p1.x + t * (p2.x - p1.x); + closestY = p1.y + t * (p2.y - p1.y); + } + closestX += offsetX; + closestY += offsetY; + + minDistanceSqr = (cx - closestX) * (cx - closestX) + (cy - closestY) * (cy - closestY); + + if (minDistanceSqr <= radiusSqr) return true; + } + + return false; + } + + /** + * Checks for collision with a polygon. + */ + protected function collidePolygon(other:Polygon):Boolean + { + var offset:Number; + var offsetX:Number = parent.x + _x - other.parent.x - other.x; + var offsetY:Number = parent.y + _y - other.parent.y - other.y; + var a:Point; + + // project other on this polygon axes + // for a collision to be present all projections must overlap + for (var i:int = 0; i < _axes.length; i++) + { + a = _axes[i]; + project(a, firstProj); + other.project(a, secondProj); + + // shift the first info with the offset + offset = offsetX * a.x + offsetY * a.y; + firstProj.min += offset; + firstProj.max += offset; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + } + + // project this polygon on other polygon axes + // for a collision to be present all projections must overlap + for (var j:int = 0; j < other._axes.length; j++) + { + a = other._axes[j]; + project(a, firstProj); + other.project(a, secondProj); + + // shift the first info with the offset + offset = offsetX * a.x + offsetY * a.y; + firstProj.min += offset; + firstProj.max += offset; + + // if firstProj not overlaps secondProj + if (firstProj.min > secondProj.max || firstProj.max < secondProj.min) + { + return false; + } + } + return true; + } + + /** @private Projects this polygon points on axis and returns min and max values in projection object. */ + override public function project(axis:Point, projection:Object):void + { + var p:Point = _transformedPoints[0]; + + var min:Number = axis.x * p.x + axis.y * p.y, // dot product + max:Number = min; + + for (var i:int = 1; i < _transformedPoints.length; i++) + { + p = _transformedPoints[i]; + var cur:Number = axis.x * p.x + axis.y * p.y; // dot product + + if (cur < min) min = cur; + else if (cur > max) max = cur; + } + projection.min = min; + projection.max = max; + } + + override public function renderDebug(graphics:Graphics):void + { + if (parent != null) + { + var offsetX:Number = parent.x +_x - FP.camera.x, + offsetY:Number = parent.y +_y - FP.camera.y; + + var sx:Number = FP.screen.scaleX * FP.screen.scale; + var sy:Number = FP.screen.scaleY * FP.screen.scale; + + graphics.beginFill(0xFFFFFF, .15); + graphics.lineStyle(1, 0xFFFFFF, 0.25); + + graphics.moveTo((points[_transformedPoints.length - 1].x + offsetX) * sx , (_transformedPoints[_transformedPoints.length - 1].y + offsetY) * sy); + for (var i:int = 0; i < _transformedPoints.length; i++) + { + graphics.lineTo((_transformedPoints[i].x + offsetX) * sx, (_transformedPoints[i].y + offsetY) * sy); + } + + graphics.endFill(); + + // draw pivot + graphics.drawCircle((offsetX + pivotX) * sx + .5, (offsetY + pivotY) * sy + .5, 2); + } + } + + /** + * Rotation angle (in degrees) of the polygon (rotates around pivot point). + */ + public function get angle():Number { return _angle; } + public function set angle(value:Number):void + { + if (value == _angle) return; + _lastAngle = _angle; + _angle = value; + transformPoints(); + + if (list != null || parent != null) update(); + } + + /** X coord to use for rotations. Defaults to top-left corner. */ + public function get pivotX():Number { return _pivotX; } + public function set pivotX(value:Number):void + { + if (_pivotX == value) return; + _pivotX = value; + transformPoints(); + + if (list != null || parent != null) update(); + } + + /** Y coord to use for rotations. Defaults to top-left corner. */ + public function get pivotY():Number { return _pivotY; } + public function set pivotY(value:Number):void + { + if (_pivotY == value) return; + _pivotY = value; + transformPoints(); + + if (list != null || parent != null) update(); + } + + /** Leftmost X coord of the polygon. */ + public function get minX():int { return _minX; } + + /** Rightmost X coord of the polygon. */ + public function get maxX():int { return _maxX; } + + /** Topmost Y coord of the polygon. */ + public function get minY():int { return _minY; } + + /** Bottommost Y coord of the polygon. */ + public function get maxY():int { return _maxY; } + + /** + * The original points (non transformed/rotated) representing the polygon. + * + * If you need to set a point yourself instead of passing in a new Vector. you need to call update() + * to makes sure the axes update as well. + */ + public function get points():Vector. { return _transformedPoints; } + public function set points(value:Vector.):void + { + if (_transformedPoints == value) return; + _transformedPoints = value; + + if (list != null || parent != null) updateAxes(); + } + + /** + * The transformed/rotated points representing the polygon. + */ + public function get transformedPoints():Vector. { return _transformedPoints; } + + /** Updates the parent's bounds for this mask. */ + override public function update():void + { + project(horizontalAxis, firstProj); // width + var projX:int = Math.round(firstProj.min); + _width = Math.round(firstProj.max - firstProj.min); + project(verticalAxis, secondProj); // height + var projY:int = Math.round(secondProj.min); + _height = Math.round(secondProj.max - secondProj.min); + + _minX = _x + projX; + _minY = _y + projY; + _maxX = Math.round(minX + _width); + _maxY = Math.round(minY + _height); + + if (list != null) + { + // update parent list + list.update(); + } + else if (parent != null) + { + parent.originX = -_x - projX; + parent.originY = -_y - projY; + parent.width = _width; + parent.height = _height; + } + } + + /** + * Creates a regular polygon (edges of same length). + * @param sides The number of sides in the polygon + * @param radius The distance that the corners are at + * @param angle How much the polygon is rotated + * @return The polygon + */ + public static function createRegular(sides:int, radius:Number, angle:Number = 0):Polygon + { + if (sides < 3) throw "The polygon needs at least 3 sides."; + + // figure out the angle required for each step + var rotationAngle:Number = (Math.PI * 2) / sides; + + // loop through and generate each point + var points:Vector. = new Vector.(); + + var startAngle:Number = 0; + for (var i:int = 0; i < sides; i++) + { + var p:Point = new Point(); + p.x = Math.cos(startAngle) * radius + radius; + p.y = Math.sin(startAngle) * radius + radius; + points.push(p); + startAngle += rotationAngle; + } + + // return the polygon + var poly:Polygon = new Polygon(points); + poly.angle = angle; + return poly; + } + + /** + * Creates a polygon from an array were even numbers are x and odd are y + * @param points Vector containing the polygon's points. + * @param angle How much the polygon is rotated + * + * @return The polygon + */ + public static function createFromFlatVector(points:Vector., angle:Number = 0):Polygon + { + var p:Vector. = new Vector.(); + + var i:int = 0; + while (i < points.length) + { + p.push(new Point(points[i++], points[i++])); + } + var poly:Polygon = new Polygon(p); + poly.angle = angle; + return poly; + } + + private function transformPoints():void + { + var p:Point; + var tp:Point; + var angleRad:Number = _angle * FP.RAD; + var lastAngleRad:Number = _lastAngle * FP.RAD; + + for (var i:int = 0; i < _points.length; i++) + { + p = _points[i]; + tp = _transformedPoints[i]; + var dx:Number = p.x - _pivotX; + var dy:Number = p.y - _pivotY; + + var pointAngle:Number = Math.atan2(dy, dx); + var length:Number = Math.sqrt(dx * dx + dy * dy); + + tp.x = Math.cos(pointAngle + angleRad) * length + _pivotX; + tp.y = Math.sin(pointAngle + angleRad) * length + _pivotY; + } + var a:Point; + + for (var j:int = 0; j < _axes.length; j++) + { + a = _axes[j]; + + var axisAngle:Number = Math.atan2(a.y, a.x); + + a.x = Math.cos(axisAngle + angleRad - lastAngleRad); + a.y = Math.sin(axisAngle + angleRad - lastAngleRad); + } + } + + private function generateAxes():void + { + _axes = new Vector.(); + var temp:Number; + var nPoints:int = _transformedPoints.length; + var edge:Point; + var i:int, j:int; + + for (i = 0, j = nPoints - 1; i < nPoints; j = i, i++) { + edge = new Point(); + edge.x = _transformedPoints[i].x - _transformedPoints[j].x; + edge.y = _transformedPoints[i].y - _transformedPoints[j].y; + + // get the axis which is perpendicular to the edge + temp = edge.y; + edge.y = -edge.x; + edge.x = temp; + edge.normalize(1); + + _axes.push(edge); + } + } + + private function removeDuplicateAxes():void + { + var i:int = _axes.length - 1; + var j:int = i - 1; + while (i > 0) + { + // if the first vector is equal or similar to the second vector, + // remove it from the list. (for example, [1, 1] and [-1, -1] + // represent the same axis) + if ((Math.abs(_axes[i].x - _axes[j].x) < EPSILON && Math.abs(_axes[i].y - _axes[j].y) < EPSILON) + || (Math.abs(_axes[j].x + _axes[i].x) < EPSILON && Math.abs(_axes[i].y + _axes[j].y) < EPSILON)) // first axis inverted + { + _axes.splice(i, 1); + i--; + } + + j--; + if (j < 0) + { + i--; + j = i - 1; + } + } + } + + private function updateAxes():void + { + generateAxes(); + removeDuplicateAxes(); + update(); + } + + // Hitbox information. + private var _angle:Number; + private var _lastAngle:Number; + private var _points:Vector.; // original points (non transformed/rotated) as passed in the constructor + private var _transformedPoints:Vector.; // transformed/rotated points + private var _axes:Vector.; + private var _projection:* = { min: 0.0, max:0.0 }; + + // Polygon pivot point. + private var _pivotX:Number = 0; + private var _pivotY:Number = 0; + + // Polygon bounding box. + private var _minX:int = 0; + private var _minY:int = 0; + private var _maxX:int = 0; + private var _maxY:int = 0; + + private var _fakeEntity:Entity; // used for Grid and Pixelmask collision + private var _fakeTileHitbox:Hitbox; // used for Grid collision + private var _fakePixelmask:Pixelmask; // used for Pixelmask collision + + private static var EPSILON:Number = 0.000000001; // used for axes comparison in removeDuplicateAxes + + private static var _axis:Point = new Point(); + private static var firstProj:* = { min: 0.0, max:0.0 }; + private static var secondProj:* = { min: 0.0, max:0.0 }; + + public static const verticalAxis:Point = new Point(0, 1); + public static const horizontalAxis:Point = new Point(1, 0); + } +} \ No newline at end of file