diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index baad5bdbb7..6cc06ea030 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -1,5 +1,6 @@ package flixel; +import flixel.group.FlxGroup; import flixel.math.FlxMath; import flixel.math.FlxRandom; import flixel.math.FlxRect; @@ -9,6 +10,7 @@ import flixel.system.frontEnds.AssetFrontEnd; import flixel.system.frontEnds.BitmapFrontEnd; import flixel.system.frontEnds.BitmapLogFrontEnd; import flixel.system.frontEnds.CameraFrontEnd; +import flixel.system.frontEnds.CollisionFrontEnd; import flixel.system.frontEnds.ConsoleFrontEnd; import flixel.system.frontEnds.DebuggerFrontEnd; import flixel.system.frontEnds.InputFrontEnd; @@ -334,6 +336,12 @@ class FlxG * @since 5.9.0 */ public static var assets(default, null):AssetFrontEnd = new AssetFrontEnd(); + + /** + * Contains helper functions relating to collision + * @since 6.2.0 + */ + public static var collision(default, null):CollisionFrontEnd = new CollisionFrontEnd(); /** * Resizes the game within the window by reapplying the current scale mode. @@ -408,31 +416,72 @@ class FlxG * * @param objectOrGroup1 The first object or group you want to check. * @param objectOrGroup2 The second object or group you want to check. If it is the same as the first, - * Flixel knows to just do a comparison within that group. - * @param notifyCallback A function with two `FlxObject` parameters - + * @param notifier A function with two `FlxObject` parameters - * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - * that is called if those two objects overlap. - * @param processCallback A function with two `FlxObject` parameters - + * @param processer A function with two `FlxObject` parameters - * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - * that is called if those two objects overlap. - * If a `ProcessCallback` is provided, then `NotifyCallback` - * will only be called if `ProcessCallback` returns true for those objects! + * If a `processor` is provided, then `notifier` + * will only be called if `processer` returns true for those objects! + * @return Whether any overlaps were detected. + */ + overload public static inline extern function overlap(objectOrGroup1:FlxBasic, objectOrGroup2:FlxBasic, ?notifier, ?processser):Bool + { + return overlapHelper(objectOrGroup1, objectOrGroup2, notifier, processser); + } + + /** + * Checks if any `FlxObject` in the group overlaps another within `FlxG.worldBounds`. + * + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * NOTE: this takes the entire area of `FlxTilemap`s into account (including "empty" tiles). + * Use `FlxTilemap#overlaps()` if you don't want that. + * + * @param group The group of objects you want to check + * @param notifier A function with two `FlxObject` parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * @param processer A function with two `FlxObject` parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * If a `processor` is provided, then `notifier` + * will only be called if `processer` returns true for those objects! + * @return Whether any overlaps were detected. + */ + overload public static inline extern function overlap(group:FlxGroup, ?notifier, ?processser):Bool + { + return overlapHelper(group, null, notifier, processser); + } + + /** + * Checks if any `FlxObject` in `FlxG.state` overlaps another within `FlxG.worldBounds`. + * + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * NOTE: this takes the entire area of `FlxTilemap`s into account (including "empty" tiles). + * Use `FlxTilemap#overlaps()` if you don't want that. + * + * @param notifier A function with two object parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * @param processer A function with two `FlxObject` parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * If a `processor` is provided, then `notifier` + * will only be called if `processer` returns true for those objects! * @return Whether any overlaps were detected. */ - public static function overlap(?objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifyCallback:Dynamic->Dynamic->Void, - ?processCallback:Dynamic->Dynamic->Bool):Bool + overload public static inline extern function overlap(?notifier, ?processser):Bool + { + return overlapHelper(state, null, notifier, processser); + } + + static function overlapHelper(objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifier:Dynamic->Dynamic->Void, + ?processser:Dynamic->Dynamic->Bool):Bool { - if (objectOrGroup1 == null) - objectOrGroup1 = state; - if (objectOrGroup2 == objectOrGroup1) - objectOrGroup2 = null; - - FlxQuadTree.divisions = worldDivisions; - final quadTree = FlxQuadTree.recycle(worldBounds.x, worldBounds.y, worldBounds.width, worldBounds.height); - quadTree.load(objectOrGroup1, objectOrGroup2, notifyCallback, processCallback); - final result:Bool = quadTree.execute(); - quadTree.destroy(); - return result; + return FlxQuadTree.executeOnce(worldBounds, worldDivisions, objectOrGroup1, objectOrGroup2, notifier, processser); } /** @@ -460,21 +509,62 @@ class FlxG * whatever floats your boat! For maximum performance try bundling a lot of objects * together using a FlxGroup (or even bundling groups together!). * - * This function just calls `FlxG.overlap` and presets the `ProcessCallback` parameter to `FlxObject.separate`. - * To create your own collision logic, write your own `ProcessCallback` and use `FlxG.overlap` to set it up. + * This function just calls `FlxG.overlap` and presets the `processer` parameter to `FlxObject.separate`. + * To create your own collision logic, write your own `processer` and use `FlxG.overlap` to set it up. + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * @param objectOrGroup1 The first object or group you want to check. + * @param objectOrGroup2 The second object or group you want to check. If it is the same as the first, + * Flixel knows to just do a comparison within that group. + * @param notifier A function with two `FlxObject` parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * @return Whether any objects were successfully collided/separated. + */ + overload public static inline extern function collide(objectOrGroup1:FlxBasic, objectOrGroup2:FlxBasic, ?notifier):Bool + { + return overlap(objectOrGroup1, objectOrGroup2, notifier, FlxObject.separate); + } + + /** + * Checks if any `FlxObject` in the group collides another within `FlxG.worldBounds` and separates any collisions. + * + * This function just calls `FlxG.overlap` and presets the `processer` parameter to `FlxObject.separate`. + * To create your own collision logic, write your own `processer` and use `FlxG.overlap` to set it up. * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. * * @param objectOrGroup1 The first object or group you want to check. * @param objectOrGroup2 The second object or group you want to check. If it is the same as the first, * Flixel knows to just do a comparison within that group. - * @param notifyCallback A function with two `FlxObject` parameters - + * @param notifier A function with two `FlxObject` parameters - * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - * that is called if those two objects overlap. * @return Whether any objects were successfully collided/separated. */ - public static inline function collide(?objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifyCallback:Dynamic->Dynamic->Void):Bool + overload public static inline extern function collide(group:FlxTypedGroup, ?notifier):Bool + { + return overlap(group, notifier, FlxObject.separate); + } + + /** + * Call this function to see if one `FlxObject` collides with another within `FlxG.worldBounds`. + * Can be called with one object and one group, or two groups, or two objects, + * whatever floats your boat! For maximum performance try bundling a lot of objects + * together using a FlxGroup (or even bundling groups together!). + * + * This function just calls `FlxG.overlap` and presets the `processer` parameter to `FlxObject.separate`. + * To create your own collision logic, write your own `processer` and use `FlxG.overlap` to set it up. + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * Flixel knows to just do a comparison within that group. + * @param notifier A function with two `FlxObject` parameters - + * e.g. `onOverlap(object1:FlxObject, object2:FlxObject)` - + * that is called if those two objects overlap. + * @return Whether any objects were successfully collided/separated. + */ + overload public static inline extern function collide(?notifier:(Dynamic, Dynamic)->Void):Bool { - return overlap(objectOrGroup1, objectOrGroup2, notifyCallback, FlxObject.separate); + return overlap(notifier, FlxObject.separate); } /** @@ -483,7 +573,7 @@ class FlxG * * @param child The `DisplayObject` to add * @param indexModifier Amount to add to the index - makes sure the index stays within bounds. - * @return The added `DisplayObject` + * @return The added `DisplayObject`) */ public static function addChildBelowMouse(child:T, indexModifier = 0):T { diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index 756d23a498..b72b234cac 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -1,10 +1,13 @@ package flixel; import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxGroup; +import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.math.FlxVelocity; import flixel.path.FlxPath; +import flixel.physics.FlxCollider; import flixel.tile.FlxBaseTilemap; import flixel.util.FlxAxes; import flixel.util.FlxColor; @@ -72,7 +75,7 @@ import openfl.display.Graphics; * - [Demos - Collision and Grouping](https://haxeflixel.com/demos/CollisionAndGrouping/) * @see [Demos - EZPlatformer](https://haxeflixel.com/demos/EZPlatformer/) */ -class FlxObject extends FlxBasic +class FlxObject extends FlxBasic implements IFlxCollider { /** * Default value for `FlxObject`'s `pixelPerfectPosition` var. @@ -93,7 +96,7 @@ class FlxObject extends FlxBasic */ public static var defaultMoves:Bool = true; - static function allowCollisionDrag(type:CollisionDragType, object1:FlxObject, object2:FlxObject):Bool + static function allowCollisionDrag(type:FlxCollisionDragType, object1:FlxObject, object2:FlxObject):Bool { return object2.active && object2.moves && switch (type) { @@ -552,23 +555,25 @@ class FlxObject extends FlxBasic /** * X position of the upper left corner of this object in world space. */ - public var x(default, set):Float = 0; + @:isVar // keep var for reflection + public var x(get, set):Float = 0; /** * Y position of the upper left corner of this object in world space. */ - public var y(default, set):Float = 0; + @:isVar // keep var for reflection + public var y(get, set):Float = 0; /** * The width of this object's hitbox. For sprites, use `offset` to control the hitbox position. */ - @:isVar + @:isVar // keep var for reflection public var width(get, set):Float; /** * The height of this object's hitbox. For sprites, use `offset` to control the hitbox position. */ - @:isVar + @:isVar // keep var for reflection public var height(get, set):Float; /** @@ -594,12 +599,14 @@ class FlxObject extends FlxBasic * Set this to `false` if you want to skip the automatic motion/movement stuff (see `updateMotion()`). * `FlxObject` and `FlxSprite` default to `true`. `FlxText`, `FlxTileblock` and `FlxTilemap` default to `false`. */ - public var moves(default, set):Bool = defaultMoves; + @:isVar // keep var for reflection + public var moves(get, set):Bool; /** * Whether an object will move/alter position after a collision. */ - public var immovable(default, set):Bool = false; + @:isVar // keep var for reflection + public var immovable(get, set):Bool = false; /** * Whether the object collides or not. For more control over what directions the object will collide from, @@ -647,12 +654,14 @@ class FlxObject extends FlxBasic * The virtual mass of the object. Default value is 1. Currently only used with elasticity * during collision resolution. Change at your own risk; effects seem crazy unpredictable so far! */ - public var mass:Float = 1; + @:isVar // keep var for reflection + public var mass(get, set):Float = 1; /** * The bounciness of this object. Only affects collisions. Default value is 0, or "not bouncy at all." */ - public var elasticity:Float = 0; + @:isVar // keep var for reflection + public var elasticity(get, set):Float = 0; /** * This is how fast you want this sprite to spin (in degrees per second). @@ -688,19 +697,22 @@ class FlxObject extends FlxBasic * Bit field of flags (use with UP, DOWN, LEFT, RIGHT, etc) indicating surface contacts. Use bitwise operators to check the values * stored here, or use isTouching(), justTouched(), etc. You can even use them broadly as boolean values if you're feeling saucy! */ - public var touching = FlxDirectionFlags.NONE; + @:isVar // keep var for reflection + public var touching(get, set) = FlxDirectionFlags.NONE; /** * Bit field of flags (use with UP, DOWN, LEFT, RIGHT, etc) indicating surface contacts from the previous game loop step. Use bitwise operators to check the values * stored here, or use isTouching(), justTouched(), etc. You can even use them broadly as boolean values if you're feeling saucy! */ - public var wasTouching = FlxDirectionFlags.NONE; + @:isVar // keep var for reflection + public var wasTouching(get, set) = FlxDirectionFlags.NONE; /** * Bit field of flags (use with UP, DOWN, LEFT, RIGHT, etc) indicating collision directions. Use bitwise operators to check the values stored here. * Useful for things like one-way platforms (e.g. allowCollisions = UP;). The accessor "solid" just flips this variable between NONE and ANY. */ - public var allowCollisions(default, set) = FlxDirectionFlags.ANY; + @:isVar // keep var for reflection + public var allowCollisions(get, set) = FlxDirectionFlags.ANY; /** * Whether this sprite is dragged along with the horizontal movement of objects it collides with @@ -708,7 +720,8 @@ class FlxObject extends FlxBasic * IMMOVABLE, ALWAYS, HEAVIER or NEVER * @since 4.11.0 */ - public var collisionXDrag:CollisionDragType = IMMOVABLE; + @:isVar // keep var for reflection + public var collisionXDrag(get, set) = FlxCollisionDragType.IMMOVABLE; /** * Whether this sprite is dragged along with the vertical movement of objects it collides with @@ -716,7 +729,8 @@ class FlxObject extends FlxBasic * IMMOVABLE, ALWAYS, HEAVIER or NEVER * @since 4.11.0 */ - public var collisionYDrag:CollisionDragType = NEVER; + @:isVar // keep var for reflection + public var collisionYDrag(get, set) = FlxCollisionDragType.NEVER; #if FLX_DEBUG /** @@ -759,6 +773,14 @@ class FlxObject extends FlxBasic * See `flixel.util.FlxPath` for more info and usage examples. */ public var path(default, set):FlxPath = null; + + var collider:FlxCollider; + + /** For `IFlxCollider` */ + public inline function getCollider() + { + return collider; + } @:noCompletion var _point:FlxPoint = FlxPoint.get(); @@ -773,6 +795,8 @@ class FlxObject extends FlxBasic */ public function new(x:Float = 0, y:Float = 0, width:Float = 0, height:Float = 0) { + collider = new FlxCollider(); + moves = defaultMoves; super(); this.x = x; @@ -790,7 +814,7 @@ class FlxObject extends FlxBasic function initVars():Void { flixelType = OBJECT; - last = FlxPoint.get(x, y); + last = collider.last; scrollFactor = FlxPoint.get(1, 1); pixelPerfectPosition = FlxObject.defaultPixelPerfectPosition; @@ -803,8 +827,8 @@ class FlxObject extends FlxBasic @:noCompletion inline function initMotionVars():Void { - velocity = FlxPoint.get(); - acceleration = FlxPoint.get(); + velocity = collider.velocity; + acceleration = collider.acceleration; drag = FlxPoint.get(); maxVelocity = FlxPoint.get(10000, 10000); } @@ -823,12 +847,12 @@ class FlxObject extends FlxBasic { super.destroy(); - velocity = FlxDestroyUtil.put(velocity); - acceleration = FlxDestroyUtil.put(acceleration); + velocity = null; + acceleration = null; drag = FlxDestroyUtil.put(drag); maxVelocity = FlxDestroyUtil.put(maxVelocity); + last = null; scrollFactor = FlxDestroyUtil.put(scrollFactor); - last = FlxDestroyUtil.put(last); _point = FlxDestroyUtil.put(_point); _rect = FlxDestroyUtil.put(_rect); } @@ -868,18 +892,16 @@ class FlxObject extends FlxBasic angularVelocity += velocityDelta; angle += angularVelocity * elapsed; angularVelocity += velocityDelta; - - velocityDelta = 0.5 * (FlxVelocity.computeVelocity(velocity.x, acceleration.x, drag.x, maxVelocity.x, elapsed) - velocity.x); - velocity.x += velocityDelta; - var delta = velocity.x * elapsed; - velocity.x += velocityDelta; - x += delta; - - velocityDelta = 0.5 * (FlxVelocity.computeVelocity(velocity.y, acceleration.y, drag.y, maxVelocity.y, elapsed) - velocity.y); - velocity.y += velocityDelta; - delta = velocity.y * elapsed; - velocity.y += velocityDelta; - y += delta; + + // if (collider.drag == NONE && !drag.isZero()) + collider.drag = ORTHO(drag.x, drag.y); + + // if (collider.maxVelocity == NONE && !maxVelocity.isZero()) + collider.maxVelocity = ORTHO(maxVelocity.x, maxVelocity.y); + + collider.update(elapsed); + @:bypassAccessor x = collider.bounds.x; + @:bypassAccessor y = collider.bounds.y; } /** @@ -913,7 +935,7 @@ class FlxObject extends FlxBasic { return group.any(overlapsCallback.bind(_, 0, 0, inScreenSpace, camera)); } - + if (objectOrGroup.flixelType == TILEMAP) { // Since tilemap's have to be the caller, not the target, to do proper tile-based collisions, @@ -921,13 +943,13 @@ class FlxObject extends FlxBasic var tilemap:FlxBaseTilemap = cast objectOrGroup; return tilemap.overlaps(this, inScreenSpace, camera); } - - var object:FlxObject = cast objectOrGroup; + + final object:FlxObject = cast objectOrGroup; if (!inScreenSpace) { return (object.x + object.width > x) && (object.x < x + width) && (object.y + object.height > y) && (object.y < y + height); } - + if (camera == null) camera = getDefaultCamera(); @@ -938,7 +960,7 @@ class FlxObject extends FlxBasic && (objectScreenPos.y + object.height > _point.y) && (objectScreenPos.y < _point.y + height); } - + @:noCompletion inline function overlapsCallback(objectOrGroup:FlxBasic, x:Float, y:Float, inScreenSpace:Bool, camera:FlxCamera):Bool { @@ -1357,17 +1379,29 @@ class FlxObject extends FlxBasic LabelValuePair.weak("velocity", velocity) ]); } - + + @:noCompletion + inline function get_x():Float + { + return collider.x; + } + @:noCompletion function set_x(value:Float):Float { - return x = value; + return x = collider.bounds.x = value; } - + + @:noCompletion + inline function get_y():Float + { + return collider.y; + } + @:noCompletion function set_y(value:Float):Float { - return y = value; + return y = collider.bounds.y = value; } @:noCompletion @@ -1381,7 +1415,7 @@ class FlxObject extends FlxBasic } #end - return width = value; + return width = collider.bounds.width = value; } @:noCompletion @@ -1395,19 +1429,19 @@ class FlxObject extends FlxBasic } #end - return height = value; + return height = collider.bounds.height = value; } @:noCompletion function get_width():Float { - return width; + return collider.bounds.width; } @:noCompletion function get_height():Float { - return height; + return collider.bounds.height; } @:noCompletion @@ -1429,16 +1463,28 @@ class FlxObject extends FlxBasic return angle = value; } + @:noCompletion + inline function get_moves():Bool + { + return collider.moves; + } + @:noCompletion function set_moves(value:Bool):Bool { - return moves = value; + return moves = collider.moves = value; } + @:noCompletion + inline function get_immovable():Bool + { + return collider.immovable; + } + @:noCompletion function set_immovable(value:Bool):Bool { - return immovable = value; + return immovable = collider.immovable = value; } @:noCompletion @@ -1446,13 +1492,91 @@ class FlxObject extends FlxBasic { return pixelPerfectRender = value; } - + + @:noCompletion + inline function get_touching():FlxDirectionFlags + { + return collider.touching; + } + + @:noCompletion + inline function set_touching(value:FlxDirectionFlags):FlxDirectionFlags + { + return touching = collider.touching = value; + } + + @:noCompletion + inline function get_wasTouching():FlxDirectionFlags + { + return collider.touching; + } + + @:noCompletion + inline function set_wasTouching(value:FlxDirectionFlags):FlxDirectionFlags + { + return wasTouching = collider.wasTouching = value; + } + + @:noCompletion + inline function get_allowCollisions():FlxDirectionFlags + { + return collider.allowCollisions; + } + @:noCompletion function set_allowCollisions(value:FlxDirectionFlags):FlxDirectionFlags { - return allowCollisions = value; + return allowCollisions = collider.allowCollisions = value; } - + + @:noCompletion + inline function get_collisionXDrag():FlxCollisionDragType + { + return collider.collisionXDrag; + } + + @:noCompletion + inline function set_collisionXDrag(value:FlxCollisionDragType):FlxCollisionDragType + { + return collisionXDrag = collider.collisionXDrag = value; + } + + @:noCompletion + inline function get_collisionYDrag():FlxCollisionDragType + { + return collider.collisionYDrag; + } + + @:noCompletion + inline function set_collisionYDrag(value:FlxCollisionDragType):FlxCollisionDragType + { + return collisionYDrag = collider.collisionYDrag = value; + } + + @:noCompletion + inline function get_mass():Float + { + return collider.mass; + } + + @:noCompletion + inline function set_mass(value:Float):Float + { + return mass = collider.mass = value; + } + + @:noCompletion + inline function get_elasticity():Float + { + return collider.elasticity; + } + + @:noCompletion + inline function set_elasticity(value:Float):Float + { + return elasticity = collider.elasticity = value; + } + #if FLX_DEBUG @:noCompletion function set_debugBoundingBoxColorSolid(color:FlxColor) @@ -1488,20 +1612,5 @@ class FlxObject extends FlxBasic } } -/** - * Determines when to apply collision drag to one object that collided with another. - */ -enum abstract CollisionDragType(Int) -{ - /** Never drags on colliding objects. */ - var NEVER = 0; - - /** Always drags on colliding objects. */ - var ALWAYS = 1; - - /** Drags when colliding with immovable objects. */ - var IMMOVABLE = 2; - - /** Drags when colliding with heavier objects. Immovable objects have infinite mass. */ - var HEAVIER = 3; -} +@:deprecated("CollisionDragType is deprecated, use flixel.physics.FlxCollider.FlxCollisionDragType, instead") +typedef CollisionDragType = flixel.physics.FlxCollider.FlxCollisionDragType; \ No newline at end of file diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index cd4704bfff..1eb9f48515 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1731,13 +1731,13 @@ class FlxSprite extends FlxObject interface IFlxSprite extends IFlxBasic { - var x(default, set):Float; - var y(default, set):Float; + var x(get, set):Float; + var y(get, set):Float; var alpha(default, set):Float; var angle(default, set):Float; var facing(default, set):FlxDirectionFlags; - var moves(default, set):Bool; - var immovable(default, set):Bool; + var moves(get, set):Bool; + var immovable(get, set):Bool; var offset(default, null):FlxPoint; var origin(default, null):FlxPoint; diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 8c973a377f..6ef5cc1f42 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -285,6 +285,32 @@ class FlxRect implements IFlxPooled return result; } + /** + * Checks to see if this rectangle overlaps another in the X axis + * + * @param rect The other rectangle + */ + public inline function overlapsX(rect:FlxRect):Bool + { + final result = rect.right > left + && rect.left < right; + rect.putWeak(); + return result; + } + + /** + * Checks to see if this rectangle overlaps another in the Y axis + * + * @param rect The other rectangle + */ + public inline function overlapsY(rect:FlxRect):Bool + { + final result = rect.bottom > top + && rect.top < bottom; + rect.putWeak(); + return result; + } + /** * Checks to see if this rectangle fully contains another * @@ -301,7 +327,7 @@ class FlxRect implements IFlxPooled rect.putWeak(); return result; } - + /** * Returns true if this FlxRect contains the FlxPoint * @@ -534,6 +560,41 @@ class FlxRect implements IFlxPooled return result.set(x0, y0, x1 - x0, y1 - y0); } + /** + * How much this rectangle overlaps the other in the X axis + */ + public inline function intersectionX(rect:FlxRect):Float + { + final result = intersectionXHelper(rect); + rect.putWeak(); + return result; + } + + inline function intersectionXHelper(rect:FlxRect):Float + { + final l = x < rect.x ? rect.x : x; + final r = right > rect.right ? rect.right : right; + return r <= l ? 0 : r - l; + } + + + /** + * How much this rectangle overlaps the other in the Y axis + */ + public inline function intersectionY(rect:FlxRect):Float + { + final result = intersectionYHelper(rect); + rect.putWeak(); + return result; + } + + inline function intersectionYHelper(rect:FlxRect):Float + { + final t = y < rect.y ? rect.y : y; + final b = bottom > rect.bottom ? rect.bottom : bottom; + return b <= t ? 0 : b - t; + } + /** * Resizes `this` instance so that it fits within the intersection of the this and * the target rect. If there is no overlap between them, The result is an empty rect. diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx new file mode 100644 index 0000000000..3b2168274b --- /dev/null +++ b/flixel/physics/FlxCollider.hx @@ -0,0 +1,687 @@ +package flixel.physics; + +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.tile.FlxBaseTilemap; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDirectionFlags; +import flixel.util.FlxSignal; + +typedef ProcessCallback = (IFlxCollider, IFlxCollider)->Bool; +typedef Processer = (a:IFlxCollider, b:IFlxCollider, callback:ProcessCallback)->Bool; +typedef OverlapComputer = (a:IFlxCollider, b:IFlxCollider, result:FlxPoint)->FlxPoint; + +enum FlxColliderShape +{ + /** Axis-aligned bounding box, a rectangle that cannot rotate. The default shape */ + AABB; + + // CIRCLE; // coming soon + + /** + * A custom shape that can compute it's own overlap. + * + * @param name This shape's identifier, useful for resolving two custom shapes + * @param computeOverlap A function that takes two colliders and determines how much + * they overlap, and in which direction + */ + CUSTOM(name:String, computeOverlap:(collider:IFlxCollider, ?result:FlxPoint)->FlxPoint); +} + +enum FlxColliderType +{ + /** Colliders that contain a single shape */ + SHAPE(shape:FlxColliderShape); + + /** Special type that processes colliding objects against nearby tiles */ + TILEMAP; + + /** + * A type consisting of multiple "child" colliders, where, unlike groups, the colliders are not + * individually added to the quad tree, but checked against the parent's bounds before using + * the given `processer` to check each child + * + * @param processer A function called on any child colliders whos bounds overlap the given collider + */ + MULTI(processer:(collider:IFlxCollider, func:(IFlxCollider)->Bool)->Bool); +} + +class FlxCollider +{ + /** The axis-aligned world bounds of this collider */ + public var bounds(default, null):FlxRect = FlxRect.get(); + + /** The world position of this collider prior to this frame's update */ + public var last(default, null):FlxPoint = FlxPoint.get(); + + public var velocity(default, null):FlxPoint = FlxPoint.get(); + + public var acceleration(default, null):FlxPoint = FlxPoint.get(); + + public var dragMode:FlxDragMode = INERTIAL; + + public var drag:FlxForceType = NONE; + + public var maxVelocity:FlxForceType = NONE; + + public var allowCollisions = FlxDirectionFlags.ANY; + + public var touching = FlxDirectionFlags.NONE; + + public var wasTouching = FlxDirectionFlags.NONE; + + /** Whether the collider can be moved by other colliders */ + public var immovable = false; + + public var mass = 1.0; + + /** The amount of momentum conserved after a collision, defaults to `0` meaning, collisions will stop the collider */ + public var elasticity = 0.0; + + public var onBoundsCollide = new FlxTypedSignal<(collider:IFlxCollider)->Void>(); + public var onCollide = new FlxTypedSignal<(collider:IFlxCollider, overlap:FlxPoint)->Void>(); + public var onSeparate = new FlxTypedSignal<(collider:IFlxCollider, overlap:FlxPoint)->Void>(); + + /** + * Whether this sprite is dragged along with the horizontal movement of objects it collides with + * (makes sense for horizontally-moving platforms in platformers for example). Use values + * IMMOVABLE, ALWAYS, HEAVIER or NEVER + */ + public var collisionXDrag:FlxCollisionDragType = IMMOVABLE; + + /** + * Whether this sprite is dragged along with the vertical movement of objects it collides with + * (for sticking to vertically-moving platforms in platformers for example). Use values + * IMMOVABLE, ALWAYS, HEAVIER or NEVER + */ + public var collisionYDrag:FlxCollisionDragType = NEVER; + + public var x(get, set):Float; + + public var y(get, set):Float; + + public var width(get, set):Float; + + public var height(get, set):Float; + + public var centerX(get, never):Float; + + public var centerY(get, never):Float; + + public var left(get, never):Float; + + public var right(get, never):Float; + + public var top(get, never):Float; + + public var bottom(get, never):Float; + + /** The distance this collider has moved this frame */ + public var deltaX(get, never):Float; + + /** The distance this collider has moved this frame */ + public var deltaY(get, never):Float; + + /** Whether this collider will update its position, velocity and acceleration */ + public var moves:Bool; + + public var type:FlxColliderType; + + /** + * Creates a new collider + * + * @param type Defaults to `SHAPE(AABB)` + */ + public function new (?type:FlxColliderType) + { + if (type == null) + type = SHAPE(AABB); + this.type = type; + } + + public function destroy() + { + bounds = FlxDestroyUtil.destroy(bounds); + last = FlxDestroyUtil.destroy(last); + velocity = FlxDestroyUtil.destroy(velocity); + acceleration = FlxDestroyUtil.destroy(acceleration); + onCollide.removeAll(); + onSeparate.removeAll(); + } + + public function update(elapsed:Float) + { + last.set(x, y); + if (moves) + { + final lastVelocityX = velocity.x; + final lastVelocityY = velocity.y; + velocity.x += acceleration.x * elapsed; + velocity.y += acceleration.y * elapsed; + + applyDrag(elapsed); + constrainVelocity(); + + x += (lastVelocityX + 0.5 * (velocity.x - lastVelocityX)) * elapsed; + y += (lastVelocityY + 0.5 * (velocity.y - lastVelocityY)) * elapsed; + } + } + + function applyDrag(elapsed:Float) + { + switch drag + { + case NONE: + case ORTHO(dragX, dragY): + + if (dragX > 0) + velocity.x = FlxColliderUtil.applyDrag1D(velocity.x, acceleration.x, dragX * elapsed, dragMode); + + if (dragY > 0) + velocity.y = FlxColliderUtil.applyDrag1D(velocity.y, acceleration.y, dragY * elapsed, dragMode); + + case LINEAR(drag): + + final apply = switch dragMode + { + case ALWAYS: true; + case INERTIAL: acceleration.isZero(); + case SKID: velocity.dot(acceleration) < 0; + } + + if (apply) + { + final speed = velocity.length; + final frameDrag = FlxColliderUtil.getDrag1D(speed, drag) * elapsed; + velocity.length = speed + drag; + } + } + } + + function constrainVelocity() + { + switch maxVelocity + { + case NONE: + case ORTHO(maxX, maxY): + if (maxX > 0) + { + if (velocity.x > maxX) + velocity.x = maxX; + else if (velocity.x < -maxX) + velocity.x = -maxX; + } + + if (maxY > 0) + { + if (velocity.y > maxY) + velocity.y = maxY; + else if (velocity.y < -maxY) + velocity.y = -maxY; + } + case LINEAR(max): + + final speed = velocity.length; + if (speed > max) + velocity.scale(max / speed, max / speed); + } + } + + /** + * The smallest rect that contains the object in it's current and last position + * + * @param rect Optional point to store the result, if `null` one is created + * @since 6.2.0 + */ + public function getDeltaRect(?rect:FlxRect) + { + if (rect == null) + rect = FlxRect.get(); + + rect.x = bounds.x > last.x ? last.x : bounds.x; + rect.right = (bounds.x > last.x ? bounds.x : last.x) + bounds.width; + rect.y = bounds.y > last.y ? last.y : bounds.y; + rect.bottom = (bounds.y > last.y ? bounds.y : last.y) + bounds.height; + + return rect; + } + + inline function get_x():Float { return bounds.x; } + inline function get_y():Float { return bounds.y; } + inline function get_width():Float { return bounds.width; } + inline function get_height():Float { return bounds.height; } + + inline function set_x(value:Float):Float { return bounds.x = value; } + inline function set_y(value:Float):Float { return bounds.y = value; } + inline function set_width(value:Float):Float { return bounds.width = value; } + inline function set_height(value:Float):Float { return bounds.height = value; } + + inline function get_centerX():Float { return bounds.x + bounds.width * 0.5; } + inline function get_centerY():Float { return bounds.y + bounds.height * 0.5; } + inline function get_left():Float { return bounds.left; } + inline function get_right():Float { return bounds.right; } + inline function get_top():Float { return bounds.top; } + inline function get_bottom():Float { return bounds.bottom; } + + inline function get_deltaX():Float { return x - last.x; } + inline function get_deltaY():Float { return y - last.y; } +} + +class FlxColliderUtil +{ + /** + * A tween-like function that takes a starting velocity and some other factors and returns an altered velocity. + * + * @param velocity The x or y component of the starting speed + * @param acceleration The rate at which the velocity is changing. + * @param drag The deceleration of the object + * @param max An absolute value cap for the velocity (0 for no cap). + * @param elapsed The amount of time passed in to the latest update cycle + * @return The altered velocity value. + */ + public static function applyDrag1D(velocity:Float, acceleration:Float, drag:Float, mode:FlxDragMode):Float + { + final apply = velocity != 0 && switch mode + { + case ALWAYS: true; + case INERTIAL: acceleration == 0; + case SKID: (acceleration == 0 || ((acceleration > 0) != (velocity > 0))); + } + + return apply ? getDrag1D(velocity, drag) : velocity; + } + + public static function getDrag1D(velocity:Float, drag:Float):Float + { + return if (velocity > 0 && velocity - drag > 0) + velocity - drag; + else if (velocity < 0 && velocity + drag < 0) + velocity + drag; + else + 0; + } + + /** + * Checks whether the two objects' delta rects overlap + * @see FlxCollision.getDeltaRect + * @since 6.2.0 + */ + overload public static inline extern function overlapsDelta(a:IFlxCollider, b:IFlxCollider) + { + return overlapsDeltaHelper(a.getCollider(), b.getCollider()); + } + + /** + * Checks whether the two colliders' delta rects overlap + * + * @see FlxCollider.getDeltaRect + * @since 6.2.0 + */ + overload public static inline extern function overlapsDelta(a:FlxCollider, b:FlxCollider) + { + return overlapsDeltaHelper(a, b); + } + + static function overlapsDeltaHelper(a:FlxCollider, b:FlxCollider) + { + final rect1 = a.getDeltaRect(); + final rect2 = b.getDeltaRect(); + + final result = rect1.overlaps(rect2); + + rect1.put(); + rect2.put(); + return result; + } + + public static function process(a:IFlxCollider, b:IFlxCollider, func:(IFlxCollider, IFlxCollider)->Bool):Bool + { + final colliderA = a.getCollider(); + final colliderB = b.getCollider(); + return func(a, b) && processSub(a, b, colliderA, colliderB, func); + } + + static function processSub(a:IFlxCollider, b:IFlxCollider, colliderA:FlxCollider, colliderB:FlxCollider, func:(IFlxCollider, IFlxCollider)->Bool):Bool + { + return switch colliderA.type + { + case TILEMAP: + final tilemap:FlxBaseTilemap = cast a; + return tilemap.forEachCollidingTile(b, (tile)->processSub(tile, b, tile.getCollider(), colliderB, func), false); + case MULTI(processer): + return processer(b, (childA)->processSub(childA, b, childA.getCollider(), colliderB, func)); + case SHAPE(shape): + return switch colliderB.type + { + case TILEMAP: + final tilemap:FlxBaseTilemap = cast b; + return tilemap.forEachCollidingTile(a, (tile)->processSub(a, tile, colliderA, tile.getCollider(), func), false); + case MULTI(processer): + return processer(a, (childB)->processSub(a, childB, colliderA, childB.getCollider(), func)); + case SHAPE(shape): + return func(a, b); + } + } + } + + public static function computeCollisionOverlap(a:IFlxCollider, b:IFlxCollider, maxOverlap:Float, ?result:FlxPoint):FlxPoint + { + final colliderA = a.getCollider(); + final colliderB = b.getCollider(); + return switch [colliderA.type, colliderB.type] + { + case [SHAPE(shapeA), SHAPE(shapeB)]: + switch [shapeA, shapeB] + { + case [CUSTOM(_, func), _]: func(b, result); + case [_, CUSTOM(_, func)]: func(a, result).negate(); + case [AABB, AABB]: computeCollisionOverlapAabb(colliderA, colliderB, maxOverlap, result); + case [shapeA, shapeB]: throw 'Unexpected types: [$shapeA, $shapeB]'; + } + default: + throw "Cannot compute overlap with a MULTI or TILEMAP collider"; + } + } + + //{ region --- TILEMAP --- + + // /** + // * Helper to compute the overlap of two objects, this is used when + // * `objectA.computeCollisionOverlap(objectB)` is called on two objects + // */ + // public static function computeCollisionOverlapTilemap(tilemap:FlxBaseTilemap, b:FlxObject, ?result:FlxPoint) + // { + // if (result == null) + // result = FlxPoint.get(); + // else + // result.set(); + + // final overlap = FlxPoint.get(); + // function each (tile:FlxObject) + // { + // FlxColliderUtil.computeCollisionOverlap(tile, b, overlap); + // // if (result.isZero() && (overlap.x != 0 || overlap.y != 0)) + // if (result.lengthSquared < overlap.lengthSquared) + // { + // result.copyFrom(overlap); + // return true; + // } + // return false; + // } + // tilemap.forEachCollidingTile(b, each, false); + // overlap.put(); + // return result; + // } + + //{ endregion --- TILEMAP --- + + + //{ region --- AABB --- + + /** + * Helper to compute the overlap of two objects, this is used when + * `a.computeCollisionOverlap(b)` is called on two objects + */ + public static function computeCollisionOverlapAabb(a:FlxCollider, b:FlxCollider, maxOverlap:Float, ?result:FlxPoint) + { + if (result == null) + result = FlxPoint.get(); + + if (!checkForPenetration(a, b)) + return result.set(0, 0); + + final allowX = checkCollisionEdgesX(a, b); + final allowY = checkCollisionEdgesY(a, b); + if (!allowX && !allowY) + return result.set(0, 0); + + function abs(n:Float) return n < 0 ? -n : n; + + // only X + if ((allowX && !allowY) || penetratedOnX(a, b)) + { + final overlap = computeCollisionOverlapXAabb(a, b); + if (abs(overlap) > maxOverlap) + return result; + + return result.set(overlap, 0); + } + + // only Y + if ((!allowX && allowY) || penetratedOnY(a, b)) + { + final overlap = computeCollisionOverlapYAabb(a, b); + if (abs(overlap) > maxOverlap) + return result; + + return result.set(0, overlap); + } + + result.set(computeCollisionOverlapXAabb(a, b), computeCollisionOverlapYAabb(a, b)); + + final absX = abs(result.x); + final absY = abs(result.y); + + // separate on the smaller axis + if (absX > absY) + { + result.x = 0; + if (absY > maxOverlap) + result.y = 0; + } + else + { + result.y = 0; + if (absX > maxOverlap) + result.x = 0; + } + + return result; + } + + public static function penetratedOnX(a:FlxCollider, b:FlxCollider) + { + return (lastOverlappedY(a, b) && a.bounds.overlapsY(b.bounds)) + || (checkForFullPenetrationX(a, b) && a.bounds.overlapsX(b.bounds) && lastOverlappedX(a, b)); + } + + public static function penetratedOnY(a:FlxCollider, b:FlxCollider) + { + return (lastOverlappedX(a, b) && a.bounds.overlapsX(b.bounds)) + || (checkForFullPenetrationY(a, b) && a.bounds.overlapsY(b.bounds) && lastOverlappedY(a, b)); + } + + public static function lastOverlappedX(a:FlxCollider, b:FlxCollider) + { + return a.last.x < b.last.x + b.width && a.last.x + a.width > b.last.x; + } + + public static function lastOverlappedY(a:FlxCollider, b:FlxCollider) + { + return a.last.y < b.last.y + b.height && a.last.y + a.height > b.last.y; + } + + /** + * Checks whether the colliders overlap, or if they did overlap this frame + */ + public static function checkForPenetration(a:FlxCollider, b:FlxCollider) + { + return a.bounds.overlaps(b.bounds) + || (checkForFullPenetrationX(a, b) && (a.bounds.overlapsY(b.bounds) || lastOverlappedY(a, b))) + || (checkForFullPenetrationY(a, b) && (a.bounds.overlapsX(b.bounds) || lastOverlappedX(a, b))); + } + + /** + * Checks whether one collider fully passed the other, this frame, in the X axis + */ + public static function checkForFullPenetrationX(a:FlxCollider, b:FlxCollider) + { + return (a.deltaX > b.deltaX + ? a.left > b.right && a.last.x + a.width < b.last.x + : a.right < b.left && a.last.x > b.last.x + b.width); + } + + /** + * Checks whether one collider fully passed the other, this frame, in the Y axis + */ + public static function checkForFullPenetrationY(a:FlxCollider, b:FlxCollider) + { + return (a.deltaY > b.deltaY + ? a.top > b.bottom && a.last.y + a.height < b.last.y + : a.bottom < b.top && a.last.y > b.last.y + b.height); + } + + /** + * Helper to compute the X overlap of two objects, this is used when + * `a.computeCollisionOverlapX(b)` is called on two objects + */ + public static function computeCollisionOverlapXAabb(a:FlxCollider, b:FlxCollider):Float + { + if (a.deltaX > b.deltaX) + return a.x + a.width - b.x; + + return a.x - b.width - b.x; + } + + /** + * Helper to compute the Y overlap of two objects, this is used when + * `a.computeCollisionOverlapY(b)` is called on two objects + */ + public static function computeCollisionOverlapYAabb(a:FlxCollider, b:FlxCollider):Float + { + if (a.deltaY > b.deltaY) + return a.y + a.height - b.y; + + return a.y - b.height - b.y; + } + + //} endregion --- AABB --- + + /** + * Helper to determine which edges of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + * + * @param elseBoth Whether to return `NONE` or "both" directions, when the objects are + * not moving relative to one another + */ + public static function getCollisionEdges(a:FlxCollider, b:FlxCollider, elseBoth = false) + { + return getCollisionEdgesX(a, b, elseBoth) | getCollisionEdgesY(a, b, elseBoth); + } + + /** + * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + * + * @param elseBoth Whether to return `NONE` or "both" directions, when the objects are + * not moving relative to one another + */ + public static function getCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false):FlxDirectionFlags + { + final deltaDiff = a.deltaX - b.deltaX; + return abs(deltaDiff) < 0.0001 ? (elseBoth ? RIGHT | LEFT : NONE) : deltaDiff > 0 ? RIGHT : LEFT; + } + + + /** + * Helper to determine which vertical edge of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + * + * @param elseBoth Whether to return `NONE` or "both" directions, when the objects are + * not moving relative to one another + */ + public static function getCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false):FlxDirectionFlags + { + final deltaDiff = a.deltaY - b.deltaY; + return abs(deltaDiff) < 0.0001 ? (elseBoth ? DOWN | UP : NONE) : deltaDiff > 0 ? DOWN : UP; + } + + static inline function canObjectCollide(obj:FlxCollider, dir:FlxDirectionFlags) + { + return obj.allowCollisions.has(dir); + } + + /** + * Returns whether thetwo objects can collide in the X direction they are traveling. + * Checks `allowCollisions`. + * + * @param elseBoth Whether to return `NONE` or "both" directions, when the objects are + * not moving relative to one another + */ + public static function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false) + { + final dir = getCollisionEdgesX(a, b, elseBoth); + return (dir.has(RIGHT) && canObjectCollide(a, RIGHT) && canObjectCollide(b, LEFT)) + || (dir.has(LEFT) && canObjectCollide(a, LEFT) && canObjectCollide(b, RIGHT)); + } + + /** + * Returns whether thetwo objects can collide in the Y direction they are traveling. + * Checks `allowCollisions`. + * + * @param elseBoth Whether to return `NONE` or "both" directions, when the objects are + * not moving relative to one another + */ + public static function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false) + { + final dir = getCollisionEdgesY(a, b, elseBoth); + return (dir.has(DOWN) && canObjectCollide(a, DOWN) && canObjectCollide(b, UP)) + || (dir.has(UP) && canObjectCollide(a, UP) && canObjectCollide(b, DOWN)); + } +} + +interface IFlxCollider +{ + /** + * The collider of this object + * **Note:** For FlxObjects calling this will copy the objects collision properties into the collider + */ + function getCollider():FlxCollider; +} + +/** + * Determines when to apply collision drag to one object that collided with another. + */ +enum abstract FlxCollisionDragType(Int) +{ + /** Never drags on colliding objects. */ + var NEVER = 0; + + /** Always drags on colliding objects. */ + var ALWAYS = 1; + + /** Drags when colliding with immovable objects. */ + var IMMOVABLE = 2; + + /** Drags when colliding with heavier objects. Immovable objects have infinite mass. */ + var HEAVIER = 3; +} + +enum FlxForceType +{ + NONE; + ORTHO(x:Float, y:Float); + LINEAR(amount:Float); +} + +enum FlxDragMode +{ + /** Drag is applied every frame */ + ALWAYS; + + /** Drag is applied every frame that the object is not accelerating */ + INERTIAL; + + /** Drag is applied every frame that the object is not accelerating in the current direction of motion */ + SKID; +} + +private inline function abs(n:Float) +{ + return n > 0 ? n : -n; +} + +private inline function min(a:Float, b:Float) +{ + return a < b ? a : b; +} \ No newline at end of file diff --git a/flixel/physics/FlxCollisionQuadTree.hx b/flixel/physics/FlxCollisionQuadTree.hx new file mode 100644 index 0000000000..afd33dab44 --- /dev/null +++ b/flixel/physics/FlxCollisionQuadTree.hx @@ -0,0 +1,297 @@ +package flixel.physics; + +import flixel.FlxBasic; +import flixel.FlxObject; +import flixel.group.FlxGroup; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.physics.FlxCollider; +import flixel.system.FlxLinkedList; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxPool; + +typedef NotifyCallback = (IFlxCollider, IFlxCollider) -> Void; + +/** + * A fairly generic quad tree structure for rapid overlap checks. + * FlxCollisionQuadTree is also configured for single or dual list operation. + * You can add items either to its A list or its B list. + * When you do an overlap check, you can compare the A list to itself, + * or the A list against the B list. Handy for different things! + */ +class FlxCollisionQuadTree implements IFlxDestroyable implements IFlxPooled +{ + public static var pool:FlxPool = new FlxPool(() -> new FlxCollisionQuadTree()); + + /** + * Controls the granularity of the quad tree. Default is 6 (decent performance on large and small worlds). + */ + public var divisions:Int = 0; + + public var rect:FlxRect; + + final listA:Array = []; + final listB:Array = []; + + var nw:Null; + var ne:Null; + var se:Null; + var sw:Null; + + overload public static inline extern function executeOnce(x, y, width, height, divisions, objectA, objectB, notifier, processer) + { + final quad = get(x, y, width, height, divisions); + final result = quad.loadAndExecute(objectA, objectB, notifier, processer); + quad.put(); + return result; + } + + overload public static inline extern function executeOnce(rect, divisions, objectA, objectB, notifier, processer) + { + return executeOnce(rect.x, rect.y, rect.width, rect.height, divisions, objectA, objectB, notifier, processer); + } + + overload public static inline extern function get(x, y, width, height, divisions) + { + return pool.get().reset(x, y, width, height, divisions); + } + + overload public static inline extern function get(rect:FlxRect, divisions:Int) + { + return get(rect.x, rect.y, rect.width, rect.height, divisions); + } + + overload static inline extern function getSub(x, y, width, height, parent) + { + return pool.get().resetSub(x, y, width, height, parent); + } + + overload static inline extern function getSub(rect:FlxRect, parent) + { + return getSub(rect.x, rect.y, rect.width, rect.height, parent); + } + + function new() {} + + public function reset(x:Float, y:Float, width:Float, height:Float, divisions:Int) + { + this.divisions = divisions; + + rect = FlxRect.get(x, y, width, height); + + listA.resize(0); + listB.resize(0); + + return this; + } + + public function resetSub(x:Float, y:Float, width:Float, height:Float, parent:FlxCollisionQuadTree) + { + return reset(x, y, width, height, parent.divisions - 1); + } + + /** + * Clean up memory. + */ + public function destroy():Void + { + listA.resize(0); + listB.resize(0); + + nw = FlxDestroyUtil.destroy(nw); + ne = FlxDestroyUtil.destroy(ne); + sw = FlxDestroyUtil.destroy(sw); + se = FlxDestroyUtil.destroy(se); + } + + public function put() + { + pool.put(this); + } + + /** + * Adds the objects or groups' members to the quadtree, searches for overlaps, + * processes them with the `processCallback`, calls the `notifyCallback` and eventually + * returns true if there were any overlaps. + * + * @param objectOrGroup1 Any object that is or extends FlxObject or FlxGroup. + * @param objectOrGroup2 Any object that is or extends FlxObject or FlxGroup. + * If null, the first parameter will be checked against itself. + * @param notifyCallback A function called whenever two overlapping objects are found, + * and the processCallback is `null` or returns `true`. + * @param processCallback A function called whenever two overlapping objects are found. + * This will return true if the notifyCallback should be called. + * @return Whether or not any overlaps were found. + */ + public function loadAndExecute(objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifier:NotifyCallback, ?processer:ProcessCallback):Bool + { + load(objectOrGroup1, objectOrGroup2); + return execute(objectOrGroup2 != null, notifier, processer); + } + + function load(objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic):Void + { + add(objectOrGroup1, true); + if (objectOrGroup2 != null && objectOrGroup2 != objectOrGroup1) + add(objectOrGroup2, false); + } + + /** + * Call this function to add an object to the root of the tree. + * This function will recursively add all group members, but not the groups themselves. + * + * @param basic FlxObjects are just added, FlxGroups are recursed and their applicable members added accordingly. + * @param list A int flag indicating the list to which you want to add the objects. Options are A_LIST and B_LIST. + */ + @:access(flixel.group.FlxTypedGroup.resolveGroup) + function add(basic:FlxBasic, listA:Bool):Void + { + final group = FlxTypedGroup.resolveGroup(basic); + if (group != null) + { + for (member in group.members) + { + if (member != null && member.exists) + add(member, listA); + } + } + else if (basic is IFlxCollider) + { + final collider = (cast basic : IFlxCollider).getCollider(); + if (basic.exists && collider.allowCollisions != NONE) + { + addCollider(cast basic, collider, listA); + } + } + else + { + throw 'Can only add FlxGroups and IFlxColliders to quad trees'; + } + } + + /** + * Internal function for recursively navigating and creating the tree + * while adding objects to the appropriate nodes. + */ + function addCollider(object:IFlxCollider, collider:FlxCollider, isA:Bool):Void + { + final bounds = collider.bounds; + // If this quad (not its children) lies entirely inside this object, add it here + if (divisions > 0 || bounds.contains(rect)) + { + (isA ? listA : listB).push(object); + return; + } + + final quadrant = FlxRect.get(); + + getQuadrant(false, false, quadrant); + if (quadrant.overlaps(bounds)) + { + if (nw == null) + nw = getSub(quadrant, this); + + nw.addCollider(object, collider, isA); + } + + getQuadrant(true, false, quadrant); + if (quadrant.overlaps(bounds)) + { + if (ne == null) + ne = getSub(quadrant, this); + + ne.addCollider(object, collider, isA); + } + + getQuadrant(false, true, quadrant); + if (quadrant.overlaps(bounds)) + { + if (sw == null) + sw = getSub(quadrant, this); + + sw.addCollider(object, collider, isA); + } + + getQuadrant(true, true, quadrant); + if (quadrant.overlaps(bounds)) + { + if (se == null) + se = getSub(quadrant, this); + + se.addCollider(object, collider, isA); + } + + quadrant.put(); + } + + function execute(useBothLists:Bool, notifier:NotifyCallback, processer:ProcessCallback):Bool + { + var processed = false; + + if (useBothLists) + { + for (a in 0...listA.length) + { + for (b in 0...listB.length) + { + if (process(listA[a], listB[b], notifier, processer)) + processed = true; + } + } + } + else + { + for (a in 0...listA.length) + { + for (b in a...listA.length) + { + if (process(listA[a], listA[b], notifier, processer)) + processed = true; + } + } + } + + // Advance through the tree by calling overlap on each child + if (nw != null && nw.execute(useBothLists, notifier, processer)) + processed = true; + + if (ne != null && ne.execute(useBothLists, notifier, processer)) + processed = true; + + if (se != null && se.execute(useBothLists, notifier, processer)) + processed = true; + + if (sw != null && sw.execute(useBothLists, notifier, processer)) + processed = true; + + return processed; + } + + function process(a:IFlxCollider, b:IFlxCollider, notifier:Null, processer:Null):Bool + { + if (a.getCollider().bounds.overlaps(b.getCollider().bounds) && (processer == null || processer(a, b))) + { + if (notifier != null) + notifier(a, b); + + return true; + } + + return false; + } + + function getQuadrant(up:Bool, left:Bool, result:FlxRect) + { + final halfX = rect.width / 2; + final halfY = rect.height / 2; + + if (up && left) + result.set(rect.x, rect.y, halfX, halfY); + else if (up && !left) + result.set(rect.x + halfX, rect.y, halfX, rect.height); + else if (!up && left) + result.set(rect.x, rect.y + halfY, rect.width, halfY); + else if (!up && !left) + result.set(rect.x + halfX, rect.y + halfY, halfX, halfY); + } +} diff --git a/flixel/system/FlxQuadTree.hx b/flixel/system/FlxQuadTree.hx index a922e2f925..c2338e3186 100644 --- a/flixel/system/FlxQuadTree.hx +++ b/flixel/system/FlxQuadTree.hx @@ -2,9 +2,15 @@ package flixel.system; import flixel.FlxBasic; import flixel.FlxObject; -import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxGroup; +import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.util.FlxCollision; import flixel.util.FlxDestroyUtil; +import flixel.util.FlxPool; + +typedef ProcessCallback = (FlxObject, FlxObject) -> Bool; +typedef NotifyCallback = (FlxObject, FlxObject) -> Void; /** * A fairly generic quad tree structure for rapid overlap checks. @@ -13,695 +19,265 @@ import flixel.util.FlxDestroyUtil; * When you do an overlap check, you can compare the A list to itself, * or the A list against the B list. Handy for different things! */ -class FlxQuadTree extends FlxRect +class FlxQuadTree implements IFlxDestroyable implements IFlxPooled { - /** - * Flag for specifying that you want to add an object to the A list. - */ - public static inline var A_LIST:Int = 0; - - /** - * Flag for specifying that you want to add an object to the B list. - */ - public static inline var B_LIST:Int = 1; - + public static var pool:FlxPool = new FlxPool(() -> new FlxQuadTree()); + /** * Controls the granularity of the quad tree. Default is 6 (decent performance on large and small worlds). */ - public static var divisions:Int; - - public var exists:Bool; - - /** - * Whether this branch of the tree can be subdivided or not. - */ - var _canSubdivide:Bool; - - /** - * Refers to the internal A and B linked lists, - * which are used to store objects in the leaves. - */ - var _headA:FlxLinkedList; - - /** - * Refers to the internal A and B linked lists, - * which are used to store objects in the leaves. - */ - var _tailA:FlxLinkedList; - - /** - * Refers to the internal A and B linked lists, - * which are used to store objects in the leaves. - */ - var _headB:FlxLinkedList; - - /** - * Refers to the internal A and B linked lists, - * which are used to store objects in the leaves. - */ - var _tailB:FlxLinkedList; - - /** - * Internal, governs and assists with the formation of the tree. - */ - static var _min:Int; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _northWestTree:FlxQuadTree; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _northEastTree:FlxQuadTree; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _southEastTree:FlxQuadTree; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _southWestTree:FlxQuadTree; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _leftEdge:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _rightEdge:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _topEdge:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _bottomEdge:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _halfWidth:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _halfHeight:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _midpointX:Float; - - /** - * Internal, governs and assists with the formation of the tree. - */ - var _midpointY:Float; - - /** - * Internal, used to reduce recursive method parameters during object placement and tree formation. - */ - static var _object:FlxObject; - - /** - * Internal, used to reduce recursive method parameters during object placement and tree formation. - */ - static var _objectLeftEdge:Float; - - /** - * Internal, used to reduce recursive method parameters during object placement and tree formation. - */ - static var _objectTopEdge:Float; - - /** - * Internal, used to reduce recursive method parameters during object placement and tree formation. - */ - static var _objectRightEdge:Float; - - /** - * Internal, used to reduce recursive method parameters during object placement and tree formation. - */ - static var _objectBottomEdge:Float; - - /** - * Internal, used during tree processing and overlap checks. - */ - static var _list:Int; - - /** - * Internal, used during tree processing and overlap checks. - */ - static var _useBothLists:Bool; - - /** - * Internal, used during tree processing and overlap checks. - */ - static var _processingCallback:FlxObject->FlxObject->Bool; - - /** - * Internal, used during tree processing and overlap checks. - */ - static var _notifyCallback:FlxObject->FlxObject->Void; - - /** - * Internal, used during tree processing and overlap checks. - */ - static var _iterator:FlxLinkedList; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _objectHullX:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _objectHullY:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _objectHullWidth:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _objectHullHeight:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _checkObjectHullX:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _checkObjectHullY:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _checkObjectHullWidth:Float; - - /** - * Internal, helpers for comparing actual object-to-object overlap - see overlapNode(). - */ - static var _checkObjectHullHeight:Float; - - /** - * Pooling mechanism, turn FlxQuadTree into a linked list, when FlxQuadTrees are destroyed, they get added to the list, and when they get recycled they get removed. - */ - public static var _NUM_CACHED_QUAD_TREES:Int = 0; - - static var _cachedTreesHead:FlxQuadTree; - - var next:FlxQuadTree; - - /** - * Private, use recycle instead. - */ - function new(X:Float, Y:Float, Width:Float, Height:Float, ?Parent:FlxQuadTree) + public var divisions:Int; + + public var rect:FlxRect; + + final listA:Array = []; + final listB:Array = []; + + var nw:Null; + var ne:Null; + var se:Null; + var sw:Null; + + // var minSize:Float; + + overload public static inline extern function executeOnce(x, y, width, height, divisions, objectA, objectB, notifier, processer) { - super(); - set(X, Y, Width, Height); - reset(X, Y, Width, Height, Parent); + final quad = get(x, y, width, height, divisions); + final result = quad.loadAndExecute(objectA, objectB, notifier, processer); + quad.put(); + return result; } - + + overload public static inline extern function executeOnce(rect, divisions, objectA, objectB, notifier, processer) + { + return executeOnce(rect.x, rect.y, rect.width, rect.height, divisions, objectA, objectB, notifier, processer); + } + /** * Recycle a cached Quad Tree node, or creates a new one if needed. - * @param X The X-coordinate of the point in space. - * @param Y The Y-coordinate of the point in space. - * @param Width Desired width of this node. - * @param Height Desired height of this node. - * @param Parent The parent branch or node. Pass null to create a root. + * @param x The X-coordinate of the point in space. + * @param y The Y-coordinate of the point in space. + * @param width Desired width of this node. + * @param height Desired height of this node. + * @param divisions Desired height of this node. */ - public static function recycle(X:Float, Y:Float, Width:Float, Height:Float, ?Parent:FlxQuadTree):FlxQuadTree + public static function get(x, y, width, height, divisions) { - if (_cachedTreesHead != null) - { - var cachedTree:FlxQuadTree = _cachedTreesHead; - _cachedTreesHead = _cachedTreesHead.next; - _NUM_CACHED_QUAD_TREES--; - - cachedTree.reset(X, Y, Width, Height, Parent); - return cachedTree; - } - else - return new FlxQuadTree(X, Y, Width, Height, Parent); + return pool.get().reset(x, y, width, height, divisions); } - - /** - * Clear cached Quad Tree nodes. You might want to do this when loading new levels (probably not though, no need to clear cache unless you run into memory problems). - */ - public static function clearCache():Void + + static function getSub(x, y, width, height, parent) { - // null out next pointers to help out garbage collector - while (_cachedTreesHead != null) - { - var node = _cachedTreesHead; - _cachedTreesHead = _cachedTreesHead.next; - node.next = null; - } - _NUM_CACHED_QUAD_TREES = 0; + return pool.get().resetSub(x, y, width, height, parent); } - - public function reset(X:Float, Y:Float, Width:Float, Height:Float, ?Parent:FlxQuadTree):Void + + function new() {} + + public function reset(x:Float, y:Float, width:Float, height:Float, divisions:Int) { - exists = true; - - set(X, Y, Width, Height); - - _headA = _tailA = FlxLinkedList.recycle(); - _headB = _tailB = FlxLinkedList.recycle(); - - // Copy the parent's children (if there are any) - if (Parent != null) - { - var iterator:FlxLinkedList; - var ot:FlxLinkedList; - if (Parent._headA.object != null) - { - iterator = Parent._headA; - while (iterator != null) - { - if (_tailA.object != null) - { - ot = _tailA; - _tailA = FlxLinkedList.recycle(); - ot.next = _tailA; - } - _tailA.object = iterator.object; - iterator = iterator.next; - } - } - if (Parent._headB.object != null) - { - iterator = Parent._headB; - while (iterator != null) - { - if (_tailB.object != null) - { - ot = _tailB; - _tailB = FlxLinkedList.recycle(); - ot.next = _tailB; - } - _tailB.object = iterator.object; - iterator = iterator.next; - } - } - } - else - { - _min = Math.floor((width + height) / (2 * divisions)); - } - _canSubdivide = (width > _min) || (height > _min); - - // Set up comparison/sort helpers - _northWestTree = null; - _northEastTree = null; - _southEastTree = null; - _southWestTree = null; - _leftEdge = x; - _rightEdge = x + width; - _halfWidth = width / 2; - _midpointX = _leftEdge + _halfWidth; - _topEdge = y; - _bottomEdge = y + height; - _halfHeight = height / 2; - _midpointY = _topEdge + _halfHeight; + this.divisions = divisions; + + rect = FlxRect.get(x, y, width, height); + + listA.resize(0); + listB.resize(0); + + return this; } - + + public function resetSub(x:Float, y:Float, width:Float, height:Float, parent:FlxQuadTree) + { + return reset(x, y, width, height, parent.divisions - 1); + } + /** * Clean up memory. */ - override public function destroy():Void + public function destroy():Void { - _headA = FlxDestroyUtil.destroy(_headA); - _headB = FlxDestroyUtil.destroy(_headB); - - _tailA = FlxDestroyUtil.destroy(_tailA); - _tailB = FlxDestroyUtil.destroy(_tailB); - - _northWestTree = FlxDestroyUtil.destroy(_northWestTree); - _northEastTree = FlxDestroyUtil.destroy(_northEastTree); - - _southWestTree = FlxDestroyUtil.destroy(_southWestTree); - _southEastTree = FlxDestroyUtil.destroy(_southEastTree); - - _object = null; - _processingCallback = null; - _notifyCallback = null; - - exists = false; - - // Deposit this tree into the linked list for reusal. - next = _cachedTreesHead; - _cachedTreesHead = this; - _NUM_CACHED_QUAD_TREES++; - - super.destroy(); + rect = FlxDestroyUtil.put(rect); + listA.resize(0); + listB.resize(0); + + nw = FlxDestroyUtil.put(nw); + ne = FlxDestroyUtil.put(ne); + sw = FlxDestroyUtil.put(sw); + se = FlxDestroyUtil.put(se); } - - /** - * Load objects and/or groups into the quad tree, and register notify and processing callbacks. - * @param ObjectOrGroup1 Any object that is or extends FlxObject or FlxGroup. - * @param ObjectOrGroup2 Any object that is or extends FlxObject or FlxGroup. If null, the first parameter will be checked against itself. - * @param NotifyCallback A function with the form myFunction(Object1:FlxObject,Object2:FlxObject):void that is called whenever two objects are found to overlap in world space, and either no ProcessCallback is specified, or the ProcessCallback returns true. - * @param ProcessCallback A function with the form myFunction(Object1:FlxObject,Object2:FlxObject):Boolean that is called whenever two objects are found to overlap in world space. The NotifyCallback is only called if this function returns true. See FlxObject.separate(). - */ - public function load(ObjectOrGroup1:FlxBasic, ?ObjectOrGroup2:FlxBasic, ?NotifyCallback:FlxObject->FlxObject->Void, - ?ProcessCallback:FlxObject->FlxObject->Bool):Void + + public function put() { - add(ObjectOrGroup1, A_LIST); - if (ObjectOrGroup2 != null) - { - add(ObjectOrGroup2, B_LIST); - _useBothLists = true; - } - else - { - _useBothLists = false; - } - _notifyCallback = NotifyCallback; - _processingCallback = ProcessCallback; + pool.put(this); } - + + /** + * Adds the objects or groups' members to the quadtree, searches for overlaps, + * processes them with the `processCallback`, calls the `notifyCallback` and eventually + * returns true if there were any overlaps. + * + * @param objectOrGroup1 Any object that is or extends FlxObject or FlxGroup. + * @param objectOrGroup2 Any object that is or extends FlxObject or FlxGroup. + * If null, the first parameter will be checked against itself. + * @param notifyCallback A function called whenever two overlapping objects are found, + * and the processCallback is `null` or returns `true`. + * @param processCallback A function called whenever two overlapping objects are found. + * This will return true if the notifyCallback should be called. + * @return Whether or not any overlaps were found. + */ + public function loadAndExecute(objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifier:NotifyCallback, ?processer:ProcessCallback):Bool + { + load(objectOrGroup1, objectOrGroup2); + return execute(objectOrGroup2 != null, notifier, processer); + } + + public function load(objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic):Void + { + add(objectOrGroup1, true); + if (objectOrGroup2 != null && objectOrGroup2 != objectOrGroup1) + add(objectOrGroup2, false); + } + /** * Call this function to add an object to the root of the tree. - * This function will recursively add all group members, but - * not the groups themselves. - * @param ObjectOrGroup FlxObjects are just added, FlxGroups are recursed and their applicable members added accordingly. - * @param List A int flag indicating the list to which you want to add the objects. Options are A_LIST and B_LIST. + * This function will recursively add all group members, but not the groups themselves. + * + * @param basic FlxObjects are just added, FlxGroups are recursed and their applicable members added accordingly. + * @param list A int flag indicating the list to which you want to add the objects. Options are A_LIST and B_LIST. */ @:access(flixel.group.FlxTypedGroup.resolveGroup) - public function add(ObjectOrGroup:FlxBasic, list:Int):Void + function add(basic:FlxBasic, listA:Bool):Void { - _list = list; - - var group = FlxTypedGroup.resolveGroup(ObjectOrGroup); + final group = FlxTypedGroup.resolveGroup(basic); if (group != null) { - var i:Int = 0; - var basic:FlxBasic; - var members:Array = group.members; - var l:Int = group.length; - while (i < l) + for (member in group.members) { - basic = members[i++]; - if (basic != null && basic.exists) - { - group = FlxTypedGroup.resolveGroup(basic); - if (group != null) - { - add(group, list); - } - else - { - _object = cast basic; - if (_object.exists && _object.allowCollisions != NONE) - { - _objectLeftEdge = _object.x; - _objectTopEdge = _object.y; - _objectRightEdge = _object.x + _object.width; - _objectBottomEdge = _object.y + _object.height; - addObject(); - } - } - } + if (member != null && member.exists) + add(member, listA); } } + else if (basic is FlxObject) + { + final object:FlxObject = cast basic; + if (object.exists && object.allowCollisions != NONE) + addObject(object, listA); + } else { - _object = cast ObjectOrGroup; - if (_object.exists && _object.allowCollisions != NONE) - { - _objectLeftEdge = _object.x; - _objectTopEdge = _object.y; - _objectRightEdge = _object.x + _object.width; - _objectBottomEdge = _object.y + _object.height; - addObject(); - } + throw 'Can only add FlxGroups, FlxSpriteGroups and FlxObjects to quad trees'; } } - + /** * Internal function for recursively navigating and creating the tree * while adding objects to the appropriate nodes. */ - function addObject():Void + function addObject(object:FlxObject, isA:Bool):Void { - // If this quad (not its children) lies entirely inside this object, add it here - if (!_canSubdivide - || (_leftEdge >= _objectLeftEdge && _rightEdge <= _objectRightEdge && _topEdge >= _objectTopEdge && _bottomEdge <= _objectBottomEdge)) + final bounds = object.getHitbox(); + // If this quad lies entirely inside this object, add it here + if (divisions > 0 || bounds.contains(rect)) { - addToList(); + (isA ? listA : listB).push(object); + bounds.put(); return; } - - // See if the selected object fits completely inside any of the quadrants - if ((_objectLeftEdge > _leftEdge) && (_objectRightEdge < _midpointX)) + + final quadrant = FlxRect.get(); + + getQuadrant(false, false, quadrant); + if (quadrant.overlaps(bounds)) { - if ((_objectTopEdge > _topEdge) && (_objectBottomEdge < _midpointY)) - { - if (_northWestTree == null) - { - _northWestTree = FlxQuadTree.recycle(_leftEdge, _topEdge, _halfWidth, _halfHeight, this); - } - _northWestTree.addObject(); - return; - } - if ((_objectTopEdge > _midpointY) && (_objectBottomEdge < _bottomEdge)) - { - if (_southWestTree == null) - { - _southWestTree = FlxQuadTree.recycle(_leftEdge, _midpointY, _halfWidth, _halfHeight, this); - } - _southWestTree.addObject(); - return; - } + if (nw == null) + nw = getSub(quadrant.x, quadrant.y, quadrant.width, quadrant.height, this); + + nw.addObject(object, isA); } - if ((_objectLeftEdge > _midpointX) && (_objectRightEdge < _rightEdge)) + + getQuadrant(true, false, quadrant); + if (quadrant.overlaps(bounds)) { - if ((_objectTopEdge > _topEdge) && (_objectBottomEdge < _midpointY)) - { - if (_northEastTree == null) - { - _northEastTree = FlxQuadTree.recycle(_midpointX, _topEdge, _halfWidth, _halfHeight, this); - } - _northEastTree.addObject(); - return; - } - if ((_objectTopEdge > _midpointY) && (_objectBottomEdge < _bottomEdge)) - { - if (_southEastTree == null) - { - _southEastTree = FlxQuadTree.recycle(_midpointX, _midpointY, _halfWidth, _halfHeight, this); - } - _southEastTree.addObject(); - return; - } + if (ne == null) + ne = getSub(quadrant.x, quadrant.y, quadrant.width, quadrant.height, this); + + ne.addObject(object, isA); } - - // If it wasn't completely contained we have to check out the partial overlaps - if ((_objectRightEdge > _leftEdge) && (_objectLeftEdge < _midpointX) && (_objectBottomEdge > _topEdge) && (_objectTopEdge < _midpointY)) + + getQuadrant(false, true, quadrant); + if (quadrant.overlaps(bounds)) { - if (_northWestTree == null) - { - _northWestTree = FlxQuadTree.recycle(_leftEdge, _topEdge, _halfWidth, _halfHeight, this); - } - _northWestTree.addObject(); + if (sw == null) + sw = getSub(quadrant.x, quadrant.y, quadrant.width, quadrant.height, this); + + sw.addObject(object, isA); } - if ((_objectRightEdge > _midpointX) && (_objectLeftEdge < _rightEdge) && (_objectBottomEdge > _topEdge) && (_objectTopEdge < _midpointY)) + + getQuadrant(true, true, quadrant); + if (quadrant.overlaps(bounds)) { - if (_northEastTree == null) - { - _northEastTree = FlxQuadTree.recycle(_midpointX, _topEdge, _halfWidth, _halfHeight, this); - } - _northEastTree.addObject(); - } - if ((_objectRightEdge > _midpointX) && (_objectLeftEdge < _rightEdge) && (_objectBottomEdge > _midpointY) && (_objectTopEdge < _bottomEdge)) - { - if (_southEastTree == null) - { - _southEastTree = FlxQuadTree.recycle(_midpointX, _midpointY, _halfWidth, _halfHeight, this); - } - _southEastTree.addObject(); - } - if ((_objectRightEdge > _leftEdge) && (_objectLeftEdge < _midpointX) && (_objectBottomEdge > _midpointY) && (_objectTopEdge < _bottomEdge)) - { - if (_southWestTree == null) - { - _southWestTree = FlxQuadTree.recycle(_leftEdge, _midpointY, _halfWidth, _halfHeight, this); - } - _southWestTree.addObject(); + if (se == null) + se = getSub(quadrant.x, quadrant.y, quadrant.width, quadrant.height, this); + + se.addObject(object, isA); } + + quadrant.put(); + bounds.put(); } - - /** - * Internal function for recursively adding objects to leaf lists. - */ - function addToList():Void + + public function execute(useBothLists:Bool, notifier:NotifyCallback, processer:ProcessCallback):Bool { - var ot:FlxLinkedList; - if (_list == A_LIST) - { - if (_tailA.object != null) - { - ot = _tailA; - _tailA = FlxLinkedList.recycle(); - ot.next = _tailA; - } - _tailA.object = _object; - } - else + var processed = false; + + final listB = useBothLists ? this.listB : this.listA; + for (a in 0...listA.length) { - if (_tailB.object != null) + final objectA = listA[a]; + final rectA = FlxCollision.getDeltaRect(objectA); + for (b in 0...listB.length) { - ot = _tailB; - _tailB = FlxLinkedList.recycle(); - ot.next = _tailB; + final objectB = listB[b]; + final rectB = FlxCollision.getDeltaRect(objectB); + if (processOverlap(objectA, objectB, rectA, rectB, notifier, processer)) + processed = true; } - _tailB.object = _object; - } - if (!_canSubdivide) - { - return; - } - if (_northWestTree != null) - { - _northWestTree.addToList(); - } - if (_northEastTree != null) - { - _northEastTree.addToList(); - } - if (_southEastTree != null) - { - _southEastTree.addToList(); - } - if (_southWestTree != null) - { - _southWestTree.addToList(); } + + + // Advance through the tree by calling overlap on each child + if (nw != null && nw.execute(useBothLists, notifier, processer)) + processed = true; + + if (ne != null && ne.execute(useBothLists, notifier, processer)) + processed = true; + + if (se != null && se.execute(useBothLists, notifier, processer)) + processed = true; + + if (sw != null && sw.execute(useBothLists, notifier, processer)) + processed = true; + + return processed; } - - /** - * FlxQuadTree's other main function. Call this after adding objects - * using FlxQuadTree.load() to compare the objects that you loaded. - * @return Whether or not any overlaps were found. - */ - public function execute():Bool + + function processOverlap(a:FlxObject, b:FlxObject, rectA:FlxRect, rectB:FlxRect, notifier:Null, processer:Null):Bool { - var overlapProcessed:Bool = false; - - if (_headA.object != null) - { - var iterator = _headA; - while (iterator != null) - { - _object = iterator.object; - if (_useBothLists) - { - _iterator = _headB; - } - else - { - _iterator = iterator.next; - } - if (_object != null && _object.exists && _object.allowCollisions != NONE && _iterator != null && _iterator.object != null && overlapNode()) - { - overlapProcessed = true; - } - iterator = iterator.next; - } - } - - // Advance through the tree by calling overlap on each child - if ((_northWestTree != null) && _northWestTree.execute()) + if (rectA.overlaps(rectB) && (processer == null || processer(a, b))) { - overlapProcessed = true; + if (notifier != null) + notifier(a, b); + + return true; } - if ((_northEastTree != null) && _northEastTree.execute()) - { - overlapProcessed = true; - } - if ((_southEastTree != null) && _southEastTree.execute()) - { - overlapProcessed = true; - } - if ((_southWestTree != null) && _southWestTree.execute()) - { - overlapProcessed = true; - } - - return overlapProcessed; + + return false; } - - /** - * An internal function for comparing an object against the contents of a node. - * @return Whether or not any overlaps were found. - */ - function overlapNode():Bool + + function getQuadrant(up:Bool, left:Bool, result:FlxRect) { - // Calculate bulk hull for _object - _objectHullX = (_object.x < _object.last.x) ? _object.x : _object.last.x; - _objectHullY = (_object.y < _object.last.y) ? _object.y : _object.last.y; - _objectHullWidth = _object.x - _object.last.x; - _objectHullWidth = _object.width + ((_objectHullWidth > 0) ? _objectHullWidth : -_objectHullWidth); - _objectHullHeight = _object.y - _object.last.y; - _objectHullHeight = _object.height + ((_objectHullHeight > 0) ? _objectHullHeight : -_objectHullHeight); - - // Walk the list and check for overlaps - var overlapProcessed:Bool = false; - var checkObject:FlxObject; - - while (_iterator != null) - { - checkObject = _iterator.object; - if (_object == checkObject || !checkObject.exists || checkObject.allowCollisions == NONE) - { - _iterator = _iterator.next; - continue; - } - - // Calculate bulk hull for checkObject - _checkObjectHullX = (checkObject.x < checkObject.last.x) ? checkObject.x : checkObject.last.x; - _checkObjectHullY = (checkObject.y < checkObject.last.y) ? checkObject.y : checkObject.last.y; - _checkObjectHullWidth = checkObject.x - checkObject.last.x; - _checkObjectHullWidth = checkObject.width + ((_checkObjectHullWidth > 0) ? _checkObjectHullWidth : -_checkObjectHullWidth); - _checkObjectHullHeight = checkObject.y - checkObject.last.y; - _checkObjectHullHeight = checkObject.height + ((_checkObjectHullHeight > 0) ? _checkObjectHullHeight : -_checkObjectHullHeight); - - // Check for intersection of the two hulls - if ((_objectHullX + _objectHullWidth > _checkObjectHullX) - && (_objectHullX < _checkObjectHullX + _checkObjectHullWidth) - && (_objectHullY + _objectHullHeight > _checkObjectHullY) - && (_objectHullY < _checkObjectHullY + _checkObjectHullHeight)) - { - // Execute callback functions if they exist - if (_processingCallback == null || _processingCallback(_object, checkObject)) - { - overlapProcessed = true; - if (_notifyCallback != null) - { - _notifyCallback(_object, checkObject); - } - } - } - if (_iterator != null) - { - _iterator = _iterator.next; - } - } - - return overlapProcessed; + result.set(rect.x, rect.y, rect.width / 2, rect.height / 2); + + if (!left) result.x += result.width; + if (!up ) result.y += result.height; } -} +} \ No newline at end of file diff --git a/flixel/system/debug/stats/Stats.hx b/flixel/system/debug/stats/Stats.hx index c76b12963b..10ddd2faee 100644 --- a/flixel/system/debug/stats/Stats.hx +++ b/flixel/system/debug/stats/Stats.hx @@ -1,15 +1,16 @@ package flixel.system.debug.stats; -import openfl.display.BitmapData; -import openfl.system.System; -import openfl.text.TextField; import flixel.FlxG; import flixel.math.FlxMath; +import flixel.physics.FlxCollisionQuadTree; import flixel.system.FlxLinkedList; import flixel.system.FlxQuadTree; import flixel.system.debug.DebuggerUtil; import flixel.system.ui.FlxSystemButton; import flixel.util.FlxColor; +import openfl.display.BitmapData; +import openfl.system.System; +import openfl.text.TextField; /** @@ -296,9 +297,11 @@ class Stats extends Window drawTimeGraph.update(drwTime); updateTimeGraph.update(updTime); - + _rightTextField.text = activeCount + " (" + updTime + "ms)\n" + visibleCount + " (" + drwTime + "ms)\n" - + (FlxG.renderTile ? (drawCallsCount + "\n") : "") + FlxQuadTree._NUM_CACHED_QUAD_TREES + "\n" + FlxLinkedList._NUM_CACHED_FLX_LIST; + + (FlxG.renderTile ? (drawCallsCount + "\n") : "") + + (FlxQuadTree.pool.length + FlxCollisionQuadTree.pool.length) + "\n" + FlxLinkedList._NUM_CACHED_FLX_LIST + ; } function divide(f1:Float, f2:Float):Float diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx new file mode 100644 index 0000000000..9ec05dde83 --- /dev/null +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -0,0 +1,347 @@ +package flixel.system.frontEnds; + +import flixel.FlxG; +import flixel.FlxObject; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.physics.FlxCollider; +import flixel.physics.FlxCollisionQuadTree; +import flixel.tile.FlxBaseTilemap; +import flixel.util.FlxDirectionFlags; + +using flixel.physics.FlxCollider.FlxColliderUtil; +using flixel.util.FlxCollision; + +@:access(flixel.FlxObject) +class CollisionFrontEnd +{ + /** + * Collisions between FlxObjects will not resolve overlaps larger than this values, in pixels + */ + public var maxOverlap = 4.0; + + /** + * How many times the quad tree should divide the world on each axis. + * Generally, sparse collisions can have fewer divisons, + * while denser collision activity usually profits from more. Default value is `6`. + */ + public static var worldDivisions:Int = 6; + + public function new () {} + + /** + * Call this function to see if one `FlxObject` overlaps another within `FlxG.worldBounds`. + * Can be called with one object and one group, or two groups, or two objects, + * whatever floats your boat! For maximum performance try bundling a lot of objects + * together using a `FlxGroup` (or even bundling groups together!). + * + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * NOTE: this takes the entire area of `FlxTilemap`s into account (including "empty" tiles). + * Use `FlxTilemap#overlaps()` if you don't want that. + * + * @param a The first object or group you want to check. + * @param b The second object or group you want to check. Can be the same group as the first. + * @param notify Called on every object that overlaps another. + * @param process Called on every object that overlaps another, determines whether to call the `notify`. + * @return Whether any overlaps were detected. + */ + overload public inline extern function overlap(a, ?b, ?notify:(TA, TB)->Void, ?process:(TA, TB)->Bool) + { + return overlapHelper(a, b, notify, process); + } + + /** + * Call this function to see if one `FlxObject` overlaps another within `FlxG.worldBounds`. + * Can be called with one object and one group, or two groups, or two objects, + * whatever floats your boat! For maximum performance try bundling a lot of objects + * together using a `FlxGroup` (or even bundling groups together!). + * + * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * + * NOTE: this takes the entire area of `FlxTilemap`s into account (including "empty" tiles). + * Use `FlxTilemap#overlaps()` if you don't want that. + * + * @param a The first object or group you want to check. + * @param b The second object or group you want to check. Can be the same group as the first. + * @param notify Called on every object that overlaps another. + * @param process Called on every object that overlaps another, determines whether to call the `notify`. + * @return Whether any overlaps were detected. + */ + overload public inline extern function overlap(?notify:(TA, TB)->Void, ?process:(TA, TB)->Bool) + { + return overlapHelper(FlxG.state, null, notify, process); + } + + function overlapHelper(a:FlxBasic, b:Null, notify:Null<(TA, TB)->Void>, ?process:Null<(TA, TB)->Bool>) + { + return FlxCollisionQuadTree.executeOnce(FlxG.worldBounds, FlxG.worldDivisions, a, b, cast notify, cast process); + } + + /** + * Call this function to see if one `FlxObject` collides with another within `FlxG.worldBounds`. + * Can be called with one object and one group, or two groups, or two objects, + * whatever floats your boat! For maximum performance try bundling a lot of objects + * together using a FlxGroup (or even bundling groups together!). + * + * This function just calls `overlap` and presets the `processer` parameter to `separate`. + * To create your own collision logic, write your own `processer` and use `overlap` to set it up. + * **NOTE:** does NOT take sprites' `scrollFactor` into account, all overlaps are checked in world space. + * + * @param a The first object or group you want to check. + * @param b The second object or group you want to check. Can be the same group as the first. + * @param notify Called on every object that overlaps another. + * + * @return Whether any objects were successfully collided/separated. + */ + public inline function collide(?a:FlxBasic, ?b:FlxBasic, ?notify:(TA, TB)->Void) + { + return overlap(a, b, notify, separate); + } + + /** + * Separates 2 overlapping objects. If an object is a tilemap, + * it will separate it from any tiles that overlap it. + * + * @return Whether the objects were overlapping and were separated + */ + public function separate(a:FlxObject, b:FlxObject) + { + return FlxColliderUtil.process(a, b, checkAndSeparate); + } + + static final overlapHelperPoint = FlxPoint.get(); + static final overlapInverseHelperPoint = FlxPoint.get(); + function checkAndSeparate(a:IFlxCollider, b:IFlxCollider) + { + final colliderA = a.getCollider(); + final colliderB = b.getCollider(); + // if (colliderA.overlapsDelta(colliderB)) + // { + colliderA.onBoundsCollide.dispatch(b); + colliderB.onBoundsCollide.dispatch(a); + + if (!colliderA.type.match(SHAPE(_)) || !colliderB.type.match(SHAPE(_))) + return true; + + final overlap = a.computeCollisionOverlap(b, maxOverlap); + + + if (overlap.isZero()) + return false; + + final negativeOverlap = overlap.copyTo(overlapInverseHelperPoint); + + colliderA.onCollide.dispatch(b, overlap); + colliderB.onCollide.dispatch(a, negativeOverlap); + + // seprate x + if (overlap.x != 0) + { + updateTouchingFlagsXHelper(colliderA, colliderB); + separateXHelper(colliderA, colliderB, overlap.x); + } + + // seprate y + if (overlap.y != 0) + { + updateTouchingFlagsYHelper(colliderA, colliderB); + separateYHelper(colliderA, colliderB, overlap.y); + } + + updateObjectFields(a, colliderA); + updateObjectFields(b, colliderB); + + colliderA.onSeparate.dispatch(b, overlap); + colliderB.onSeparate.dispatch(a, negativeOverlap); + negativeOverlap.put(); + + return true; + // } + + // return false; + } + + /** + * Updates the legacy object's fields so they match the collider's, these vars are to preserve + * reflection in existing games + */ + function updateObjectFields(obj:IFlxCollider, collider:FlxCollider) + { + if (obj is FlxObject) + { + final object:FlxObject = cast obj; + @:bypassAccessor object.x = collider.x; + @:bypassAccessor object.y = collider.y; + @:bypassAccessor object.touching = collider.touching; + } + } + + function separateHelper(a:FlxCollider, b:FlxCollider, overlap:FlxPoint) + { + final delta1 = FlxPoint.get(a.x - a.last.x, a.y - a.last.y); + final delta2 = FlxPoint.get(b.x - b.last.x, b.y - b.last.y); + final vel1 = a.velocity; + final vel2 = b.velocity; + + if (!a.immovable && !b.immovable) + { + a.x -= overlap.x * 0.5; + a.y -= overlap.y * 0.5; + b.x += overlap.x * 0.5; + b.y += overlap.y * 0.5; + + final mass1 = a.mass; + final mass2 = b.mass; + final momentum = mass1 * vel1.length + mass2 * vel2.length; + + // TODO: rebound x/y on overlap normal + a.velocity.x = (momentum + a.elasticity * mass2 * (vel2.x - vel1.x)) / (mass1 + mass2); + b.velocity.x = (momentum + b.elasticity * mass1 * (vel1.x - vel2.x)) / (mass1 + mass2); + } + else if (!a.immovable) + { + a.x -= overlap.x; + a.y -= overlap.y; + + // TODO: rebound x/y on overlap normal + a.velocity.x = vel2.x - vel1.x * a.elasticity; + } + else if (!b.immovable) + { + b.x += overlap.x; + b.y += overlap.y; + + // TODO: rebound x/y on overlap normal + b.velocity.x = vel1.x - vel2.x * b.elasticity; + } + } + + function separateXHelper(a:FlxCollider, b:FlxCollider, overlap:Float) + { + final delta1 = a.x - a.last.x; + final delta2 = b.x - b.last.x; + final vel1 = a.velocity.x; + final vel2 = b.velocity.x; + + if (!a.immovable && !b.immovable) + { + a.x -= overlap * 0.5; + b.x += overlap * 0.5; + + final mass1 = a.mass; + final mass2 = b.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + a.velocity.x = (momentum + a.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + b.velocity.x = (momentum + b.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); + } + else if (!a.immovable) + { + a.x -= overlap; + a.velocity.x = vel2 - vel1 * a.elasticity; + } + else if (!b.immovable) + { + b.x += overlap; + b.velocity.x = vel1 - vel2 * b.elasticity; + } + + // use collisionDrag properties to determine whether one object + if (allowCollisionDrag(a.collisionYDrag, a, b) && delta1 > delta2) + a.y += b.y - b.last.y; + else if (allowCollisionDrag(b.collisionYDrag, b, a) && delta2 > delta1) + b.y += a.y - a.last.y; + } + + function separateYHelper(a:FlxCollider, b:FlxCollider, overlap:Float) + { + final delta1 = a.y - a.last.y; + final delta2 = b.y - b.last.y; + final vel1 = a.velocity.y; + final vel2 = b.velocity.y; + + if (!a.immovable && !b.immovable) + { + a.y -= overlap / 2; + b.y += overlap / 2; + + final mass1 = a.mass; + final mass2 = b.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + final newVel1 = (momentum + a.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + final newVel2 = (momentum + b.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); + a.velocity.y = newVel1; + b.velocity.y = newVel2; + } + else if (!a.immovable) + { + a.y -= overlap; + a.velocity.y = vel2 - vel1 * a.elasticity; + } + else if (!b.immovable) + { + b.y += overlap; + b.velocity.y = vel1 - vel2 * b.elasticity; + } + + // use collisionDrag properties to determine whether one object + if (allowCollisionDrag(a.collisionXDrag, a, b) && delta1 > delta2) + a.x += b.x - b.last.x; + else if (allowCollisionDrag(b.collisionXDrag, b, a) && delta2 > delta1) + b.x += a.x - a.last.x; + } + + inline function canCollide(obj:FlxCollider, dir:FlxDirectionFlags) + { + return obj.allowCollisions.has(dir); + } + + function updateTouchingFlagsXHelper(a:FlxCollider, b:FlxCollider) + { + if ((a.x - a.last.x) > (b.x - b.last.x)) + { + a.touching |= RIGHT; + b.touching |= LEFT; + } + else + { + a.touching |= LEFT; + b.touching |= RIGHT; + } + } + + function updateTouchingFlagsYHelper(a:FlxCollider, b:FlxCollider) + { + if ((a.y - a.last.y) > (b.y - b.last.y)) + { + a.touching |= DOWN; + b.touching |= UP; + } + else + { + a.touching |= UP; + b.touching |= DOWN; + } + } + + function allowCollisionDrag(type:FlxCollisionDragType, a:FlxCollider, b:FlxCollider):Bool + { + return b.moves && switch (type) + { + case NEVER: false; + case ALWAYS: true; + case IMMOVABLE: b.immovable; + case HEAVIER: b.immovable || b.mass > a.mass; + } + } +} + +private inline function abs(n:Float) +{ + return n > 0 ? n : -n; +} + +private inline function min(a:Float, b:Float) +{ + return a < b ? a : b; +} \ No newline at end of file diff --git a/flixel/tile/FlxBaseTilemap.hx b/flixel/tile/FlxBaseTilemap.hx index 6619feaaff..5796c961aa 100644 --- a/flixel/tile/FlxBaseTilemap.hx +++ b/flixel/tile/FlxBaseTilemap.hx @@ -5,6 +5,7 @@ import flixel.group.FlxGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.path.FlxPathfinder; +import flixel.physics.FlxCollider; import flixel.system.FlxAssets; import flixel.util.FlxArrayUtil; import flixel.util.FlxCollision; @@ -300,22 +301,39 @@ class FlxBaseTilemap extends FlxObject */ public function isOverlappingTile(object:FlxObject, ?filter:(tile:Tile)->Bool, ?position:FlxPoint):Bool { - throw "overlapsWithCallback must be implemented"; + throw "isOverlappingTile must be implemented"; } /** - * Calls the given function on ever tile that is overlapping the target object + * Calls the given function on every tile that is overlapping the target object * * @param object The object - * @param filter Function that takes a tile and returns whether is satisfies the - * disired condition + * @param func Function that takes a tile * @param position Optional, specify a custom position for the tilemap * @return Whether any overlapping tile was found * @since 5.9.0 */ public function forEachOverlappingTile(object:FlxObject, func:(tile:Tile)->Void, ?position:FlxPoint):Bool { - throw "overlapsWithCallback must be implemented"; + throw "forEachOverlappingTile must be implemented"; + } + + /** + * Calls the given function on every tile that is overlapping the target object's delta rect + * + * **NOTE:** Tiles are iterated in the direction the object moved this frame. For example, if + * `object.last.y` is greater than `object.y`, tiles are checked from bottom to top + * + * @param collider The colliding object + * @param func Function that takes a tile and returns whether is satisfies the + * disired condition + * @return Whether any colliding tile was found + * @see FlxCollision.getDeltaRect + * @since 6.2.0 + */ + public function forEachCollidingTile(collider:IFlxCollider, func:(tile:Tile)->Bool, stopAtFirst = false):Bool + { + throw "forEachCollidingTile must be implemented"; } @:deprecated("overlapsWithCallback is deprecated, use objectOverlapsTiles(object, callback, pos), instead") // 5.9.0 @@ -360,6 +378,7 @@ class FlxBaseTilemap extends FlxObject flixelType = TILEMAP; immovable = true; moves = false; + collider.type = FlxColliderType.TILEMAP; } override function destroy():Void @@ -1616,7 +1635,7 @@ class FlxBaseTilemap extends FlxObject final mapIndex = getMapIndex(point); return tileExists(mapIndex) && getTileData(mapIndex).solid; } - + /** * Get the world coordinates and size of the entire tilemap as a FlxRect. * diff --git a/flixel/tile/FlxTile.hx b/flixel/tile/FlxTile.hx index db546d62b8..8a6aef3fc8 100644 --- a/flixel/tile/FlxTile.hx +++ b/flixel/tile/FlxTile.hx @@ -2,6 +2,7 @@ package flixel.tile; import flixel.FlxObject; import flixel.graphics.frames.FlxFrame; +import flixel.physics.FlxCollider.IFlxCollider; import flixel.tile.FlxTilemap; import flixel.util.FlxDirectionFlags; import flixel.util.FlxSignal; @@ -57,7 +58,11 @@ class FlxTile extends FlxObject * Frame graphic for this tile. */ public var frame:FlxFrame; - + + #if FLX_DEBUG + public var customDebugDraw = false; + #end + /** * Instantiate this new tile object. This is usually called from FlxTilemap.loadMap(). * @@ -107,7 +112,12 @@ class FlxTile extends FlxObject && object.x < x + width && object.y + object.height > y && object.y < y + height - && (filter == null || Std.isOfType(object, filter)); + && canCollide(object); + } + + public function canCollide(object:Any) + { + return filter == null || Std.isOfType(object, filter); } /** diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx new file mode 100644 index 0000000000..4eead299f4 --- /dev/null +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -0,0 +1,312 @@ +package flixel.tile; + +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.math.FlxMatrix; +import flixel.math.FlxPoint; +import flixel.physics.FlxCollider; +import flixel.tile.FlxTilemap; +import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDirectionFlags; +import openfl.display.BitmapData; +import openfl.display.BlendMode; +import openfl.geom.ColorTransform; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + * Used to define the "normal" or orthogonal edges of a slope, + */ +@:forward(up, down, left, right, has, hasAny, and, toString) +enum abstract FlxSlopeEdges(FlxDirectionFlags) +{ + /** ◥ **/ + var NE = cast 0x0110; // UP | RIGHT + + /** ◤ **/ + var NW = cast 0x0101; // UP | LEFT + + /** ◢ **/ + var SE = cast 0x1010; // DOWN | RIGHT + + /** ◣ **/ + var SW = cast 0x1001; // DOWN | LEFT + + var self(get, never):FlxSlopeEdges; + + inline function get_self():FlxSlopeEdges + { + #if (haxe >= version("4.3.0")) + return abstract; + #else + return cast this; + #end + } + + inline public function isSlopingUp() + { + return this.up == this.left; + } + + inline public function getSlopeSign() + { + return isSlopingUp() ? -1 : 1; + } + + /** + * The position of the slopes y intercept relative to the left of the tile, from `0` to `1` + * where `1` is the bottom of the tile and `0` is the top + */ + public function getYIntercept(grade:FlxSlopeGrade):Float + { + return switch [grade, self] + { + case [NONE, _]: + 0; + case [NORMAL, _]: + isSlopingUp() ? 1.0 : 0.0; + case [STEEP (THICK), SE] | [STEEP (THIN ), NW]: 1.0; // slope up + case [STEEP (THIN ), SE] | [STEEP (THICK), NW]: 2.0; // slope up + case [GENTLE(THICK), SW] | [GENTLE(THIN ), NE]: 0.0; // slope down + case [GENTLE(THIN ), SW] | [GENTLE(THICK), NE]: 0.5; // slope down + case [STEEP (THIN ), SW] | [STEEP (THICK), NE]: 0.0; // slope down + case [STEEP (THICK), SW] | [STEEP (THIN ), NE]: -1.0; // slope down + case [GENTLE(THIN ), SE] | [GENTLE(THICK), NW]: 1.0; // slope up + case [GENTLE(THICK), SE] | [GENTLE(THIN ), NW]: 0.5; // slope up + } + } + + public function getSlope(grade:FlxSlopeGrade):Float + { + return getSlopeSign() * switch grade + { + case STEEP(_): 2.0; + case GENTLE(_): 0.5; + case NORMAL: 1.0; + case NONE: 0.0; + } + } + + public function anyAreSolid(dir:FlxDirectionFlags) + { + // return this.not().hasAny(dir); + return this.hasAny(dir); + } +} + +@:using(flixel.tile.FlxTileSlopeUtil) +enum FlxSlopeGrade +{ + STEEP(type:SlopeGradeType); + GENTLE(type:SlopeGradeType); + NORMAL; + NONE; +} + +enum SlopeGradeType +{ + THICK; + THIN; +} + +@:access(flixel.FlxObject) +class FlxTileSlopeUtil +{ + /** + * Used to compute collsiion forces on a sloped tile + * + * @param edges Which edges of the tile are normal + * @param grade The slope of the tile + * @param downV How much downward velocity should be used to kep the object on the ground + * @param result Optional result vector, if `null` a new one is created + */ + static public function computeCollisionOverlap(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, maxOverlap:Float, ?result:FlxPoint) + { + if (grade == NONE) + return FlxColliderUtil.computeCollisionOverlapAabb(tile, object, maxOverlap, result); + + if (result == null) + result = FlxPoint.get(); + + final allowX = FlxG.collision.checkCollisionEdgesX(tile, object); + final allowY = FlxG.collision.checkCollisionEdgesY(tile, object); + if (!allowX && !allowY) + return result.set(0, 0); + + // only X + if (allowX && !allowY) + { + final overlapX = computeCollisionOverlapX(tile, object, edges, grade); + return result.set(Math.isFinite(overlapX) ? overlapX : 0, 0); + } + + // only Y + if (!allowX && allowY) + { + final overlapY = computeCollisionOverlapY(tile, object, edges, grade); + return result.set(0, Math.isFinite(overlapY) ? overlapY : 0); + } + + result.set(computeCollisionOverlapX(tile, object, edges, grade), computeCollisionOverlapY(tile, object, edges, grade)); + + if (abs(result.x) > min(tile.width, object.width)) + result.x = Math.POSITIVE_INFINITY; + + if (abs(result.y) > min(tile.height, object.height)) + result.y = Math.POSITIVE_INFINITY; + + // separate on the smaller axis + if (Math.isFinite(result.x) || Math.isFinite(result.y)) + { + if (abs(result.x) > abs(result.y)) + result.x = 0; + else + result.y = 0; + } + else + result.set(0, 0); + + return result; + } + + static function checkHitSolidWallX(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges) + { + final solidCollisions = edges.and(FlxG.collision.getCollisionEdgesX(tile, object)); + return (solidCollisions.right && object.last.x >= tile.x + tile.width) + || (solidCollisions.left && object.last.x + object.width <= tile.x); + } + + static function checkHitSolidWallY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges) + { + final solidCollisions = edges.and(FlxG.collision.getCollisionEdgesY(tile, object)); + return (solidCollisions.down && object.last.y >= tile.y + tile.height) + || (solidCollisions.up && object.last.y + object.height <= tile.y); + } + + static public function computeCollisionOverlapX(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + { + final overlapY = computeSlopeOverlapY(tile, object, edges, grade); + // check if they're hitting the solid edges + if (overlapY != 0 && checkHitSolidWallX(tile, object, edges) && tile.bounds.overlaps(object.bounds)) + return FlxColliderUtil.computeCollisionOverlapXAabb(tile, object); + + // let y separate + return Math.POSITIVE_INFINITY; + } + + static public function computeCollisionOverlapY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + { + if (checkHitSolidWallY(tile, object, edges) && tile.bounds.overlaps(object.bounds)) + return FlxColliderUtil.computeCollisionOverlapYAabb(tile, object); + + return computeSlopeOverlapY(tile, object, edges, grade); + } + + static function computeSlopeOverlapY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade):Float + { + final solidBottom = edges.down; + final slope = edges.getSlope(grade); + final useLeftCorner = slope > 0 == solidBottom; + final objX = useLeftCorner ? max(tile.x, object.x) : min(tile.x + tile.width, object.x + object.width); + + // classic slope forumla y = mx + b + final slopeY = getSlopeYAtHelper(tile, objX, slope, edges.getYIntercept(grade)); + + // Check if y intercept is outside of this tile + // TODO: + final isOutsideTile = !tile.bounds.overlaps(object.bounds); + + final isOutsideTileY = (slopeY < tile.y || slopeY >= tile.y + tile.height); + // final isOutsideTile = (slopeY < tile.y || slopeY >= tile.y + tile.height) + // && (useLeftCorner ? object.x + object.width < tile.x : object.x >= tile.x + tile.width); + + if (isOutsideTile) + return 0; + + // check bottom + if (solidBottom && object.y + object.height > slopeY) + return slopeY - (object.y + object.height); + + if (!solidBottom && object.y < slopeY) + return slopeY - object.y; + + return 0; + } + + + inline static var GLUE_SNAP = 2; + // public static function applyGlueDown(tile:FlxTile, object:FlxObject, glueDownVelocity:Float, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + public static function applyGlueDown(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, glueDownVelocity:Float, snapping = 1.0) + { + if (glueDownVelocity <= 0) + return; + + // final slope = edges.getSlope(grade); + // final b = tile.y + edges.getYIntercept(grade) * tile.height; + // final isInTile = (object.x < tile.x + tile.width && object.x + object.width > tile.x); + // if (glueDownVelocity > 0 && isInTile && FlxG.collision.getCollisionEdgesY(tile, object).up) + // if (glueDownVelocity > 0 && isInTile && overlap.y < 0) + // if (isOnSlope(tile, object)) + if (FlxG.collision.getCollisionEdgesY(tile, object).has(UP)) + { + final slope = edges.getSlope(grade); + final yInt = edges.getYIntercept(grade) * tile.height; + // final useLeftCorner = slope > 0; + // final objectLastX = useLeftCorner ? object.last.x : object.last.x + object.width; + final objectLastX = slope > 0 ? object.last.x : object.last.x + object.width; + // final lastY = slope * (objectLastX - tile.x) + b; + // function round(n:Float) { return Math.round(n * 100) / 100; } + // FlxG.watch.addQuick("down", '${round(object.last.y + object.height + 1)} > ${round(lastY)}'); + if (object.last.y < getSlopeYAtHelper(tile, objectLastX, slope, yInt)) + { + object.velocity.y = glueDownVelocity; + if (isInSlopeHelper(tile, object, edges.down, slope, yInt, snapping)) + object.touching = object.touching.with(FLOOR); + } + // FlxG.watch.addQuick("v", round(object.velocity.y)); + } + } + + static function getSlopeYAt(tile:FlxCollider, worldX:Float, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + { + return getSlopeYAtHelper(tile, worldX, edges.getSlope(grade), edges.getYIntercept(grade)); + } + + static inline function getSlopeYAtHelper(tile:FlxCollider, worldX:Float, slope:Float, yInt:Float) + { + return slope * (worldX - tile.x) + tile.y + (yInt * tile.height); + } + + static function isInSlope(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, margin:Float = 0) + { + return isInSlopeHelper(tile, object, edges.down, edges.getSlope(grade), edges.getYIntercept(grade), margin); + } + + static inline function isInSlopeHelper(tile:FlxCollider, object:FlxCollider, solidBottom:Bool, slope:Float, yInt:Float, margin:Float = 0) + { + final useLeftCorner = slope > 0; + final objX = useLeftCorner ? object.x : object.x + object.width; + final slopeY = getSlopeYAtHelper(tile, objX, slope, yInt); + + // check bottom + return solidBottom + ? (object.y + object.height + margin > slopeY) + : (object.y - margin < slopeY); + } +} + +private inline function abs(n:Float) +{ + return n > 0 ? n : -n; +} + +private inline function min(a:Float, b:Float) +{ + return a < b ? a : b; +} + +private inline function max(a:Float, b:Float) +{ + return a > b ? a : b; +} \ No newline at end of file diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index 80aecfc4d9..c229b4f8e8 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -14,8 +14,10 @@ import flixel.math.FlxMath; import flixel.math.FlxMatrix; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.physics.FlxCollider.IFlxCollider; import flixel.system.FlxAssets.FlxShader; import flixel.system.FlxAssets.FlxTilemapGraphicAsset; +import flixel.util.FlxAxes; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; import flixel.util.FlxDirectionFlags; @@ -27,6 +29,7 @@ import openfl.geom.ColorTransform; import openfl.geom.Point; import openfl.geom.Rectangle; +using flixel.util.FlxCollision; using flixel.util.FlxColorTransformUtil; #if html5 @@ -591,7 +594,18 @@ class FlxTypedTilemap extends FlxBaseTilemap for (column in 0...screenColumns) { final tile = getTileData(columnIndex); - + + #if FLX_DEBUG + if (tile.customDebugDraw) + { + tile.orient(column, row); + tile.drawDebugOnCamera(camera); + + columnIndex++; + continue; + } + #end + if (tile != null && tile.visible && !tile.ignoreDrawDebug) { rect.x = _helperPoint.x + (columnIndex % widthInTiles) * rect.width; @@ -788,6 +802,78 @@ class FlxTypedTilemap extends FlxBaseTilemap return result; } + override function forEachCollidingTile(object:IFlxCollider, func:(tile:Tile)->Bool, stopAtFirst = false):Bool + { + final collider = object.getCollider(); + if (object is FlxObject) + { + final obj:FlxObject = cast object; + function filter(tile:Tile) + { + final overlapping = tile.overlapsObject(obj); + if (overlapping && tile.callbackFunction != null) + tile.callbackFunction(tile, obj); + + final result = tile.canCollide(object) && (func == null || func(tile)); + + if (result) + tile.onCollide.dispatch(tile, obj); + + return result; + } + + final reverse = FlxAxes.fromBools(collider.last.x > collider.x, collider.last.y > collider.y); + return forEachTileOverlappingRect(collider.getDeltaRect(FlxRect.weak()), filter, reverse, stopAtFirst); + } + + function filter(tile:Tile) + { + return func == null || func(tile); + } + + final reverse = FlxAxes.fromBools(collider.last.x > collider.x, collider.last.y > collider.y); + return forEachTileOverlappingRect(collider.getDeltaRect(FlxRect.weak()), filter, reverse, stopAtFirst); + } + + function forEachTileOverlappingRect(rect:FlxRect, filter:(tile:Tile)->Bool, reverse:FlxAxes, stopAtFirst:Bool):Bool + { + inline function bindInt(value:Int, min:Int, max:Int) + { + return Std.int(FlxMath.bound(value, min, max)); + } + + // Figure out what tiles we need to check against, and bind them by the map edges + final minTileX:Int = bindInt(Math.floor((rect.x - this.x) / scaledTileWidth), 0, widthInTiles); + final minTileY:Int = bindInt(Math.floor((rect.y - this.y) / scaledTileHeight), 0, heightInTiles); + final maxTileX:Int = bindInt(Math.ceil((rect.right - this.x) / scaledTileWidth), 0, widthInTiles); + final maxTileY:Int = bindInt(Math.ceil((rect.bottom - this.y) / scaledTileHeight), 0, heightInTiles); + rect.putWeak(); + + var result = false; + for (r in 0...maxTileY - minTileY) + { + final row = reverse.y ? maxTileY - r - 1 : minTileY + r; + for (c in 0...maxTileX - minTileX) + { + final column = reverse.x ? maxTileX - c - 1 : minTileX + c; + final tile = getTileData(column, row); + if (tile == null) + continue; + + tile.orientAt(this.x, this.y, column, row); + if (filter(tile)) + { + if (stopAtFirst) + return true; + + result = true; + } + } + } + + return result; + } + override function objectOverlapsTiles(object:TObj, ?callback:(Tile, TObj)->Bool, ?position:FlxPoint, isCollision = true):Bool { var results = false; diff --git a/flixel/util/FlxCollision.hx b/flixel/util/FlxCollision.hx index df864ca9c1..45e2eca6d4 100644 --- a/flixel/util/FlxCollision.hx +++ b/flixel/util/FlxCollision.hx @@ -1,7 +1,5 @@ package flixel.util; -import openfl.display.BitmapData; -import openfl.geom.Rectangle; import flixel.FlxCamera; import flixel.FlxG; import flixel.FlxSprite; @@ -12,6 +10,8 @@ import flixel.math.FlxMatrix; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.tile.FlxTileblock; +import openfl.display.BitmapData; +import openfl.geom.Rectangle; /** * FlxCollision @@ -370,4 +370,40 @@ class FlxCollision { return calcRectEntry(rect, end, start, result); } + + /** + * The smallest rect that contains the object in it's current and last position + * + * @param rect Optional point to store the result, if `null` one is created + * @since 6.2.0 + */ + public static function getDeltaRect(object:FlxObject, ?rect:FlxRect) + { + if (rect == null) + rect = FlxRect.get(); + + rect.x = object.x > object.last.x ? object.last.x : object.x; + rect.right = (object.x > object.last.x ? object.x : object.last.x) + object.width; + rect.y = object.y > object.last.y ? object.last.y : object.y; + rect.bottom = (object.y > object.last.y ? object.y : object.last.y) + object.height; + + return rect; + } + + /** + * Checks whether the two objects' delta rects overlap + * @see FlxCollision.getDeltaRect + * @since 6.2.0 + */ + public static function overlapsDelta(object1:FlxObject, object2:FlxObject) + { + final rect1 = getDeltaRect(object1); + final rect2 = getDeltaRect(object2); + + final result = rect1.overlaps(rect2); + + rect1.put(); + rect2.put(); + return result; + } } diff --git a/flixel/util/FlxDirection.hx b/flixel/util/FlxDirection.hx index c389351294..1bab9a1bb9 100644 --- a/flixel/util/FlxDirection.hx +++ b/flixel/util/FlxDirection.hx @@ -37,6 +37,17 @@ enum abstract FlxDirection(Int) } } + public inline function flip():FlxDirectionFlags + { + return switch self + { + case RIGHT: LEFT; + case LEFT: RIGHT; + case UP: DOWN; + case DOWN: UP; + } + } + @:deprecated("implicit cast from FlxDirection to Int is deprecated, use toInt()") @:to inline function toIntImplicit() diff --git a/flixel/util/FlxDirectionFlags.hx b/flixel/util/FlxDirectionFlags.hx index 86d7b6ab49..6893276a7a 100644 --- a/flixel/util/FlxDirectionFlags.hx +++ b/flixel/util/FlxDirectionFlags.hx @@ -93,6 +93,14 @@ enum abstract FlxDirectionFlags(Int) public var right(get, never):Bool; inline function get_right() return has(RIGHT); + /** A new instance with only the left and right flags **/ + public var x(get, never):FlxDirectionFlags; + inline function get_x() return self & WALL; + + /** A new instance with only the up and down flags **/ + public var y(get, never):FlxDirectionFlags; + inline function get_y() return without(WALL); + inline function new(value:Int) { this = value; @@ -115,25 +123,40 @@ enum abstract FlxDirectionFlags(Int) } /** - * Creates a new `FlxDirections` that includes the supplied directions. + * Creates a new `FlxDirectionFlags` that includes the supplied directions. */ public inline function with(dir:FlxDirectionFlags):FlxDirectionFlags { return fromInt(this | dir.toInt()); } - + /** - * Creates a new `FlxDirections` that excludes the supplied directions. + * Creates a new `FlxDirectionFlags` that excludes the supplied directions. */ public inline function without(dir:FlxDirectionFlags):FlxDirectionFlags { return fromInt(this & ~dir.toInt()); } + public function and(dir:FlxDirectionFlags):FlxDirectionFlags + { + return fromInt(this & dir.toInt()); + } + + public function or(dir:FlxDirectionFlags):FlxDirectionFlags + { + return fromInt(this | dir.toInt()); + } + public inline function not():FlxDirectionFlags { return fromInt((~this & ANY.toInt())); } + + public inline function flip():FlxDirectionFlags + { + return fromBools(right, left, down, up); + } @:deprecated("implicit cast from FlxDirectionFlags to Int is deprecated, use toInt") @:to @@ -196,16 +219,14 @@ enum abstract FlxDirectionFlags(Int) return fromInt(dir.toInt()); } - @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead")// Expose int operators - @:op(A & B) static function and(a:FlxDirectionFlags, b:FlxDirectionFlags):FlxDirectionFlags; - @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead") - @:op(A | B) static function or(a:FlxDirectionFlags, b:FlxDirectionFlags):FlxDirectionFlags; + @:op(A & B) static function andOp(a:FlxDirectionFlags, b:FlxDirectionFlags):FlxDirectionFlags; + @:op(A | B) static function orOp(a:FlxDirectionFlags, b:FlxDirectionFlags):FlxDirectionFlags; @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead") - @:op(A > B) static function gt(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; + @:op(A > B) static function gtOp(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead") - @:op(A < B) static function lt(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; + @:op(A < B) static function ltOp(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead") - @:op(A >= B) static function gte(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; + @:op(A >= B) static function gteOp(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; @:deprecated("FlxDirectionFlags operators are deprecated, use has(), instead") - @:op(A <= B) static function lte(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; + @:op(A <= B) static function lteOp(a:FlxDirectionFlags, b:FlxDirectionFlags):Bool; } diff --git a/haxelib.json b/haxelib.json index 081932d9eb..daacbd2758 100644 --- a/haxelib.json +++ b/haxelib.json @@ -4,7 +4,7 @@ "license": "MIT", "tags": ["game", "openfl", "flash", "html5", "neko", "cpp", "android", "ios", "cross"], "description": "HaxeFlixel is a 2D game engine based on OpenFL that delivers cross-platform games.", - "version": "6.1.0", + "version": "6.2.0", "releasenote": "Various improvements to debug tools", "contributors": ["haxeflixel", "Gama11", "GeoKureli"], "dependencies": {