From 3c1c4e64defd569382810e2323d8fd3e5ed52e94 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Tue, 6 May 2025 16:09:13 -0500 Subject: [PATCH 01/14] add FlxG.collision --- flixel/FlxG.hx | 7 + flixel/system/frontEnds/CollisionFrontEnd.hx | 494 +++++++++++++++++++ 2 files changed, 501 insertions(+) create mode 100644 flixel/system/frontEnds/CollisionFrontEnd.hx diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index baad5bdbb7..bc0d315000 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -9,6 +9,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 +335,12 @@ class FlxG * @since 5.9.0 */ public static var assets(default, null):AssetFrontEnd = new AssetFrontEnd(); + + /** + * Contains helper functions relating to retrieving assets + * @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. diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx new file mode 100644 index 0000000000..e28868f391 --- /dev/null +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -0,0 +1,494 @@ +package flixel.system.frontEnds; + +import flixel.FlxG; +import flixel.FlxObject; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.tile.FlxBaseTilemap; +import flixel.util.FlxDirectionFlags; + +@:access(flixel.FlxObject) +class CollisionFrontEnd +{ + public function new () {} + + /** + * This value dictates the maximum number of pixels two objects have to intersect + * before collision stops trying to separate them. + * Don't modify this unless your objects are passing through each other. + */ + public var maxOverlap:Float = 4; + + /** + * 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>) + { + if (b == a) + b = null; + + FlxQuadTree.divisions = FlxG.worldDivisions;// TODO + final quadTree = FlxQuadTree.recycle(FlxG.worldBounds.x, FlxG.worldBounds.y, FlxG.worldBounds.width, FlxG.worldBounds.height); + quadTree.load(a, b, cast notify, cast process); + final result:Bool = quadTree.execute(); + quadTree.destroy(); + return result; + } + + /** + * 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 `ProcessCallback` parameter to `FlxObject.separate`. + * To create your own collision logic, write your own `ProcessCallback` and use `FlxG.overlap` to set it up. + * NOTE: does NOT take objects' `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(object1:FlxObject, object2:FlxObject) + { + return processCheckTilemap(object1, object2, checkAndSeparate); + + // final separatedX = separateX(object1, object2); + // final separatedY = separateY(object1, object2); + // return separatedX || separatedY; + } + + public function separateX(object1:FlxObject, object2:FlxObject) + { + return processCheckTilemap(object1, object2, checkAndSeparateX); + } + + public function separateY(object1:FlxObject, object2:FlxObject) + { + return processCheckTilemap(object1, object2, checkAndSeparateY); + } + + /** + * Internal elper that determines whether either object is a tilemap, determines + * which tiles are overlapping and calls the appropriate separator + * + * @param func The process you wish to call with both objects, or between tiles, + * + * @param isCollision Does nothing, if both objects are immovable + * @return The result of whichever separator was used + */ + function processCheckTilemap(object1:FlxObject, object2:FlxObject, func:(FlxObject, FlxObject)->Bool, + ?position:FlxPoint, isCollision = true):Bool + { + // two immovable objects cannot collide + if (isCollision && object1.immovable && object2.immovable) + return false; + + // If one of the objects is a tilemap, just pass it off. + if (object1.flixelType == TILEMAP) + { + final tilemap:FlxBaseTilemap = cast object1; + // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap + function recurseProcess(tile, _) + { + // Keep tile as first arg + return processCheckTilemap(tile, object2, func, position, isCollision); + } + return tilemap.objectOverlapsTiles(object2, recurseProcess, position); + } + else if (object2.flixelType == TILEMAP) + { + final tilemap:FlxBaseTilemap = cast object2; + // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap + function recurseProcess(tile, _) + { + // Keep tile as second arg + return processCheckTilemap(object1, tile, func, position, isCollision); + } + return tilemap.objectOverlapsTiles(object1, recurseProcess, position); + } + + return func(object1, object2); + } + + function checkAndSeparate(object1:FlxObject, object2:FlxObject) + { + if (checkCollision(object1, object2)) + { + final overlapX = computeCollisionOverlapXHelper(object1, object2); + final overlapY = computeCollisionOverlapYHelper(object1, object2); + + if ((overlapX == 0 && overlapY == 0) || (abs(overlapX) > maxOverlap && abs(overlapY) > maxOverlap)) + return false; + + if (abs(overlapX) < abs(overlapY)) + { + updateTouchingFlagsXHelper(object1, object2); + separateXHelper(object1, object2, overlapX); + } + else + { + updateTouchingFlagsYHelper(object1, object2); + separateYHelper(object1, object2, overlapY); + } + + return true; + } + + return false; + } + + function checkAndSeparateX(object1:FlxObject, object2:FlxObject) + { + if (checkCollision(object1, object2)) + { + final overlapX = computeCollisionOverlapXHelper(object1, object2); + if (abs(overlapX) > maxOverlap) + return false; + + separateXHelper(object1, object2, overlapX); + return true; + } + + return false; + } + + function checkAndSeparateY(object1:FlxObject, object2:FlxObject) + { + if (checkCollision(object1, object2)) + { + final overlapY = computeCollisionOverlapYHelper(object1, object2); + if (abs(overlapY) > maxOverlap) + return false; + + separateYHelper(object1, object2, overlapY); + return true; + } + + return false; + } + + public function checkCollision(object1:FlxObject, object2:FlxObject) + { + return checkSweptRects(object1, object2) + && (checkXCollisionHelper(object1, object2) || checkYCollisionHelper(object1, object2)); + } + + /** + * Internal function use to determine if two objects may cross path, + * by comparing the bounds they occupy this frame + */ + function checkSweptRects(object1:FlxObject, object2:FlxObject) + { + final rect1 = getSweptRect(object1); + final rect2 = getSweptRect(object2); + + final result = rect1.overlaps(rect2); + + rect1.put(); + rect2.put(); + return result; + } + + function getSweptRect(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; + } + + function separateXHelper(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final delta1 = object1.x - object1.last.x; + final delta2 = object2.x - object2.last.x; + final vel1 = object1.velocity.x; + final vel2 = object2.velocity.x; + + if (!object1.immovable && !object2.immovable) + { + #if FLX_4_LEGACY_COLLISION + legacySeparateX(object1, object2, overlap); + #else + object1.x -= overlap * 0.5; + object2.x += overlap * 0.5; + + final mass1 = object1.mass; + final mass2 = object2.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + object1.velocity.x = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + object2.velocity.x = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); + #end + } + else if (!object1.immovable) + { + object1.x -= overlap; + object1.velocity.x = vel2 - vel1 * object1.elasticity; + } + else if (!object2.immovable) + { + object2.x += overlap; + object2.velocity.x = vel1 - vel2 * object2.elasticity; + } + + // use collisionDrag properties to determine whether one object + if (allowCollisionDrag(object1.collisionYDrag, object1, object2) && delta1 > delta2) + object1.y += object2.y - object2.last.y; + else if (allowCollisionDrag(object2.collisionYDrag, object2, object1) && delta2 > delta1) + object2.y += object1.y - object1.last.y; + } + + function separateYHelper(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final delta1 = object1.y - object1.last.y; + final delta2 = object2.y - object2.last.y; + final vel1 = object1.velocity.y; + final vel2 = object2.velocity.y; + + if (!object1.immovable && !object2.immovable) + { + #if FLX_4_LEGACY_COLLISION + legacySeparateY(object1, object2, overlap); + #else + object1.y -= overlap / 2; + object2.y += overlap / 2; + + final mass1 = object1.mass; + final mass2 = object2.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + final newVel1 = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + final newVel2 = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); + object1.velocity.y = newVel1; + object2.velocity.y = newVel2; + #end + } + else if (!object1.immovable) + { + object1.y -= overlap; + object1.velocity.y = vel2 - vel1 * object1.elasticity; + } + else if (!object2.immovable) + { + object2.y += overlap; + object2.velocity.y = vel1 - vel2 * object2.elasticity; + } + + // use collisionDrag properties to determine whether one object + if (allowCollisionDrag(object1.collisionXDrag, object1, object2) && delta1 > delta2) + object1.x += object2.x - object2.last.x; + else if (allowCollisionDrag(object2.collisionXDrag, object2, object1) && delta2 > delta1) + object2.x += object1.x - object1.last.x; + } + + inline function canObjectCollide(obj:FlxObject, dir:FlxDirectionFlags) + { + return obj.allowCollisions.has(dir); + } + + function checkXCollisionHelper(object1:FlxObject, object2:FlxObject) + { + final deltaDiff = (object1.x - object1.last.x) - (object2.x - object2.last.x); + return (deltaDiff > 0 && canObjectCollide(object1, RIGHT) && canObjectCollide(object2, LEFT)) + || (deltaDiff < 0 && canObjectCollide(object1, LEFT) && canObjectCollide(object2, RIGHT)); + } + + function checkYCollisionHelper(object1:FlxObject, object2:FlxObject) + { + final deltaDiff = (object1.y - object1.last.y) - (object2.y - object2.last.y); + return (deltaDiff > 0 && canObjectCollide(object1, DOWN) && canObjectCollide(object2, UP)) + || (deltaDiff < 0 && canObjectCollide(object1, UP) && canObjectCollide(object2, DOWN)); + } + + /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns (0, 0) **/ + public function computeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) + { + if (result == null) + result = FlxPoint.get(); + + if (checkCollision(object1, object2)) + result.set(computeCollisionOverlapXHelper(object1, object2), computeCollisionOverlapYHelper(object1, object2)); + + return result; + } + + /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns 0 **/ + public function computeCollisionOverlapX(object1:FlxObject, object2:FlxObject) + { + if (checkCollision(object1, object2)) + return computeCollisionOverlapXHelper(object1, object2); + + return 0; + } + + /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns 0 **/ + public function computeCollisionOverlapY(object1:FlxObject, object2:FlxObject) + { + if (checkCollision(object1, object2)) + return computeCollisionOverlapYHelper(object1, object2); + + return 0; + } + + function computeCollisionOverlapXHelper(object1:FlxObject, object2:FlxObject) + { + if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) + return object1.x + object1.width - object2.x; + + return object1.x - object2.width - object2.x; + } + + function computeCollisionOverlapYHelper(object1:FlxObject, object2:FlxObject) + { + if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) + return object1.y + object1.height - object2.y; + + return object1.y - object2.height - object2.y; + } + + function updateTouchingFlagsXHelper(object1:FlxObject, object2:FlxObject) + { + if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) + { + object1.touching |= RIGHT; + object2.touching |= LEFT; + } + else + { + object1.touching |= LEFT; + object2.touching |= RIGHT; + } + } + + function updateTouchingFlagsYHelper(object1:FlxObject, object2:FlxObject) + { + if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) + { + object1.touching |= DOWN; + object2.touching |= UP; + } + else + { + object1.touching |= UP; + object2.touching |= DOWN; + } + } + + function allowCollisionDrag(type:CollisionDragType, object1:FlxObject, object2:FlxObject):Bool + { + return object2.active && object2.moves && switch (type) + { + case NEVER: false; + case ALWAYS: true; + case IMMOVABLE: object2.immovable; + case HEAVIER: object2.immovable || object2.mass > object1.mass; + } + } + + /** + * The separateX that existed before HaxeFlixel 5.0, preserved for anyone who + * needs to use it in an old project. Does not preserve momentum, avoid if possible + */ + static inline function legacySeparateX(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final vel1 = object1.velocity.x; + final vel2 = object2.velocity.x; + final mass1 = object1.mass; + final mass2 = object2.mass; + object1.x = object1.x - (overlap * 0.5); + object2.x += overlap * 0.5; + + var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); + var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); + final average = (newVel1 + newVel2) * 0.5; + newVel1 -= average; + newVel2 -= average; + object1.velocity.x = average + (newVel1 * object1.elasticity); + object2.velocity.x = average + (newVel2 * object2.elasticity); + } + + /** + * The separateY that existed before HaxeFlixel 5.0, preserved for anyone who + * needs to use it in an old project. Does not preserve momentum, avoid if possible + */ + static inline function legacySeparateY(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final vel1 = object1.velocity.y; + final vel2 = object2.velocity.y; + final mass1 = object1.mass; + final mass2 = object2.mass; + object1.y = object1.y - (overlap * 0.5); + object2.y += overlap * 0.5; + + var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); + var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); + final average = (newVel1 + newVel2) * 0.5; + newVel1 -= average; + newVel2 -= average; + object1.velocity.y = average + (newVel1 * object1.elasticity); + object2.velocity.y = average + (newVel2 * object2.elasticity); + } +} + +inline function abs(n:Float) +{ + return n > 0 ? n : -n; +} \ No newline at end of file From 043b1ce3014f806e6ca2cd6d8e0d3f009b08b148 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 8 May 2025 09:53:15 -0500 Subject: [PATCH 02/14] add tilemap.forEachCollidingTile --- flixel/system/frontEnds/CollisionFrontEnd.hx | 51 ++++++------------- flixel/tile/FlxBaseTilemap.hx | 27 ++++++++-- flixel/tile/FlxTilemap.hx | 53 ++++++++++++++++++++ flixel/util/FlxCollision.hx | 40 ++++++++++++++- 4 files changed, 129 insertions(+), 42 deletions(-) diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index e28868f391..7ff63ad0ad 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -7,6 +7,8 @@ import flixel.math.FlxRect; import flixel.tile.FlxBaseTilemap; import flixel.util.FlxDirectionFlags; +using flixel.util.FlxCollision; + @:access(flixel.FlxObject) class CollisionFrontEnd { @@ -131,35 +133,34 @@ class CollisionFrontEnd * @param isCollision Does nothing, if both objects are immovable * @return The result of whichever separator was used */ - function processCheckTilemap(object1:FlxObject, object2:FlxObject, func:(FlxObject, FlxObject)->Bool, - ?position:FlxPoint, isCollision = true):Bool + function processCheckTilemap(object1:FlxObject, object2:FlxObject, func:(FlxObject, FlxObject)->Bool):Bool { // two immovable objects cannot collide - if (isCollision && object1.immovable && object2.immovable) + if (object1.immovable && object2.immovable) return false; // If one of the objects is a tilemap, just pass it off. if (object1.flixelType == TILEMAP) { - final tilemap:FlxBaseTilemap = cast object1; + final tilemap:FlxBaseTilemap = cast object1; // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap - function recurseProcess(tile, _) + function recurseProcess(tile) { // Keep tile as first arg - return processCheckTilemap(tile, object2, func, position, isCollision); + return processCheckTilemap(tile, object2, func); } - return tilemap.objectOverlapsTiles(object2, recurseProcess, position); + return tilemap.forEachCollidingTile(object2, recurseProcess); } else if (object2.flixelType == TILEMAP) { - final tilemap:FlxBaseTilemap = cast object2; + final tilemap:FlxBaseTilemap = cast object2; // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap - function recurseProcess(tile, _) + function recurseProcess(tile) { // Keep tile as second arg - return processCheckTilemap(object1, tile, func, position, isCollision); + return processCheckTilemap(object1, tile, func); } - return tilemap.objectOverlapsTiles(object1, recurseProcess, position); + return tilemap.forEachCollidingTile(object1, recurseProcess); } return func(object1, object2); @@ -224,7 +225,7 @@ class CollisionFrontEnd public function checkCollision(object1:FlxObject, object2:FlxObject) { - return checkSweptRects(object1, object2) + return checkDeltaOverlaps(object1, object2) && (checkXCollisionHelper(object1, object2) || checkYCollisionHelper(object1, object2)); } @@ -232,29 +233,9 @@ class CollisionFrontEnd * Internal function use to determine if two objects may cross path, * by comparing the bounds they occupy this frame */ - function checkSweptRects(object1:FlxObject, object2:FlxObject) + function checkDeltaOverlaps(object1:FlxObject, object2:FlxObject) { - final rect1 = getSweptRect(object1); - final rect2 = getSweptRect(object2); - - final result = rect1.overlaps(rect2); - - rect1.put(); - rect2.put(); - return result; - } - - function getSweptRect(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; + return object1.overlapsDelta(object2); } function separateXHelper(object1:FlxObject, object2:FlxObject, overlap:Float) @@ -488,7 +469,7 @@ class CollisionFrontEnd } } -inline function abs(n:Float) +private inline function abs(n:Float) { return n > 0 ? n : -n; } \ No newline at end of file diff --git a/flixel/tile/FlxBaseTilemap.hx b/flixel/tile/FlxBaseTilemap.hx index 6619feaaff..caa8e81bdc 100644 --- a/flixel/tile/FlxBaseTilemap.hx +++ b/flixel/tile/FlxBaseTilemap.hx @@ -300,22 +300,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 object The 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(object:FlxObject, func:(tile:Tile)->Bool):Bool + { + throw "forEachCollidingTile must be implemented"; } @:deprecated("overlapsWithCallback is deprecated, use objectOverlapsTiles(object, callback, pos), instead") // 5.9.0 diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index 80aecfc4d9..c58507af4f 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -16,6 +16,7 @@ import flixel.math.FlxPoint; import flixel.math.FlxRect; 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 +28,7 @@ import openfl.geom.ColorTransform; import openfl.geom.Point; import openfl.geom.Rectangle; +using flixel.util.FlxCollision; using flixel.util.FlxColorTransformUtil; #if html5 @@ -788,6 +790,57 @@ class FlxTypedTilemap extends FlxBaseTilemap return result; } + override function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool):Bool + { + function filter(tile) + { + // return true, since an overlapping tile was found + return tile.overlapsObject(object) && (func == null || func(tile)); + } + + final reverse = FlxAxes.fromBools(object.last.x > object.x, object.last.y > object.y); + return forEachTileOverlappingRect(object.getDeltaRect(FlxRect.weak()), filter, reverse, false); + } + + 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; + } } From 1897c7ffa3a9de4ef78526be22c5d34cc70852d2 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Mon, 19 May 2025 10:34:44 -0500 Subject: [PATCH 03/14] major changes + work with slopes --- flixel/FlxG.hx | 2 +- flixel/FlxObject.hx | 74 ++- flixel/system/frontEnds/CollisionFrontEnd.hx | 156 +++--- flixel/tile/FlxBaseTilemap.hx | 16 +- flixel/tile/FlxTile.hx | 6 +- flixel/tile/FlxTileSlopeUtil.hx | 490 +++++++++++++++++++ flixel/tile/FlxTilemap.hx | 24 +- flixel/util/FlxDirection.hx | 11 + flixel/util/FlxDirectionFlags.hx | 43 +- haxelib.json | 2 +- 10 files changed, 699 insertions(+), 125 deletions(-) create mode 100644 flixel/tile/FlxTileSlopeUtil.hx diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index bc0d315000..74efc86e71 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -337,7 +337,7 @@ class FlxG public static var assets(default, null):AssetFrontEnd = new AssetFrontEnd(); /** - * Contains helper functions relating to retrieving assets + * Contains helper functions relating to collision * @since 6.2.0 */ public static var collision(default, null):CollisionFrontEnd = new CollisionFrontEnd(); diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index 756d23a498..def810d16b 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -1,6 +1,8 @@ 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; @@ -549,6 +551,63 @@ class FlxObject extends FlxBasic return overlap; } + /** + * Helper to compute the overlap of two objects, this is used when + * `object1.computeCollisionOverlap(object2)` is called on two objects + */ + public static function defaultComputeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) + { + if (result == null) + result = FlxPoint.get(); + + result.set(defaultComputeCollisionOverlapX(object1, object2), defaultComputeCollisionOverlapY(object1, object2)); + + function abs(n:Float) return n < 0 ? -n : n; + + final absX = abs(result.x); + final absY = abs(result.y); + + // separate on the smaller axis + if (absX > absY) + { + result.x = 0; + if (absY > SEPARATE_BIAS) + result.y = 0; + } + else + { + result.y = 0; + if (absX > SEPARATE_BIAS) + result.x = 0; + } + + return result; + } + + /** + * Helper to compute the X overlap of two objects, this is used when + * `object1.computeCollisionOverlapX(object2)` is called on two objects + */ + public static function defaultComputeCollisionOverlapX(object1:FlxObject, object2:FlxObject) + { + if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) + return object1.x + object1.width - object2.x; + + return object1.x - object2.width - object2.x; + } + + /** + * Helper to compute the Y overlap of two objects, this is used when + * `object1.computeCollisionOverlapY(object2)` is called on two objects + */ + public static function defaultComputeCollisionOverlapY(object1:FlxObject, object2:FlxObject) + { + if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) + return object1.y + object1.height - object2.y; + + return object1.y - object2.height - object2.y; + } + /** * X position of the upper left corner of this object in world space. */ @@ -913,7 +972,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 +980,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,12 +997,17 @@ 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 { return overlaps(objectOrGroup, inScreenSpace, camera); } + + public function computeCollisionOverlap(object:FlxObject, ?result:FlxPoint):FlxPoint + { + return defaultComputeCollisionOverlap(this, object, result); + } /** * Checks to see if this `FlxObject` were located at the given position, diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index 7ff63ad0ad..e29d7c9a56 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -14,13 +14,6 @@ class CollisionFrontEnd { public function new () {} - /** - * This value dictates the maximum number of pixels two objects have to intersect - * before collision stops trying to separate them. - * Don't modify this unless your objects are passing through each other. - */ - public var maxOverlap:Float = 4; - /** * 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, @@ -114,16 +107,6 @@ class CollisionFrontEnd // return separatedX || separatedY; } - public function separateX(object1:FlxObject, object2:FlxObject) - { - return processCheckTilemap(object1, object2, checkAndSeparateX); - } - - public function separateY(object1:FlxObject, object2:FlxObject) - { - return processCheckTilemap(object1, object2, checkAndSeparateY); - } - /** * Internal elper that determines whether either object is a tilemap, determines * which tiles are overlapping and calls the appropriate separator @@ -166,25 +149,32 @@ class CollisionFrontEnd return func(object1, object2); } + static final overlapHelperPoint = FlxPoint.get(); function checkAndSeparate(object1:FlxObject, object2:FlxObject) { - if (checkCollision(object1, object2)) + if (checkDeltaOverlaps(object1, object2)) { - final overlapX = computeCollisionOverlapXHelper(object1, object2); - final overlapY = computeCollisionOverlapYHelper(object1, object2); - - if ((overlapX == 0 && overlapY == 0) || (abs(overlapX) > maxOverlap && abs(overlapY) > maxOverlap)) + // check if any collisions are allowed + final allowX = checkCollisionXHelper(object1, object2); + final allowY = checkCollisionYHelper(object1, object2); + if (!allowX && !allowY) return false; - if (abs(overlapX) < abs(overlapY)) + // determine the amount of overlap + final overlap = object1.computeCollisionOverlap(object2, overlapHelperPoint); + + // seprate x + if (allowX && overlap.x != 0) { updateTouchingFlagsXHelper(object1, object2); - separateXHelper(object1, object2, overlapX); + separateXHelper(object1, object2, overlap.x); } - else + + // seprate y + if (allowY && overlap.y != 0) { updateTouchingFlagsYHelper(object1, object2); - separateYHelper(object1, object2, overlapY); + separateYHelper(object1, object2, overlap.y); } return true; @@ -193,40 +183,10 @@ class CollisionFrontEnd return false; } - function checkAndSeparateX(object1:FlxObject, object2:FlxObject) - { - if (checkCollision(object1, object2)) - { - final overlapX = computeCollisionOverlapXHelper(object1, object2); - if (abs(overlapX) > maxOverlap) - return false; - - separateXHelper(object1, object2, overlapX); - return true; - } - - return false; - } - - function checkAndSeparateY(object1:FlxObject, object2:FlxObject) - { - if (checkCollision(object1, object2)) - { - final overlapY = computeCollisionOverlapYHelper(object1, object2); - if (abs(overlapY) > maxOverlap) - return false; - - separateYHelper(object1, object2, overlapY); - return true; - } - - return false; - } - public function checkCollision(object1:FlxObject, object2:FlxObject) { return checkDeltaOverlaps(object1, object2) - && (checkXCollisionHelper(object1, object2) || checkYCollisionHelper(object1, object2)); + && (checkCollisionXHelper(object1, object2) || checkCollisionYHelper(object1, object2)); } /** @@ -320,69 +280,62 @@ class CollisionFrontEnd object2.x += object1.x - object1.last.x; } - inline function canObjectCollide(obj:FlxObject, dir:FlxDirectionFlags) + /** + * Helper to determine which edges of `object1`, if any, will strike the opposing edge of `object2` + * based solely on their delta positions + */ + public function getCollisionEdge(object1:FlxObject, object2:FlxObject) { - return obj.allowCollisions.has(dir); + return getCollisionEdgeX(object1, object2) | getCollisionEdgeY(object1, object2); } - function checkXCollisionHelper(object1:FlxObject, object2:FlxObject) + + /** + * Helper to determine which horizontal edge of `object1`, if any, will strike the opposing edge of `object2` + * based solely on their delta positions + */ + public function getCollisionEdgeX(object1:FlxObject, object2:FlxObject) { final deltaDiff = (object1.x - object1.last.x) - (object2.x - object2.last.x); - return (deltaDiff > 0 && canObjectCollide(object1, RIGHT) && canObjectCollide(object2, LEFT)) - || (deltaDiff < 0 && canObjectCollide(object1, LEFT) && canObjectCollide(object2, RIGHT)); + return deltaDiff == 0 ? NONE : deltaDiff > 0 ? RIGHT : LEFT; } - function checkYCollisionHelper(object1:FlxObject, object2:FlxObject) + /** + * Helper to determine which vertical edge of `object1`, if any, will strike the opposing edge of `object2` + * based solely on their delta positions + */ + public function getCollisionEdgeY(object1:FlxObject, object2:FlxObject) { final deltaDiff = (object1.y - object1.last.y) - (object2.y - object2.last.y); - return (deltaDiff > 0 && canObjectCollide(object1, DOWN) && canObjectCollide(object2, UP)) - || (deltaDiff < 0 && canObjectCollide(object1, UP) && canObjectCollide(object2, DOWN)); - } - - /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns (0, 0) **/ - public function computeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) - { - if (result == null) - result = FlxPoint.get(); - - if (checkCollision(object1, object2)) - result.set(computeCollisionOverlapXHelper(object1, object2), computeCollisionOverlapYHelper(object1, object2)); - - return result; + return abs(deltaDiff) < 0.0001 ? NONE : deltaDiff > 0 ? DOWN : UP; } - /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns 0 **/ - public function computeCollisionOverlapX(object1:FlxObject, object2:FlxObject) + inline function canObjectCollide(obj:FlxObject, dir:FlxDirectionFlags) { - if (checkCollision(object1, object2)) - return computeCollisionOverlapXHelper(object1, object2); - - return 0; + return obj.allowCollisions.has(dir); } - /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns 0 **/ - public function computeCollisionOverlapY(object1:FlxObject, object2:FlxObject) + function checkCollisionXHelper(object1:FlxObject, object2:FlxObject) { - if (checkCollision(object1, object2)) - return computeCollisionOverlapYHelper(object1, object2); - - return 0; + final dir = getCollisionEdgeX(object1, object2); + return (dir == RIGHT && canObjectCollide(object1, RIGHT) && canObjectCollide(object2, LEFT)) + || (dir == LEFT && canObjectCollide(object1, LEFT) && canObjectCollide(object2, RIGHT)); } - function computeCollisionOverlapXHelper(object1:FlxObject, object2:FlxObject) + function checkCollisionYHelper(object1:FlxObject, object2:FlxObject) { - if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) - return object1.x + object1.width - object2.x; - - return object1.x - object2.width - object2.x; + final dir = getCollisionEdgeY(object1, object2); + return (dir == DOWN && canObjectCollide(object1, DOWN) && canObjectCollide(object2, UP)) + || (dir == UP && canObjectCollide(object1, UP) && canObjectCollide(object2, DOWN)); } - function computeCollisionOverlapYHelper(object1:FlxObject, object2:FlxObject) + /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns (0, 0) **/ + public function computeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) { - if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) - return object1.y + object1.height - object2.y; - - return object1.y - object2.height - object2.y; + if (checkCollision(object1, object2)) + object1.computeCollisionOverlap(object2, result); + + return result; } function updateTouchingFlagsXHelper(object1:FlxObject, object2:FlxObject) @@ -472,4 +425,9 @@ class CollisionFrontEnd 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 caa8e81bdc..4181dc377f 100644 --- a/flixel/tile/FlxBaseTilemap.hx +++ b/flixel/tile/FlxBaseTilemap.hx @@ -330,7 +330,7 @@ class FlxBaseTilemap extends FlxObject * @see FlxCollision.getDeltaRect * @since 6.2.0 */ - public function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool):Bool + public function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool, stopAtFirst = false):Bool { throw "forEachCollidingTile must be implemented"; } @@ -1633,7 +1633,19 @@ class FlxBaseTilemap extends FlxObject final mapIndex = getMapIndex(point); return tileExists(mapIndex) && getTileData(mapIndex).solid; } - + + override function computeCollisionOverlap(object:FlxObject, ?result:FlxPoint):FlxPoint + { + function each (t:Tile) + { + result = t.computeCollisionOverlap(object, result); + return result.x != 0 || result.y != 0; + } + // TODO: Resolve multiple overlapping tiles rather than the first + forEachCollidingTile(object, each, true); + return result; + } + /** * 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..e004c5ced2 100644 --- a/flixel/tile/FlxTile.hx +++ b/flixel/tile/FlxTile.hx @@ -57,7 +57,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(). * diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx new file mode 100644 index 0000000000..9b0d4e4a41 --- /dev/null +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -0,0 +1,490 @@ +package flixel.tile; + +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.math.FlxMatrix; +import flixel.math.FlxPoint; +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) +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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float, ?result:FlxPoint) + { + if (grade == NONE) + return FlxObject.defaultComputeCollisionOverlap(tile, object, result); + + if (result == null) + result = FlxPoint.get(); + + result.set(computeCollisionOverlapX(tile, object, edges, grade), computeCollisionOverlapY(tile, object, edges, grade, downV)); + + 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:FlxTile, object:FlxObject, edges:FlxSlopeEdges) + { + final solidCollisions = edges.and(FlxG.collision.getCollisionEdgeX(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:FlxTile, object:FlxObject, edges:FlxSlopeEdges) + { + final solidCollisions = edges.and(FlxG.collision.getCollisionEdgeY(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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + { + final overlapY = computeSlopeOverlapY(tile, object, edges, grade, 0); + // check if they're hitting the solid edges + if (overlapY != 0 && checkHitSolidWallX(tile, object, edges)) + return FlxObject.defaultComputeCollisionOverlapX(tile, object); + + // let y separate + return Math.POSITIVE_INFINITY; + } + + static public function computeCollisionOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float) + { + if (checkHitSolidWallY(tile, object, edges)) + return FlxObject.defaultComputeCollisionOverlapY(tile, object); + + return computeSlopeOverlapY(tile, object, edges, grade, downV); + } + + inline static var GLUE_SNAP = 2; + static function computeSlopeOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float):Float + { + final solidBottom = edges.down; + final slope = edges.getSlope(grade); + // b = y intercept in world space + final b = tile.y + edges.getYIntercept(grade) * tile.height; + 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 y = slope * (objX - tile.x) + b; + + // Check if y intercept is outside of this tile + final isOutsideTile = (y < tile.y || y > tile.y + tile.height) + && (useLeftCorner ? object.x + object.width < tile.x : object.x > tile.x + tile.width); + + // check bottom + if (solidBottom) + { + // FlxG.watch.removeQuick("down"); + FlxG.watch.addQuick('in.${grade}', '${object.x < tile.x + tile.width && object.x + object.width > tile.x}'); + if (downV > 0 && (object.x < tile.x + tile.width && object.x + object.width > tile.x)) + { + // final objectLastX = useLeftCorner ? 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 + object.height + 1 > lastY) + // { + object.touching = FLOOR; + object.velocity.y = downV; + // } + // FlxG.watch.addQuick("v", round(object.velocity.y)); + } + + if (object.y + object.height > y) + { + if (isOutsideTile) + return 0; + + return y - (object.y + object.height); + } + } + else + { + if (isOutsideTile) + return 0; + + // check top + if (object.y < y) + return y - object.y; + } + + return 0; + } + + // static function solveCollisionSlopeNorthwest(slope:FlxTile, object:FlxObject):Void + // { + // if (object.x + object.width > slope.x + slope.width + _snapping) + // { + // return; + // } + // // Calculate the corner point of the object + // final objPos = FlxPoint.get(); + // objPos.x = Math.floor(object.x + object.width + _snapping); + // objPos.y = Math.floor(object.y + object.height); + + // // Calculate position of the point on the slope that the object might overlap + // // this would be one side of the object projected onto the slope's surface + // _slopePoint.x = objPos.x; + // _slopePoint.y = (slope.y + scaledTileHeight) - (_slopePoint.x - slope.x); + + // var tileId:Int = slope.index; + // if (checkThinSteep(tileId)) + // { + // if (_slopePoint.x - slope.x <= scaledTileWidth / 2) + // { + // return; + // } + // else + // { + // _slopePoint.y = slope.y + scaledTileHeight * (2 - (2 * (_slopePoint.x - slope.x) / scaledTileWidth)) + _snapping; + // if (_downwardsGlue && object.velocity.x > 0) + // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; + // } + // } + // else if (checkThickSteep(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight * (1 - (2 * ((_slopePoint.x - slope.x) / scaledTileWidth))) + _snapping; + // if (_downwardsGlue && object.velocity.x > 0) + // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; + // } + // else if (checkThickGentle(tileId)) + // { + // _slopePoint.y = slope.y + (scaledTileHeight - _slopePoint.x + slope.x) / 2; + // if (_downwardsGlue && object.velocity.x > 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // else if (checkThinGentle(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight - (_slopePoint.x - slope.x) / 2; + // if (_downwardsGlue && object.velocity.x > 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // else + // { + // if (_downwardsGlue && object.velocity.x > 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // // Fix the slope point to the slope tile + // fixSlopePoint(slope); + + // // Check if the object is inside the slope + // if (objPos.x > slope.x + _snapping + // && objPos.x < slope.x + scaledTileWidth + object.width + _snapping + // && objPos.y >= _slopePoint.y + // && objPos.y <= slope.y + scaledTileHeight) + // { + // // Call the collide function for the floor slope + // onCollideFloorSlope(slope, object); + // } + // } + + // static function solveCollisionSlopeNortheast(slope:FlxTile, object:FlxObject):Void + // { + // if (object.x < slope.x - _snapping) + // { + // return; + // } + // // Calculate the corner point of the object + // _objPoint.x = Math.floor(object.x - _snapping); + // _objPoint.y = Math.floor(object.y + object.height); + + // // Calculate position of the point on the slope that the object might overlap + // // this would be one side of the object projected onto the slope's surface + // _slopePoint.x = _objPoint.x; + // _slopePoint.y = (slope.y + scaledTileHeight) - (slope.x - _slopePoint.x + scaledTileWidth); + + // var tileId:Int = slope.index; + // if (checkThinSteep(tileId)) + // { + // if (_slopePoint.x - slope.x >= scaledTileWidth / 2) + // { + // return; + // } + // else + // { + // _slopePoint.y = slope.y + scaledTileHeight * 2 * ((_slopePoint.x - slope.x) / scaledTileWidth) + _snapping; + // } + // if (_downwardsGlue && object.velocity.x < 0) + // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; + // } + // else if (checkThickSteep(tileId)) + // { + // _slopePoint.y = slope.y - scaledTileHeight * (1 + (2 * ((slope.x - _slopePoint.x) / scaledTileWidth))) + _snapping; + // if (_downwardsGlue && object.velocity.x < 0) + // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; + // } + // else if (checkThickGentle(tileId)) + // { + // _slopePoint.y = slope.y + (scaledTileHeight - slope.x + _slopePoint.x - scaledTileWidth) / 2; + // if (_downwardsGlue && object.velocity.x < 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // else if (checkThinGentle(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight - (slope.x - _slopePoint.x + scaledTileWidth) / 2; + // if (_downwardsGlue && object.velocity.x < 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // else + // { + // if (_downwardsGlue && object.velocity.x < 0) + // object.velocity.x *= _slopeSlowDownFactor; + // } + // // Fix the slope point to the slope tile + // fixSlopePoint(slope); + + // // Check if the object is inside the slope + // if (_objPoint.x > slope.x - object.width - _snapping + // && _objPoint.x < slope.x + scaledTileWidth + _snapping + // && _objPoint.y >= _slopePoint.y + // && _objPoint.y <= slope.y + scaledTileHeight) + // { + // // Call the collide function for the floor slope + // onCollideFloorSlope(slope, object); + // } + // } + + // static function solveCollisionSlopeSouthwest(slope:FlxTile, object:FlxObject):Void + // { + // // Calculate the corner point of the object + // _objPoint.x = Math.floor(object.x + object.width + _snapping); + // _objPoint.y = Math.ceil(object.y); + + // // Calculate position of the point on the slope that the object might overlap + // // this would be one side of the object projected onto the slope's surface + // _slopePoint.x = _objPoint.x; + // _slopePoint.y = slope.y + (_slopePoint.x - slope.x); + + // var tileId:Int = slope.index; + // if (checkThinSteep(tileId)) + // { + // if (_slopePoint.x - slope.x <= scaledTileWidth / 2) + // { + // return; + // } + // else + // { + // _slopePoint.y = slope.y - scaledTileHeight * (1 + (2 * ((slope.x - _slopePoint.x) / scaledTileWidth))) - _snapping; + // } + // } + // else if (checkThickSteep(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight * 2 * ((_slopePoint.x - slope.x) / scaledTileWidth) - _snapping; + // } + // else if (checkThickGentle(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight - (slope.x - _slopePoint.x + scaledTileWidth) / 2; + // } + // else if (checkThinGentle(tileId)) + // { + // _slopePoint.y = slope.y + (scaledTileHeight - slope.x + _slopePoint.x - scaledTileWidth) / 2; + // } + + // // Fix the slope point to the slope tile + // fixSlopePoint(slope); + + // // Check if the object is inside the slope + // if (_objPoint.x > slope.x + _snapping + // && _objPoint.x < slope.x + scaledTileWidth + object.width + _snapping + // && _objPoint.y <= _slopePoint.y + // && _objPoint.y >= slope.y) + // { + // // Call the collide function for the floor slope + // onCollideCeilSlope(slope, object); + // } + // } + + // static function solveCollisionSlopeSoutheast(slope:FlxTile, object:FlxObject):Void + // { + // // Calculate the corner point of the object + // _objPoint.x = Math.floor(object.x - _snapping); + // _objPoint.y = Math.ceil(object.y); + + // // Calculate position of the point on the slope that the object might overlap + // // this would be one side of the object projected onto the slope's surface + // _slopePoint.x = _objPoint.x; + // _slopePoint.y = (slope.y) + (slope.x - _slopePoint.x + scaledTileWidth); + + // var tileId:Int = slope.index; + // if (checkThinSteep(tileId)) + // { + // if (_slopePoint.x - slope.x >= scaledTileWidth / 2) + // { + // return; + // } + // else + // { + // _slopePoint.y = slope.y + scaledTileHeight * (1 - (2 * ((_slopePoint.x - slope.x) / scaledTileWidth))) - _snapping; + // } + // } + // else if (checkThickSteep(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight * (2 - (2 * (_slopePoint.x - slope.x) / scaledTileWidth)) - _snapping; + // } + // else if (checkThickGentle(tileId)) + // { + // _slopePoint.y = slope.y + scaledTileHeight - (_slopePoint.x - slope.x) / 2; + // } + // else if (checkThinGentle(tileId)) + // { + // _slopePoint.y = slope.y + (scaledTileHeight - _slopePoint.x + slope.x) / 2; + // } + + // // Fix the slope point to the slope tile + // fixSlopePoint(slope); + + // // Check if the object is inside the slope + // if (_objPoint.x > slope.x - object.width - _snapping + // && _objPoint.x < slope.x + scaledTileWidth + _snapping + // && _objPoint.y <= _slopePoint.y + // && _objPoint.y >= slope.y) + // { + // // Call the collide function for the floor slope + // onCollideCeilSlope(slope, object); + // } + // } +} + +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 c58507af4f..f2b1032836 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -593,7 +593,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; @@ -790,16 +801,19 @@ class FlxTypedTilemap extends FlxBaseTilemap return result; } - override function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool):Bool + override function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool, stopAtFirst = false):Bool { function filter(tile) { - // return true, since an overlapping tile was found - return tile.overlapsObject(object) && (func == null || func(tile)); + final overlapping = tile.overlapsObject(object); + if (overlapping && tile.callbackFunction != null) + tile.callbackFunction(tile, object); + + return overlapping && (func == null || func(tile)); } final reverse = FlxAxes.fromBools(object.last.x > object.x, object.last.y > object.y); - return forEachTileOverlappingRect(object.getDeltaRect(FlxRect.weak()), filter, reverse, false); + return forEachTileOverlappingRect(object.getDeltaRect(FlxRect.weak()), filter, reverse, stopAtFirst); } function forEachTileOverlappingRect(rect:FlxRect, filter:(tile:Tile)->Bool, reverse:FlxAxes, stopAtFirst:Bool):Bool 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": { From a870bc971faaea33165f150d6519662b4dab6ac0 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Tue, 27 May 2025 08:21:15 -0500 Subject: [PATCH 04/14] huge refactor to quadtrees --- flixel/FlxG.hx | 131 ++- flixel/system/FlxQuadTree.hx | 832 +++++-------------- flixel/system/debug/stats/Stats.hx | 6 +- flixel/system/frontEnds/CollisionFrontEnd.hx | 10 +- 4 files changed, 316 insertions(+), 663 deletions(-) diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index 74efc86e71..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; @@ -415,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. */ - public static function overlap(?objectOrGroup1:FlxBasic, ?objectOrGroup2:FlxBasic, ?notifyCallback:Dynamic->Dynamic->Void, - ?processCallback:Dynamic->Dynamic->Bool):Bool + overload public static inline extern function overlap(objectOrGroup1:FlxBasic, objectOrGroup2:FlxBasic, ?notifier, ?processser):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 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. + */ + 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 + { + return FlxQuadTree.executeOnce(worldBounds, worldDivisions, objectOrGroup1, objectOrGroup2, notifier, processser); } /** @@ -467,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 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(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 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(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); } /** @@ -490,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/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..4642754024 100644 --- a/flixel/system/debug/stats/Stats.hx +++ b/flixel/system/debug/stats/Stats.hx @@ -296,9 +296,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 + "\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 index e29d7c9a56..6ebf742312 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -60,15 +60,7 @@ class CollisionFrontEnd function overlapHelper(a:FlxBasic, b:Null, notify:Null<(TA, TB)->Void>, ?process:Null<(TA, TB)->Bool>) { - if (b == a) - b = null; - - FlxQuadTree.divisions = FlxG.worldDivisions;// TODO - final quadTree = FlxQuadTree.recycle(FlxG.worldBounds.x, FlxG.worldBounds.y, FlxG.worldBounds.width, FlxG.worldBounds.height); - quadTree.load(a, b, cast notify, cast process); - final result:Bool = quadTree.execute(); - quadTree.destroy(); - return result; + return FlxQuadTree.executeOnce(FlxG.worldBounds, FlxG.worldDivisions, a, b, cast notify, cast process); } /** From ba8c9a1a9123175a7c327765bac830e8f71a58d1 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Wed, 28 May 2025 12:04:02 -0500 Subject: [PATCH 05/14] add FlxColliders WIP --- flixel/FlxObject.hx | 295 +++++---- flixel/FlxSprite.hx | 8 +- flixel/physics/FlxCollider.hx | 603 +++++++++++++++++++ flixel/physics/FlxCollisionQuadTree.hx | 297 +++++++++ flixel/system/debug/stats/Stats.hx | 9 +- flixel/system/frontEnds/CollisionFrontEnd.hx | 454 +++++++------- flixel/tile/FlxBaseTilemap.hx | 22 +- flixel/tile/FlxTile.hx | 8 +- flixel/tile/FlxTileSlopeUtil.hx | 121 ++-- flixel/tile/FlxTilemap.hx | 35 +- 10 files changed, 1431 insertions(+), 421 deletions(-) create mode 100644 flixel/physics/FlxCollider.hx create mode 100644 flixel/physics/FlxCollisionQuadTree.hx diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index def810d16b..b72b234cac 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -7,6 +7,7 @@ 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; @@ -74,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. @@ -95,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) { @@ -551,83 +552,28 @@ class FlxObject extends FlxBasic return overlap; } - /** - * Helper to compute the overlap of two objects, this is used when - * `object1.computeCollisionOverlap(object2)` is called on two objects - */ - public static function defaultComputeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) - { - if (result == null) - result = FlxPoint.get(); - - result.set(defaultComputeCollisionOverlapX(object1, object2), defaultComputeCollisionOverlapY(object1, object2)); - - function abs(n:Float) return n < 0 ? -n : n; - - final absX = abs(result.x); - final absY = abs(result.y); - - // separate on the smaller axis - if (absX > absY) - { - result.x = 0; - if (absY > SEPARATE_BIAS) - result.y = 0; - } - else - { - result.y = 0; - if (absX > SEPARATE_BIAS) - result.x = 0; - } - - return result; - } - - /** - * Helper to compute the X overlap of two objects, this is used when - * `object1.computeCollisionOverlapX(object2)` is called on two objects - */ - public static function defaultComputeCollisionOverlapX(object1:FlxObject, object2:FlxObject) - { - if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) - return object1.x + object1.width - object2.x; - - return object1.x - object2.width - object2.x; - } - - /** - * Helper to compute the Y overlap of two objects, this is used when - * `object1.computeCollisionOverlapY(object2)` is called on two objects - */ - public static function defaultComputeCollisionOverlapY(object1:FlxObject, object2:FlxObject) - { - if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) - return object1.y + object1.height - object2.y; - - return object1.y - object2.height - object2.y; - } - /** * 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; /** @@ -653,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, @@ -706,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). @@ -747,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 @@ -767,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 @@ -775,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 /** @@ -818,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(); @@ -832,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; @@ -849,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; @@ -862,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); } @@ -882,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); } @@ -927,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; } /** @@ -1003,11 +966,6 @@ class FlxObject extends FlxBasic { return overlaps(objectOrGroup, inScreenSpace, camera); } - - public function computeCollisionOverlap(object:FlxObject, ?result:FlxPoint):FlxPoint - { - return defaultComputeCollisionOverlap(this, object, result); - } /** * Checks to see if this `FlxObject` were located at the given position, @@ -1421,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 @@ -1445,7 +1415,7 @@ class FlxObject extends FlxBasic } #end - return width = value; + return width = collider.bounds.width = value; } @:noCompletion @@ -1459,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 @@ -1493,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 @@ -1510,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) @@ -1552,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/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx new file mode 100644 index 0000000000..b2a152936d --- /dev/null +++ b/flixel/physics/FlxCollider.hx @@ -0,0 +1,603 @@ +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:(a:IFlxCollider, b: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; + + public var immovable = false; + + public var mass = 1.0; + + 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; + + 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) + { + 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; } +} + +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, ?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(a, b, result); + case [_, CUSTOM(_, func)]: func(a, b, result); + case [AABB, AABB]: computeCollisionOverlapAabb(colliderA, colliderB, 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, ?result:FlxPoint) + { + if (result == null) + result = FlxPoint.get(); + + if (!a.bounds.overlaps(b.bounds)) + return result.set(0, 0); + + final allowX = FlxG.collision.checkCollisionEdgesX(a, b); + final allowY = FlxG.collision.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) + { + final overlap = computeCollisionOverlapXAabb(a, b); + if (abs(overlap) > FlxG.collision.maxOverlap) + return result; + + return result.set(overlap, 0); + } + + // only Y + if (!allowX && allowY) + { + final overlap = computeCollisionOverlapYAabb(a, b); + if (abs(overlap) > FlxG.collision.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 > FlxG.collision.maxOverlap) + result.y = 0; + } + else + { + result.y = 0; + if (absX > FlxG.collision.maxOverlap) + result.x = 0; + } + + return result; + } + + /** + * 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.x - a.last.x) > (b.x - b.last.x)) + 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.y - a.last.y) > (b.y - b.last.y)) + 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 + */ + public static function getCollisionEdges(a:FlxCollider, b:FlxCollider) + { + return getCollisionEdgesX(a, b) | getCollisionEdgesY(a, b); + } + + /** + * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + */ + public static function getCollisionEdgesX(a:FlxCollider, b:FlxCollider):FlxDirectionFlags + { + final deltaDiff = (a.x - a.last.x) - (b.x - b.last.x); + return abs(deltaDiff) < 0.0001 ? (RIGHT | LEFT) : 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 + */ + public static function getCollisionEdgesY(a:FlxCollider, b:FlxCollider):FlxDirectionFlags + { + final deltaDiff = (a.y - a.last.y) - (b.y - b.last.y); + return abs(deltaDiff) < 0.0001 ? 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`. + */ + public static function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider) + { + final dir = getCollisionEdgesX(a, b); + 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`. + */ + public static function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider) + { + final dir = getCollisionEdgesY(a, b); + 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/debug/stats/Stats.hx b/flixel/system/debug/stats/Stats.hx index 4642754024..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; /** @@ -299,7 +300,7 @@ class Stats extends Window _rightTextField.text = activeCount + " (" + updTime + "ms)\n" + visibleCount + " (" + drwTime + "ms)\n" + (FlxG.renderTile ? (drawCallsCount + "\n") : "") - + FlxQuadTree.pool.length + "\n" + FlxLinkedList._NUM_CACHED_FLX_LIST + + (FlxQuadTree.pool.length + FlxCollisionQuadTree.pool.length) + "\n" + FlxLinkedList._NUM_CACHED_FLX_LIST ; } diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index 6ebf742312..9be17c5c6f 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -4,14 +4,29 @@ 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; + + /** + * 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 () {} /** @@ -60,7 +75,7 @@ class CollisionFrontEnd function overlapHelper(a:FlxBasic, b:Null, notify:Null<(TA, TB)->Void>, ?process:Null<(TA, TB)->Bool>) { - return FlxQuadTree.executeOnce(FlxG.worldBounds, FlxG.worldDivisions, a, b, cast notify, cast process); + return FlxCollisionQuadTree.executeOnce(FlxG.worldBounds, FlxG.worldDivisions, a, b, cast notify, cast process); } /** @@ -69,9 +84,9 @@ class CollisionFrontEnd * 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. - * NOTE: does NOT take objects' `scrollFactor` into account, all overlaps are checked in world space. + * 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. @@ -90,328 +105,331 @@ class CollisionFrontEnd * * @return Whether the objects were overlapping and were separated */ - public function separate(object1:FlxObject, object2:FlxObject) - { - return processCheckTilemap(object1, object2, checkAndSeparate); - - // final separatedX = separateX(object1, object2); - // final separatedY = separateY(object1, object2); - // return separatedX || separatedY; - } - - /** - * Internal elper that determines whether either object is a tilemap, determines - * which tiles are overlapping and calls the appropriate separator - * - * @param func The process you wish to call with both objects, or between tiles, - * - * @param isCollision Does nothing, if both objects are immovable - * @return The result of whichever separator was used - */ - function processCheckTilemap(object1:FlxObject, object2:FlxObject, func:(FlxObject, FlxObject)->Bool):Bool + public function separate(a:FlxObject, b:FlxObject) { - // two immovable objects cannot collide - if (object1.immovable && object2.immovable) - return false; - - // If one of the objects is a tilemap, just pass it off. - if (object1.flixelType == TILEMAP) - { - final tilemap:FlxBaseTilemap = cast object1; - // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap - function recurseProcess(tile) - { - // Keep tile as first arg - return processCheckTilemap(tile, object2, func); - } - return tilemap.forEachCollidingTile(object2, recurseProcess); - } - else if (object2.flixelType == TILEMAP) - { - final tilemap:FlxBaseTilemap = cast object2; - // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap - function recurseProcess(tile) - { - // Keep tile as second arg - return processCheckTilemap(object1, tile, func); - } - return tilemap.forEachCollidingTile(object1, recurseProcess); - } - - return func(object1, object2); + return FlxColliderUtil.process(a, b, checkAndSeparate); } static final overlapHelperPoint = FlxPoint.get(); - function checkAndSeparate(object1:FlxObject, object2:FlxObject) + static final overlapInverseHelperPoint = FlxPoint.get(); + function checkAndSeparate(a:IFlxCollider, b:IFlxCollider) { - if (checkDeltaOverlaps(object1, object2)) - { - // check if any collisions are allowed - final allowX = checkCollisionXHelper(object1, object2); - final allowY = checkCollisionYHelper(object1, object2); - if (!allowX && !allowY) + 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); + + if (overlap.isZero()) return false; - // determine the amount of overlap - final overlap = object1.computeCollisionOverlap(object2, overlapHelperPoint); + final negativeOverlap = overlap.copyTo(overlapInverseHelperPoint); + + colliderA.onCollide.dispatch(b, overlap); + colliderB.onCollide.dispatch(a, negativeOverlap); // seprate x - if (allowX && overlap.x != 0) + if (overlap.x != 0) { - updateTouchingFlagsXHelper(object1, object2); - separateXHelper(object1, object2, overlap.x); + updateTouchingFlagsXHelper(colliderA, colliderB); + separateXHelper(colliderA, colliderB, overlap.x); } // seprate y - if (allowY && overlap.y != 0) + if (overlap.y != 0) { - updateTouchingFlagsYHelper(object1, object2); - separateYHelper(object1, object2, overlap.y); + 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; + // return false; } - public function checkCollision(object1:FlxObject, object2:FlxObject) + /** + * 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) { - return checkDeltaOverlaps(object1, object2) - && (checkCollisionXHelper(object1, object2) || checkCollisionYHelper(object1, object2)); + if (obj is FlxObject) + { + final object:FlxObject = cast obj; + @:bypassAccessor object.x = collider.x; + @:bypassAccessor object.y = collider.y; + @:bypassAccessor object.touching = collider.touching; + } } - /** - * Internal function use to determine if two objects may cross path, - * by comparing the bounds they occupy this frame - */ - function checkDeltaOverlaps(object1:FlxObject, object2:FlxObject) + public function checkCollision(a:FlxCollider, b:FlxCollider) + { + return a.overlapsDelta(b) + && (checkCollisionEdgesX(a, b) || checkCollisionEdgesY(a, b)); + } + + function separateHelper(a:FlxCollider, b:FlxCollider, overlap:FlxPoint) { - return object1.overlapsDelta(object2); + 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(object1:FlxObject, object2:FlxObject, overlap:Float) + function separateXHelper(a:FlxCollider, b:FlxCollider, overlap:Float) { - final delta1 = object1.x - object1.last.x; - final delta2 = object2.x - object2.last.x; - final vel1 = object1.velocity.x; - final vel2 = object2.velocity.x; + 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 (!object1.immovable && !object2.immovable) + if (!a.immovable && !b.immovable) { - #if FLX_4_LEGACY_COLLISION - legacySeparateX(object1, object2, overlap); - #else - object1.x -= overlap * 0.5; - object2.x += overlap * 0.5; + a.x -= overlap * 0.5; + b.x += overlap * 0.5; - final mass1 = object1.mass; - final mass2 = object2.mass; + final mass1 = a.mass; + final mass2 = b.mass; final momentum = mass1 * vel1 + mass2 * vel2; - object1.velocity.x = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); - object2.velocity.x = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); - #end + a.velocity.x = (momentum + a.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + b.velocity.x = (momentum + b.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); } - else if (!object1.immovable) + else if (!a.immovable) { - object1.x -= overlap; - object1.velocity.x = vel2 - vel1 * object1.elasticity; + a.x -= overlap; + a.velocity.x = vel2 - vel1 * a.elasticity; } - else if (!object2.immovable) + else if (!b.immovable) { - object2.x += overlap; - object2.velocity.x = vel1 - vel2 * object2.elasticity; + b.x += overlap; + b.velocity.x = vel1 - vel2 * b.elasticity; } // use collisionDrag properties to determine whether one object - if (allowCollisionDrag(object1.collisionYDrag, object1, object2) && delta1 > delta2) - object1.y += object2.y - object2.last.y; - else if (allowCollisionDrag(object2.collisionYDrag, object2, object1) && delta2 > delta1) - object2.y += object1.y - object1.last.y; + 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(object1:FlxObject, object2:FlxObject, overlap:Float) + function separateYHelper(a:FlxCollider, b:FlxCollider, overlap:Float) { - final delta1 = object1.y - object1.last.y; - final delta2 = object2.y - object2.last.y; - final vel1 = object1.velocity.y; - final vel2 = object2.velocity.y; + 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 (!object1.immovable && !object2.immovable) + if (!a.immovable && !b.immovable) { - #if FLX_4_LEGACY_COLLISION - legacySeparateY(object1, object2, overlap); - #else - object1.y -= overlap / 2; - object2.y += overlap / 2; + a.y -= overlap / 2; + b.y += overlap / 2; - final mass1 = object1.mass; - final mass2 = object2.mass; + final mass1 = a.mass; + final mass2 = b.mass; final momentum = mass1 * vel1 + mass2 * vel2; - final newVel1 = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); - final newVel2 = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); - object1.velocity.y = newVel1; - object2.velocity.y = newVel2; - #end + 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 (!object1.immovable) + else if (!a.immovable) { - object1.y -= overlap; - object1.velocity.y = vel2 - vel1 * object1.elasticity; + a.y -= overlap; + a.velocity.y = vel2 - vel1 * a.elasticity; } - else if (!object2.immovable) + else if (!b.immovable) { - object2.y += overlap; - object2.velocity.y = vel1 - vel2 * object2.elasticity; + b.y += overlap; + b.velocity.y = vel1 - vel2 * b.elasticity; } // use collisionDrag properties to determine whether one object - if (allowCollisionDrag(object1.collisionXDrag, object1, object2) && delta1 > delta2) - object1.x += object2.x - object2.last.x; - else if (allowCollisionDrag(object2.collisionXDrag, object2, object1) && delta2 > delta1) - object2.x += object1.x - object1.last.x; + 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; + } + + /** + * Helper to determine which edges of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + */ + overload public inline extern function getCollisionEdges(a:IFlxCollider, b:IFlxCollider) + { + return FlxColliderUtil.getCollisionEdges(a.getCollider(), b.getCollider()); + } + + /** + * Helper to determine which edges of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + */ + overload public inline extern function getCollisionEdges(a:FlxCollider, b:FlxCollider) + { + return FlxColliderUtil.getCollisionEdges(a, b); + } + + /** + * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` + * based solely on their delta positions + */ + overload public inline extern function getCollisionEdgesX(a:IFlxCollider, b:IFlxCollider) + { + return FlxColliderUtil.getCollisionEdgesX(a.getCollider(), b.getCollider()); } /** - * Helper to determine which edges of `object1`, if any, will strike the opposing edge of `object2` + * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` * based solely on their delta positions */ - public function getCollisionEdge(object1:FlxObject, object2:FlxObject) + overload public inline extern function getCollisionEdgesX(a:FlxCollider, b:FlxCollider) { - return getCollisionEdgeX(object1, object2) | getCollisionEdgeY(object1, object2); + return FlxColliderUtil.getCollisionEdgesX(a, b); } /** - * Helper to determine which horizontal edge of `object1`, if any, will strike the opposing edge of `object2` + * Helper to determine which vertical edge of `a`, if any, will strike the opposing edge of `b` * based solely on their delta positions */ - public function getCollisionEdgeX(object1:FlxObject, object2:FlxObject) + overload public inline extern function getCollisionEdgesY(a:IFlxCollider, b:IFlxCollider) { - final deltaDiff = (object1.x - object1.last.x) - (object2.x - object2.last.x); - return deltaDiff == 0 ? NONE : deltaDiff > 0 ? RIGHT : LEFT; + return FlxColliderUtil.getCollisionEdgesY(a.getCollider(), b.getCollider()); } /** - * Helper to determine which vertical edge of `object1`, if any, will strike the opposing edge of `object2` + * Helper to determine which vertical edge of `a`, if any, will strike the opposing edge of `b` * based solely on their delta positions */ - public function getCollisionEdgeY(object1:FlxObject, object2:FlxObject) + overload public inline extern function getCollisionEdgesY(a:FlxCollider, b:FlxCollider) { - final deltaDiff = (object1.y - object1.last.y) - (object2.y - object2.last.y); - return abs(deltaDiff) < 0.0001 ? NONE : deltaDiff > 0 ? DOWN : UP; + return FlxColliderUtil.getCollisionEdgesY(a, b); } - inline function canObjectCollide(obj:FlxObject, dir:FlxDirectionFlags) + inline function canCollide(obj:FlxCollider, dir:FlxDirectionFlags) { return obj.allowCollisions.has(dir); } - function checkCollisionXHelper(object1:FlxObject, object2:FlxObject) + /** + * Returns whether thetwo objects can collide in the X direction they are traveling. + * Checks `allowCollisions`. + */ + overload public inline extern function checkCollisionEdgesX(a:IFlxCollider, b:IFlxCollider) { - final dir = getCollisionEdgeX(object1, object2); - return (dir == RIGHT && canObjectCollide(object1, RIGHT) && canObjectCollide(object2, LEFT)) - || (dir == LEFT && canObjectCollide(object1, LEFT) && canObjectCollide(object2, RIGHT)); + return FlxColliderUtil.checkCollisionEdgesX(a.getCollider(), b.getCollider()); } - function checkCollisionYHelper(object1:FlxObject, object2:FlxObject) + /** + * Returns whether thetwo objects can collide in the X direction they are traveling. + * Checks `allowCollisions`. + */ + overload public inline extern function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider) { - final dir = getCollisionEdgeY(object1, object2); - return (dir == DOWN && canObjectCollide(object1, DOWN) && canObjectCollide(object2, UP)) - || (dir == UP && canObjectCollide(object1, UP) && canObjectCollide(object2, DOWN)); + return FlxColliderUtil.checkCollisionEdgesX(a, b); } - /** Determines if the two objects crossed pathed this frame and computes their overlap, otherwise returns (0, 0) **/ - public function computeCollisionOverlap(object1:FlxObject, object2:FlxObject, ?result:FlxPoint) + /** + * Returns whether thetwo objects can collide in the Y direction they are traveling. + * Checks `allowCollisions`. + */ + overload public inline extern function checkCollisionEdgesY(a:IFlxCollider, b:IFlxCollider) { - if (checkCollision(object1, object2)) - object1.computeCollisionOverlap(object2, result); - - return result; + return FlxColliderUtil.checkCollisionEdgesY(a.getCollider(), b.getCollider()); } - function updateTouchingFlagsXHelper(object1:FlxObject, object2:FlxObject) + /** + * Returns whether thetwo objects can collide in the Y direction they are traveling. + * Checks `allowCollisions`. + */ + overload public inline extern function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider) + { + return FlxColliderUtil.checkCollisionEdgesY(a, b); + } + + function updateTouchingFlagsXHelper(a:FlxCollider, b:FlxCollider) { - if ((object1.x - object1.last.x) > (object2.x - object2.last.x)) + if ((a.x - a.last.x) > (b.x - b.last.x)) { - object1.touching |= RIGHT; - object2.touching |= LEFT; + a.touching |= RIGHT; + b.touching |= LEFT; } else { - object1.touching |= LEFT; - object2.touching |= RIGHT; + a.touching |= LEFT; + b.touching |= RIGHT; } } - function updateTouchingFlagsYHelper(object1:FlxObject, object2:FlxObject) + function updateTouchingFlagsYHelper(a:FlxCollider, b:FlxCollider) { - if ((object1.y - object1.last.y) > (object2.y - object2.last.y)) + if ((a.y - a.last.y) > (b.y - b.last.y)) { - object1.touching |= DOWN; - object2.touching |= UP; + a.touching |= DOWN; + b.touching |= UP; } else { - object1.touching |= UP; - object2.touching |= DOWN; + a.touching |= UP; + b.touching |= DOWN; } } - function allowCollisionDrag(type:CollisionDragType, object1:FlxObject, object2:FlxObject):Bool + function allowCollisionDrag(type:FlxCollisionDragType, a:FlxCollider, b:FlxCollider):Bool { - return object2.active && object2.moves && switch (type) + return b.moves && switch (type) { case NEVER: false; case ALWAYS: true; - case IMMOVABLE: object2.immovable; - case HEAVIER: object2.immovable || object2.mass > object1.mass; + case IMMOVABLE: b.immovable; + case HEAVIER: b.immovable || b.mass > a.mass; } } - - /** - * The separateX that existed before HaxeFlixel 5.0, preserved for anyone who - * needs to use it in an old project. Does not preserve momentum, avoid if possible - */ - static inline function legacySeparateX(object1:FlxObject, object2:FlxObject, overlap:Float) - { - final vel1 = object1.velocity.x; - final vel2 = object2.velocity.x; - final mass1 = object1.mass; - final mass2 = object2.mass; - object1.x = object1.x - (overlap * 0.5); - object2.x += overlap * 0.5; - - var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); - var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); - final average = (newVel1 + newVel2) * 0.5; - newVel1 -= average; - newVel2 -= average; - object1.velocity.x = average + (newVel1 * object1.elasticity); - object2.velocity.x = average + (newVel2 * object2.elasticity); - } - - /** - * The separateY that existed before HaxeFlixel 5.0, preserved for anyone who - * needs to use it in an old project. Does not preserve momentum, avoid if possible - */ - static inline function legacySeparateY(object1:FlxObject, object2:FlxObject, overlap:Float) - { - final vel1 = object1.velocity.y; - final vel2 = object2.velocity.y; - final mass1 = object1.mass; - final mass2 = object2.mass; - object1.y = object1.y - (overlap * 0.5); - object2.y += overlap * 0.5; - - var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); - var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); - final average = (newVel1 + newVel2) * 0.5; - newVel1 -= average; - newVel2 -= average; - object1.velocity.y = average + (newVel1 * object1.elasticity); - object2.velocity.y = average + (newVel2 * object2.elasticity); - } } private inline function abs(n:Float) diff --git a/flixel/tile/FlxBaseTilemap.hx b/flixel/tile/FlxBaseTilemap.hx index 4181dc377f..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; @@ -323,14 +324,14 @@ class FlxBaseTilemap extends FlxObject * **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 object The object - * @param func Function that takes a tile and returns whether is satisfies the - * disired condition + * @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(object:FlxObject, func:(tile:Tile)->Bool, stopAtFirst = false):Bool + public function forEachCollidingTile(collider:IFlxCollider, func:(tile:Tile)->Bool, stopAtFirst = false):Bool { throw "forEachCollidingTile must be implemented"; } @@ -377,6 +378,7 @@ class FlxBaseTilemap extends FlxObject flixelType = TILEMAP; immovable = true; moves = false; + collider.type = FlxColliderType.TILEMAP; } override function destroy():Void @@ -1634,18 +1636,6 @@ class FlxBaseTilemap extends FlxObject return tileExists(mapIndex) && getTileData(mapIndex).solid; } - override function computeCollisionOverlap(object:FlxObject, ?result:FlxPoint):FlxPoint - { - function each (t:Tile) - { - result = t.computeCollisionOverlap(object, result); - return result.x != 0 || result.y != 0; - } - // TODO: Resolve multiple overlapping tiles rather than the first - forEachCollidingTile(object, each, true); - return result; - } - /** * 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 e004c5ced2..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; @@ -111,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 index 9b0d4e4a41..80e479946a 100644 --- a/flixel/tile/FlxTileSlopeUtil.hx +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -4,6 +4,7 @@ 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; @@ -120,15 +121,34 @@ class FlxTileSlopeUtil * @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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float, ?result:FlxPoint) + static public function computeCollisionOverlap(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, ?result:FlxPoint) { if (grade == NONE) - return FlxObject.defaultComputeCollisionOverlap(tile, object, result); + return FlxColliderUtil.computeCollisionOverlapAabb(tile.getCollider(), object.getCollider(), result); if (result == null) result = FlxPoint.get(); - result.set(computeCollisionOverlapX(tile, object, edges, grade), computeCollisionOverlapY(tile, object, edges, grade, downV)); + 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; @@ -152,39 +172,38 @@ class FlxTileSlopeUtil static function checkHitSolidWallX(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges) { - final solidCollisions = edges.and(FlxG.collision.getCollisionEdgeX(tile, object)); + 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:FlxTile, object:FlxObject, edges:FlxSlopeEdges) { - final solidCollisions = edges.and(FlxG.collision.getCollisionEdgeY(tile, object)); + 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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade) { - final overlapY = computeSlopeOverlapY(tile, object, edges, grade, 0); + final overlapY = computeSlopeOverlapY(tile, object, edges, grade); // check if they're hitting the solid edges if (overlapY != 0 && checkHitSolidWallX(tile, object, edges)) - return FlxObject.defaultComputeCollisionOverlapX(tile, object); + return FlxColliderUtil.computeCollisionOverlapXAabb(tile.getCollider(), object.getCollider()); // let y separate return Math.POSITIVE_INFINITY; } - static public function computeCollisionOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float) + static public function computeCollisionOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade) { if (checkHitSolidWallY(tile, object, edges)) - return FlxObject.defaultComputeCollisionOverlapY(tile, object); + return FlxColliderUtil.computeCollisionOverlapYAabb(tile.getCollider(), object.getCollider()); - return computeSlopeOverlapY(tile, object, edges, grade, downV); + return computeSlopeOverlapY(tile, object, edges, grade); } - inline static var GLUE_SNAP = 2; - static function computeSlopeOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, downV:Float):Float + static function computeSlopeOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade):Float { final solidBottom = edges.down; final slope = edges.getSlope(grade); @@ -200,44 +219,56 @@ class FlxTileSlopeUtil final isOutsideTile = (y < tile.y || y > 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) + if (solidBottom && object.y + object.height > y) + return y - (object.y + object.height); + + if (!solidBottom && object.y < y) + return y - 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:FlxTile, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, glueDownVelocity:Float) + { + 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.getCollider(), object).up) { - // FlxG.watch.removeQuick("down"); - FlxG.watch.addQuick('in.${grade}', '${object.x < tile.x + tile.width && object.x + object.width > tile.x}'); - if (downV > 0 && (object.x < tile.x + tile.width && object.x + object.width > tile.x)) - { - // final objectLastX = useLeftCorner ? 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 + object.height + 1 > lastY) - // { - object.touching = FLOOR; - object.velocity.y = downV; - // } - // FlxG.watch.addQuick("v", round(object.velocity.y)); - } - - if (object.y + object.height > y) + final useLeftCorner = slope > 0; + final objectLastX = useLeftCorner ? 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 < lastY) { - if (isOutsideTile) - return 0; - - return y - (object.y + object.height); + object.touching = object.touching.with(FLOOR); + object.velocity.y = glueDownVelocity; } + // FlxG.watch.addQuick("v", round(object.velocity.y)); } - else - { - if (isOutsideTile) - return 0; - - // check top - if (object.y < y) - return y - object.y; - } - - return 0; + } + + public static function isOnSlope(tile:FlxTile, object:FlxCollider) + { + final isInTile = (object.x < tile.x + tile.width && object.x + object.width > tile.x); + // return isInTile && overlap.y < 0; + // return isInTile && object.velocity.y >= 0; + // return object.velocity.y >= 0; + return object.velocity.y >= 0; } // static function solveCollisionSlopeNorthwest(slope:FlxTile, object:FlxObject):Void diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index f2b1032836..c229b4f8e8 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -14,6 +14,7 @@ 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; @@ -801,19 +802,37 @@ class FlxTypedTilemap extends FlxBaseTilemap return result; } - override function forEachCollidingTile(object:FlxObject, func:(tile:Tile)->Bool, stopAtFirst = false):Bool + override function forEachCollidingTile(object:IFlxCollider, func:(tile:Tile)->Bool, stopAtFirst = false):Bool { - function filter(tile) + final collider = object.getCollider(); + if (object is FlxObject) { - final overlapping = tile.overlapsObject(object); - if (overlapping && tile.callbackFunction != null) - tile.callbackFunction(tile, object); + 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; + } - return overlapping && (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 filter(tile:Tile) + { + return func == null || func(tile); } - final reverse = FlxAxes.fromBools(object.last.x > object.x, object.last.y > object.y); - return forEachTileOverlappingRect(object.getDeltaRect(FlxRect.weak()), filter, reverse, stopAtFirst); + 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 From c13213b946cb6e2150b27dd0a7ecf5f9ca94125b Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 29 May 2025 10:56:27 -0500 Subject: [PATCH 06/14] clarity --- flixel/tile/FlxTileSlopeUtil.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx index 80e479946a..1b0598f742 100644 --- a/flixel/tile/FlxTileSlopeUtil.hx +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -246,7 +246,7 @@ class FlxTileSlopeUtil // 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.getCollider(), object).up) + if (FlxG.collision.getCollisionEdgesY(tile, object).has(UP)) { final useLeftCorner = slope > 0; final objectLastX = useLeftCorner ? object.last.x : object.last.x + object.width; From a2a5f6f8d08bd5bb8cf6b9cc3f36bafa39139a96 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 29 May 2025 10:57:42 -0500 Subject: [PATCH 07/14] custom shapes' callback only takes the other collider --- flixel/physics/FlxCollider.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx index b2a152936d..4169070010 100644 --- a/flixel/physics/FlxCollider.hx +++ b/flixel/physics/FlxCollider.hx @@ -25,7 +25,7 @@ enum FlxColliderShape * @param computeOverlap A function that takes two colliders and determines how much * they overlap, and in which direction */ - CUSTOM(name:String, computeOverlap:(a:IFlxCollider, b:IFlxCollider, ?result:FlxPoint)->FlxPoint); + CUSTOM(name:String, computeOverlap:(collider:IFlxCollider, ?result:FlxPoint)->FlxPoint); } enum FlxColliderType @@ -357,8 +357,8 @@ class FlxColliderUtil case [SHAPE(shapeA), SHAPE(shapeB)]: switch [shapeA, shapeB] { - case [CUSTOM(_, func), _]: func(a, b, result); - case [_, CUSTOM(_, func)]: func(a, b, result); + case [CUSTOM(_, func), _]: func(b, result); + case [_, CUSTOM(_, func)]: func(a, result).negate(); case [AABB, AABB]: computeCollisionOverlapAabb(colliderA, colliderB, result); case [shapeA, shapeB]: throw 'Unexpected types: [$shapeA, $shapeB]'; } From b6f46b334fb4e86dc7259b11ab03c3cecc7cfaf8 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 29 May 2025 11:00:25 -0500 Subject: [PATCH 08/14] take FlxCollider in slope util --- flixel/tile/FlxTileSlopeUtil.hx | 264 ++------------------------------ 1 file changed, 11 insertions(+), 253 deletions(-) diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx index 1b0598f742..1c7131f3e7 100644 --- a/flixel/tile/FlxTileSlopeUtil.hx +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -18,7 +18,7 @@ import openfl.geom.Rectangle; /** * Used to define the "normal" or orthogonal edges of a slope, */ -@:forward(up, down, left, right, has, hasAny, and) +@:forward(up, down, left, right, has, hasAny, and, toString) enum abstract FlxSlopeEdges(FlxDirectionFlags) { /** ◥ **/ @@ -121,10 +121,10 @@ class FlxTileSlopeUtil * @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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade, ?result:FlxPoint) + static public function computeCollisionOverlap(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, ?result:FlxPoint) { if (grade == NONE) - return FlxColliderUtil.computeCollisionOverlapAabb(tile.getCollider(), object.getCollider(), result); + return FlxColliderUtil.computeCollisionOverlapAabb(tile, object, result); if (result == null) result = FlxPoint.get(); @@ -170,40 +170,40 @@ class FlxTileSlopeUtil return result; } - static function checkHitSolidWallX(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges) + 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:FlxTile, object:FlxObject, edges:FlxSlopeEdges) + 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:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + 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)) - return FlxColliderUtil.computeCollisionOverlapXAabb(tile.getCollider(), object.getCollider()); + return FlxColliderUtil.computeCollisionOverlapXAabb(tile, object); // let y separate return Math.POSITIVE_INFINITY; } - static public function computeCollisionOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade) + static public function computeCollisionOverlapY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade) { if (checkHitSolidWallY(tile, object, edges)) - return FlxColliderUtil.computeCollisionOverlapYAabb(tile.getCollider(), object.getCollider()); + return FlxColliderUtil.computeCollisionOverlapYAabb(tile, object); return computeSlopeOverlapY(tile, object, edges, grade); } - static function computeSlopeOverlapY(tile:FlxTile, object:FlxObject, edges:FlxSlopeEdges, grade:FlxSlopeGrade):Float + static function computeSlopeOverlapY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade):Float { final solidBottom = edges.down; final slope = edges.getSlope(grade); @@ -235,7 +235,7 @@ class FlxTileSlopeUtil 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:FlxTile, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, glueDownVelocity:Float) + public static function applyGlueDown(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, glueDownVelocity:Float) { if (glueDownVelocity <= 0) return; @@ -261,248 +261,6 @@ class FlxTileSlopeUtil // FlxG.watch.addQuick("v", round(object.velocity.y)); } } - - public static function isOnSlope(tile:FlxTile, object:FlxCollider) - { - final isInTile = (object.x < tile.x + tile.width && object.x + object.width > tile.x); - // return isInTile && overlap.y < 0; - // return isInTile && object.velocity.y >= 0; - // return object.velocity.y >= 0; - return object.velocity.y >= 0; - } - - // static function solveCollisionSlopeNorthwest(slope:FlxTile, object:FlxObject):Void - // { - // if (object.x + object.width > slope.x + slope.width + _snapping) - // { - // return; - // } - // // Calculate the corner point of the object - // final objPos = FlxPoint.get(); - // objPos.x = Math.floor(object.x + object.width + _snapping); - // objPos.y = Math.floor(object.y + object.height); - - // // Calculate position of the point on the slope that the object might overlap - // // this would be one side of the object projected onto the slope's surface - // _slopePoint.x = objPos.x; - // _slopePoint.y = (slope.y + scaledTileHeight) - (_slopePoint.x - slope.x); - - // var tileId:Int = slope.index; - // if (checkThinSteep(tileId)) - // { - // if (_slopePoint.x - slope.x <= scaledTileWidth / 2) - // { - // return; - // } - // else - // { - // _slopePoint.y = slope.y + scaledTileHeight * (2 - (2 * (_slopePoint.x - slope.x) / scaledTileWidth)) + _snapping; - // if (_downwardsGlue && object.velocity.x > 0) - // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; - // } - // } - // else if (checkThickSteep(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight * (1 - (2 * ((_slopePoint.x - slope.x) / scaledTileWidth))) + _snapping; - // if (_downwardsGlue && object.velocity.x > 0) - // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; - // } - // else if (checkThickGentle(tileId)) - // { - // _slopePoint.y = slope.y + (scaledTileHeight - _slopePoint.x + slope.x) / 2; - // if (_downwardsGlue && object.velocity.x > 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // else if (checkThinGentle(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight - (_slopePoint.x - slope.x) / 2; - // if (_downwardsGlue && object.velocity.x > 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // else - // { - // if (_downwardsGlue && object.velocity.x > 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // // Fix the slope point to the slope tile - // fixSlopePoint(slope); - - // // Check if the object is inside the slope - // if (objPos.x > slope.x + _snapping - // && objPos.x < slope.x + scaledTileWidth + object.width + _snapping - // && objPos.y >= _slopePoint.y - // && objPos.y <= slope.y + scaledTileHeight) - // { - // // Call the collide function for the floor slope - // onCollideFloorSlope(slope, object); - // } - // } - - // static function solveCollisionSlopeNortheast(slope:FlxTile, object:FlxObject):Void - // { - // if (object.x < slope.x - _snapping) - // { - // return; - // } - // // Calculate the corner point of the object - // _objPoint.x = Math.floor(object.x - _snapping); - // _objPoint.y = Math.floor(object.y + object.height); - - // // Calculate position of the point on the slope that the object might overlap - // // this would be one side of the object projected onto the slope's surface - // _slopePoint.x = _objPoint.x; - // _slopePoint.y = (slope.y + scaledTileHeight) - (slope.x - _slopePoint.x + scaledTileWidth); - - // var tileId:Int = slope.index; - // if (checkThinSteep(tileId)) - // { - // if (_slopePoint.x - slope.x >= scaledTileWidth / 2) - // { - // return; - // } - // else - // { - // _slopePoint.y = slope.y + scaledTileHeight * 2 * ((_slopePoint.x - slope.x) / scaledTileWidth) + _snapping; - // } - // if (_downwardsGlue && object.velocity.x < 0) - // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; - // } - // else if (checkThickSteep(tileId)) - // { - // _slopePoint.y = slope.y - scaledTileHeight * (1 + (2 * ((slope.x - _slopePoint.x) / scaledTileWidth))) + _snapping; - // if (_downwardsGlue && object.velocity.x < 0) - // object.velocity.x *= 1 - (1 - _slopeSlowDownFactor) * 3; - // } - // else if (checkThickGentle(tileId)) - // { - // _slopePoint.y = slope.y + (scaledTileHeight - slope.x + _slopePoint.x - scaledTileWidth) / 2; - // if (_downwardsGlue && object.velocity.x < 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // else if (checkThinGentle(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight - (slope.x - _slopePoint.x + scaledTileWidth) / 2; - // if (_downwardsGlue && object.velocity.x < 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // else - // { - // if (_downwardsGlue && object.velocity.x < 0) - // object.velocity.x *= _slopeSlowDownFactor; - // } - // // Fix the slope point to the slope tile - // fixSlopePoint(slope); - - // // Check if the object is inside the slope - // if (_objPoint.x > slope.x - object.width - _snapping - // && _objPoint.x < slope.x + scaledTileWidth + _snapping - // && _objPoint.y >= _slopePoint.y - // && _objPoint.y <= slope.y + scaledTileHeight) - // { - // // Call the collide function for the floor slope - // onCollideFloorSlope(slope, object); - // } - // } - - // static function solveCollisionSlopeSouthwest(slope:FlxTile, object:FlxObject):Void - // { - // // Calculate the corner point of the object - // _objPoint.x = Math.floor(object.x + object.width + _snapping); - // _objPoint.y = Math.ceil(object.y); - - // // Calculate position of the point on the slope that the object might overlap - // // this would be one side of the object projected onto the slope's surface - // _slopePoint.x = _objPoint.x; - // _slopePoint.y = slope.y + (_slopePoint.x - slope.x); - - // var tileId:Int = slope.index; - // if (checkThinSteep(tileId)) - // { - // if (_slopePoint.x - slope.x <= scaledTileWidth / 2) - // { - // return; - // } - // else - // { - // _slopePoint.y = slope.y - scaledTileHeight * (1 + (2 * ((slope.x - _slopePoint.x) / scaledTileWidth))) - _snapping; - // } - // } - // else if (checkThickSteep(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight * 2 * ((_slopePoint.x - slope.x) / scaledTileWidth) - _snapping; - // } - // else if (checkThickGentle(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight - (slope.x - _slopePoint.x + scaledTileWidth) / 2; - // } - // else if (checkThinGentle(tileId)) - // { - // _slopePoint.y = slope.y + (scaledTileHeight - slope.x + _slopePoint.x - scaledTileWidth) / 2; - // } - - // // Fix the slope point to the slope tile - // fixSlopePoint(slope); - - // // Check if the object is inside the slope - // if (_objPoint.x > slope.x + _snapping - // && _objPoint.x < slope.x + scaledTileWidth + object.width + _snapping - // && _objPoint.y <= _slopePoint.y - // && _objPoint.y >= slope.y) - // { - // // Call the collide function for the floor slope - // onCollideCeilSlope(slope, object); - // } - // } - - // static function solveCollisionSlopeSoutheast(slope:FlxTile, object:FlxObject):Void - // { - // // Calculate the corner point of the object - // _objPoint.x = Math.floor(object.x - _snapping); - // _objPoint.y = Math.ceil(object.y); - - // // Calculate position of the point on the slope that the object might overlap - // // this would be one side of the object projected onto the slope's surface - // _slopePoint.x = _objPoint.x; - // _slopePoint.y = (slope.y) + (slope.x - _slopePoint.x + scaledTileWidth); - - // var tileId:Int = slope.index; - // if (checkThinSteep(tileId)) - // { - // if (_slopePoint.x - slope.x >= scaledTileWidth / 2) - // { - // return; - // } - // else - // { - // _slopePoint.y = slope.y + scaledTileHeight * (1 - (2 * ((_slopePoint.x - slope.x) / scaledTileWidth))) - _snapping; - // } - // } - // else if (checkThickSteep(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight * (2 - (2 * (_slopePoint.x - slope.x) / scaledTileWidth)) - _snapping; - // } - // else if (checkThickGentle(tileId)) - // { - // _slopePoint.y = slope.y + scaledTileHeight - (_slopePoint.x - slope.x) / 2; - // } - // else if (checkThinGentle(tileId)) - // { - // _slopePoint.y = slope.y + (scaledTileHeight - _slopePoint.x + slope.x) / 2; - // } - - // // Fix the slope point to the slope tile - // fixSlopePoint(slope); - - // // Check if the object is inside the slope - // if (_objPoint.x > slope.x - object.width - _snapping - // && _objPoint.x < slope.x + scaledTileWidth + _snapping - // && _objPoint.y <= _slopePoint.y - // && _objPoint.y >= slope.y) - // { - // // Call the collide function for the floor slope - // onCollideCeilSlope(slope, object); - // } - // } } private inline function abs(n:Float) From e5c15130ee966ac4b309f525724cbac5eb70446e Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 09:12:47 -0500 Subject: [PATCH 09/14] add rect overlapsX/Y --- flixel/math/FlxRect.hx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 8c973a377f..0ba9420a14 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 * From a005fd5c8467aeffdb6561232747536301e4a49c Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 09:21:00 -0500 Subject: [PATCH 10/14] fix glue down and full penetration --- flixel/physics/FlxCollider.hx | 121 ++++++++++++++----- flixel/system/frontEnds/CollisionFrontEnd.hx | 30 +++-- flixel/tile/FlxTileSlopeUtil.hx | 71 ++++++++--- 3 files changed, 165 insertions(+), 57 deletions(-) diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx index 4169070010..52cf7fdbeb 100644 --- a/flixel/physics/FlxCollider.hx +++ b/flixel/physics/FlxCollider.hx @@ -70,10 +70,12 @@ class FlxCollider 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>(); @@ -114,6 +116,13 @@ class FlxCollider 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; @@ -142,16 +151,20 @@ class FlxCollider public function update(elapsed:Float) { - 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; + 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) @@ -249,6 +262,9 @@ class FlxCollider 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 @@ -411,7 +427,7 @@ class FlxColliderUtil if (result == null) result = FlxPoint.get(); - if (!a.bounds.overlaps(b.bounds)) + if (!checkForPenetration(a, b)) return result.set(0, 0); final allowX = FlxG.collision.checkCollisionEdgesX(a, b); @@ -422,7 +438,7 @@ class FlxColliderUtil function abs(n:Float) return n < 0 ? -n : n; // only X - if (allowX && !allowY) + if (checkForFullPenetrationX(a, b) || allowX && !allowY) { final overlap = computeCollisionOverlapXAabb(a, b); if (abs(overlap) > FlxG.collision.maxOverlap) @@ -432,7 +448,7 @@ class FlxColliderUtil } // only Y - if (!allowX && allowY) + if (checkForFullPenetrationY(a, b) || !allowX && allowY) { final overlap = computeCollisionOverlapYAabb(a, b); if (abs(overlap) > FlxG.collision.maxOverlap) @@ -450,26 +466,57 @@ class FlxColliderUtil if (absX > absY) { result.x = 0; - if (absY > FlxG.collision.maxOverlap) + if (absY > FlxG.collision.maxOverlap)// Todo: pass in maxOverlap result.y = 0; } else { result.y = 0; - if (absX > FlxG.collision.maxOverlap) + if (absX > FlxG.collision.maxOverlap)// Todo: pass in maxOverlap result.x = 0; } return result; } + /** + * 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)) + || (checkForFullPenetrationY(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.bounds.overlapsY(b.bounds) && (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.bounds.overlapsX(b.bounds) && (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.x - a.last.x) > (b.x - b.last.x)) + if (a.deltaX > b.deltaX) return a.x + a.width - b.x; return a.x - b.width - b.x; @@ -481,7 +528,7 @@ class FlxColliderUtil */ public static function computeCollisionOverlapYAabb(a:FlxCollider, b:FlxCollider):Float { - if ((a.y - a.last.y) > (b.y - b.last.y)) + if (a.deltaY > b.deltaY) return a.y + a.height - b.y; return a.y - b.height - b.y; @@ -492,30 +539,40 @@ class FlxColliderUtil /** * 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) + public static function getCollisionEdges(a:FlxCollider, b:FlxCollider, elseBoth = false) { - return getCollisionEdgesX(a, b) | getCollisionEdgesY(a, b); + 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):FlxDirectionFlags + public static function getCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false):FlxDirectionFlags { - final deltaDiff = (a.x - a.last.x) - (b.x - b.last.x); - return abs(deltaDiff) < 0.0001 ? (RIGHT | LEFT) : deltaDiff > 0 ? RIGHT : LEFT; + 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):FlxDirectionFlags + public static function getCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false):FlxDirectionFlags { - final deltaDiff = (a.y - a.last.y) - (b.y - b.last.y); - return abs(deltaDiff) < 0.0001 ? NONE : deltaDiff > 0 ? DOWN : UP; + 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) @@ -526,10 +583,13 @@ class FlxColliderUtil /** * 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) + public static function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false) { - final dir = getCollisionEdgesX(a, b); + 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)); } @@ -537,10 +597,13 @@ class FlxColliderUtil /** * 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) + public static function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false) { - final dir = getCollisionEdgesY(a, b); + 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)); } diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index 9be17c5c6f..e46860f784 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -18,7 +18,7 @@ class CollisionFrontEnd /** * Collisions between FlxObjects will not resolve overlaps larger than this values, in pixels */ - public var maxOverlap = 4; + public var maxOverlap = 4.0; /** * How many times the quad tree should divide the world on each axis. @@ -359,37 +359,49 @@ class CollisionFrontEnd /** * 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 */ - overload public inline extern function checkCollisionEdgesX(a:IFlxCollider, b:IFlxCollider) + overload public inline extern function checkCollisionEdgesX(a:IFlxCollider, b:IFlxCollider, elseBoth = false) { - return FlxColliderUtil.checkCollisionEdgesX(a.getCollider(), b.getCollider()); + return FlxColliderUtil.checkCollisionEdgesX(a.getCollider(), b.getCollider(), elseBoth); } /** * 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 */ - overload public inline extern function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider) + overload public inline extern function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false) { - return FlxColliderUtil.checkCollisionEdgesX(a, b); + return FlxColliderUtil.checkCollisionEdgesX(a, b, elseBoth); } /** * 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 */ - overload public inline extern function checkCollisionEdgesY(a:IFlxCollider, b:IFlxCollider) + overload public inline extern function checkCollisionEdgesY(a:IFlxCollider, b:IFlxCollider, elseBoth = false) { - return FlxColliderUtil.checkCollisionEdgesY(a.getCollider(), b.getCollider()); + return FlxColliderUtil.checkCollisionEdgesY(a.getCollider(), b.getCollider(), elseBoth); } /** * 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 */ - overload public inline extern function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider) + overload public inline extern function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false) { - return FlxColliderUtil.checkCollisionEdgesY(a, b); + return FlxColliderUtil.checkCollisionEdgesY(a, b, elseBoth); } function updateTouchingFlagsXHelper(a:FlxCollider, b:FlxCollider) diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx index 1c7131f3e7..9c14361826 100644 --- a/flixel/tile/FlxTileSlopeUtil.hx +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -188,7 +188,7 @@ class FlxTileSlopeUtil { final overlapY = computeSlopeOverlapY(tile, object, edges, grade); // check if they're hitting the solid edges - if (overlapY != 0 && checkHitSolidWallX(tile, object, edges)) + if (overlapY != 0 && checkHitSolidWallX(tile, object, edges) && tile.bounds.overlaps(object.bounds)) return FlxColliderUtil.computeCollisionOverlapXAabb(tile, object); // let y separate @@ -197,7 +197,7 @@ class FlxTileSlopeUtil static public function computeCollisionOverlapY(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade) { - if (checkHitSolidWallY(tile, object, edges)) + if (checkHitSolidWallY(tile, object, edges) && tile.bounds.overlaps(object.bounds)) return FlxColliderUtil.computeCollisionOverlapYAabb(tile, object); return computeSlopeOverlapY(tile, object, edges, grade); @@ -207,27 +207,29 @@ class FlxTileSlopeUtil { final solidBottom = edges.down; final slope = edges.getSlope(grade); - // b = y intercept in world space - final b = tile.y + edges.getYIntercept(grade) * tile.height; 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 y = slope * (objX - tile.x) + b; + final slopeY = getSlopeYAtHelper(tile, objX, slope, edges.getYIntercept(grade)); // Check if y intercept is outside of this tile - final isOutsideTile = (y < tile.y || y > tile.y + tile.height) - && (useLeftCorner ? object.x + object.width < tile.x : object.x > tile.x + tile.width); + // 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 > y) - return y - (object.y + object.height); + if (solidBottom && object.y + object.height > slopeY) + return slopeY - (object.y + object.height); - if (!solidBottom && object.y < y) - return y - object.y; + if (!solidBottom && object.y < slopeY) + return slopeY - object.y; return 0; } @@ -235,32 +237,63 @@ class FlxTileSlopeUtil 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) + 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 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 useLeftCorner = slope > 0; - final objectLastX = useLeftCorner ? object.last.x : object.last.x + object.width; - final lastY = slope * (objectLastX - tile.x) + b; + 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 < lastY) + if (object.last.y < getSlopeYAtHelper(tile, objectLastX, slope, yInt)) { - object.touching = object.touching.with(FLOOR); 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) From 0e07a6609a32c5a2a7b7ffc0a15be9a63caf5645 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 10:35:07 -0500 Subject: [PATCH 11/14] rect intersection x/y helpers --- flixel/math/FlxRect.hx | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 0ba9420a14..6ef5cc1f42 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -310,7 +310,7 @@ class FlxRect implements IFlxPooled rect.putWeak(); return result; } - + /** * Checks to see if this rectangle fully contains another * @@ -327,7 +327,7 @@ class FlxRect implements IFlxPooled rect.putWeak(); return result; } - + /** * Returns true if this FlxRect contains the FlxPoint * @@ -560,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. From 6f48a26f94ad4c957441c41e96de0c1dd841f8c1 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 10:36:15 -0500 Subject: [PATCH 12/14] add maxOverlap arg --- flixel/physics/FlxCollider.hx | 14 +++++++------- flixel/system/frontEnds/CollisionFrontEnd.hx | 3 ++- flixel/tile/FlxTileSlopeUtil.hx | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx index 52cf7fdbeb..0edfe864f2 100644 --- a/flixel/physics/FlxCollider.hx +++ b/flixel/physics/FlxCollider.hx @@ -364,7 +364,7 @@ class FlxColliderUtil } } - public static function computeCollisionOverlap(a:IFlxCollider, b:IFlxCollider, ?result:FlxPoint):FlxPoint + public static function computeCollisionOverlap(a:IFlxCollider, b:IFlxCollider, maxOverlap:Float, ?result:FlxPoint):FlxPoint { final colliderA = a.getCollider(); final colliderB = b.getCollider(); @@ -375,7 +375,7 @@ class FlxColliderUtil { case [CUSTOM(_, func), _]: func(b, result); case [_, CUSTOM(_, func)]: func(a, result).negate(); - case [AABB, AABB]: computeCollisionOverlapAabb(colliderA, colliderB, result); + case [AABB, AABB]: computeCollisionOverlapAabb(colliderA, colliderB, maxOverlap, result); case [shapeA, shapeB]: throw 'Unexpected types: [$shapeA, $shapeB]'; } default: @@ -422,7 +422,7 @@ class FlxColliderUtil * 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, ?result:FlxPoint) + public static function computeCollisionOverlapAabb(a:FlxCollider, b:FlxCollider, maxOverlap:Float, ?result:FlxPoint) { if (result == null) result = FlxPoint.get(); @@ -441,7 +441,7 @@ class FlxColliderUtil if (checkForFullPenetrationX(a, b) || allowX && !allowY) { final overlap = computeCollisionOverlapXAabb(a, b); - if (abs(overlap) > FlxG.collision.maxOverlap) + if (abs(overlap) > maxOverlap) return result; return result.set(overlap, 0); @@ -451,7 +451,7 @@ class FlxColliderUtil if (checkForFullPenetrationY(a, b) || !allowX && allowY) { final overlap = computeCollisionOverlapYAabb(a, b); - if (abs(overlap) > FlxG.collision.maxOverlap) + if (abs(overlap) > maxOverlap) return result; return result.set(0, overlap); @@ -466,13 +466,13 @@ class FlxColliderUtil if (absX > absY) { result.x = 0; - if (absY > FlxG.collision.maxOverlap)// Todo: pass in maxOverlap + if (absY > maxOverlap) result.y = 0; } else { result.y = 0; - if (absX > FlxG.collision.maxOverlap)// Todo: pass in maxOverlap + if (absX > maxOverlap) result.x = 0; } diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index e46860f784..136d527951 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -124,7 +124,8 @@ class CollisionFrontEnd if (!colliderA.type.match(SHAPE(_)) || !colliderB.type.match(SHAPE(_))) return true; - final overlap = a.computeCollisionOverlap(b); + final overlap = a.computeCollisionOverlap(b, maxOverlap); + if (overlap.isZero()) return false; diff --git a/flixel/tile/FlxTileSlopeUtil.hx b/flixel/tile/FlxTileSlopeUtil.hx index 9c14361826..4eead299f4 100644 --- a/flixel/tile/FlxTileSlopeUtil.hx +++ b/flixel/tile/FlxTileSlopeUtil.hx @@ -121,10 +121,10 @@ class FlxTileSlopeUtil * @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, ?result:FlxPoint) + static public function computeCollisionOverlap(tile:FlxCollider, object:FlxCollider, edges:FlxSlopeEdges, grade:FlxSlopeGrade, maxOverlap:Float, ?result:FlxPoint) { if (grade == NONE) - return FlxColliderUtil.computeCollisionOverlapAabb(tile, object, result); + return FlxColliderUtil.computeCollisionOverlapAabb(tile, object, maxOverlap, result); if (result == null) result = FlxPoint.get(); From fc47964c5da4b1fcf5d15dec9e4542f9bdcb528f Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 10:37:02 -0500 Subject: [PATCH 13/14] remove refs to FlxG.collide from FlxColliderUtil --- flixel/physics/FlxCollider.hx | 4 +- flixel/system/frontEnds/CollisionFrontEnd.hx | 109 ------------------- 2 files changed, 2 insertions(+), 111 deletions(-) diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx index 0edfe864f2..c4a6b2dd6b 100644 --- a/flixel/physics/FlxCollider.hx +++ b/flixel/physics/FlxCollider.hx @@ -430,8 +430,8 @@ class FlxColliderUtil if (!checkForPenetration(a, b)) return result.set(0, 0); - final allowX = FlxG.collision.checkCollisionEdgesX(a, b); - final allowY = FlxG.collision.checkCollisionEdgesY(a, b); + final allowX = checkCollisionEdgesX(a, b); + final allowY = checkCollisionEdgesY(a, b); if (!allowX && !allowY) return result.set(0, 0); diff --git a/flixel/system/frontEnds/CollisionFrontEnd.hx b/flixel/system/frontEnds/CollisionFrontEnd.hx index 136d527951..9ec05dde83 100644 --- a/flixel/system/frontEnds/CollisionFrontEnd.hx +++ b/flixel/system/frontEnds/CollisionFrontEnd.hx @@ -177,12 +177,6 @@ class CollisionFrontEnd } } - public function checkCollision(a:FlxCollider, b:FlxCollider) - { - return a.overlapsDelta(b) - && (checkCollisionEdgesX(a, b) || checkCollisionEdgesY(a, b)); - } - function separateHelper(a:FlxCollider, b:FlxCollider, overlap:FlxPoint) { final delta1 = FlxPoint.get(a.x - a.last.x, a.y - a.last.y); @@ -297,114 +291,11 @@ class CollisionFrontEnd b.x += a.x - a.last.x; } - /** - * Helper to determine which edges of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdges(a:IFlxCollider, b:IFlxCollider) - { - return FlxColliderUtil.getCollisionEdges(a.getCollider(), b.getCollider()); - } - - /** - * Helper to determine which edges of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdges(a:FlxCollider, b:FlxCollider) - { - return FlxColliderUtil.getCollisionEdges(a, b); - } - - /** - * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdgesX(a:IFlxCollider, b:IFlxCollider) - { - return FlxColliderUtil.getCollisionEdgesX(a.getCollider(), b.getCollider()); - } - - /** - * Helper to determine which horizontal edge of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdgesX(a:FlxCollider, b:FlxCollider) - { - return FlxColliderUtil.getCollisionEdgesX(a, b); - } - - - /** - * Helper to determine which vertical edge of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdgesY(a:IFlxCollider, b:IFlxCollider) - { - return FlxColliderUtil.getCollisionEdgesY(a.getCollider(), b.getCollider()); - } - - /** - * Helper to determine which vertical edge of `a`, if any, will strike the opposing edge of `b` - * based solely on their delta positions - */ - overload public inline extern function getCollisionEdgesY(a:FlxCollider, b:FlxCollider) - { - return FlxColliderUtil.getCollisionEdgesY(a, b); - } - inline function canCollide(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 - */ - overload public inline extern function checkCollisionEdgesX(a:IFlxCollider, b:IFlxCollider, elseBoth = false) - { - return FlxColliderUtil.checkCollisionEdgesX(a.getCollider(), b.getCollider(), elseBoth); - } - - /** - * 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 - */ - overload public inline extern function checkCollisionEdgesX(a:FlxCollider, b:FlxCollider, elseBoth = false) - { - return FlxColliderUtil.checkCollisionEdgesX(a, b, elseBoth); - } - - /** - * 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 - */ - overload public inline extern function checkCollisionEdgesY(a:IFlxCollider, b:IFlxCollider, elseBoth = false) - { - return FlxColliderUtil.checkCollisionEdgesY(a.getCollider(), b.getCollider(), elseBoth); - } - - /** - * 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 - */ - overload public inline extern function checkCollisionEdgesY(a:FlxCollider, b:FlxCollider, elseBoth = false) - { - return FlxColliderUtil.checkCollisionEdgesY(a, b, elseBoth); - } - function updateTouchingFlagsXHelper(a:FlxCollider, b:FlxCollider) { if ((a.x - a.last.x) > (b.x - b.last.x)) From 94f47e5bcf7fb52740f6542ce9014676b9183641 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 12 Jun 2025 10:37:18 -0500 Subject: [PATCH 14/14] more improvements to narrow phase AABB --- flixel/physics/FlxCollider.hx | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/flixel/physics/FlxCollider.hx b/flixel/physics/FlxCollider.hx index c4a6b2dd6b..3b2168274b 100644 --- a/flixel/physics/FlxCollider.hx +++ b/flixel/physics/FlxCollider.hx @@ -438,7 +438,7 @@ class FlxColliderUtil function abs(n:Float) return n < 0 ? -n : n; // only X - if (checkForFullPenetrationX(a, b) || allowX && !allowY) + if ((allowX && !allowY) || penetratedOnX(a, b)) { final overlap = computeCollisionOverlapXAabb(a, b); if (abs(overlap) > maxOverlap) @@ -448,7 +448,7 @@ class FlxColliderUtil } // only Y - if (checkForFullPenetrationY(a, b) || !allowX && allowY) + if ((!allowX && allowY) || penetratedOnY(a, b)) { final overlap = computeCollisionOverlapYAabb(a, b); if (abs(overlap) > maxOverlap) @@ -479,15 +479,36 @@ class FlxColliderUtil 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)) - || (checkForFullPenetrationY(a, b)); - + || (checkForFullPenetrationX(a, b) && (a.bounds.overlapsY(b.bounds) || lastOverlappedY(a, b))) + || (checkForFullPenetrationY(a, b) && (a.bounds.overlapsX(b.bounds) || lastOverlappedX(a, b))); } /** @@ -495,7 +516,7 @@ class FlxColliderUtil */ public static function checkForFullPenetrationX(a:FlxCollider, b:FlxCollider) { - return a.bounds.overlapsY(b.bounds) && (a.deltaX > b.deltaX + 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); } @@ -505,7 +526,7 @@ class FlxColliderUtil */ public static function checkForFullPenetrationY(a:FlxCollider, b:FlxCollider) { - return a.bounds.overlapsX(b.bounds) && (a.deltaY > b.deltaY + 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); }