diff --git a/flixel/FlxCamera.hx b/flixel/FlxCamera.hx index 5484ab12d9..0f544d8e42 100644 --- a/flixel/FlxCamera.hx +++ b/flixel/FlxCamera.hx @@ -1873,6 +1873,53 @@ class FlxCamera extends FlxBasic setScale(scaleX, scaleY); } + /** + * Centers `FlxSprite` by graphic size in this camera view, either by the x axis, y axis, or both. + * + * @param sprite The sprite to center. + * @param axes On what axes to center the sprite (e.g. `X`, `Y`, `XY`) - default is both. + * @return Centered sprite for chaining. + * @since 6.2.0 + */ + public function centerGraphic(sprite:T, axes:FlxAxes = XY):T + { + final graphicBounds = sprite.getAccurateScreenBounds(null, this); + + if (axes.x) + { + final offset = sprite.x - graphicBounds.x; + sprite.x = (width - graphicBounds.width) / 2 + offset; + } + + if (axes.y) + { + final offset = sprite.y - graphicBounds.y; + sprite.y = (height - graphicBounds.height) / 2 + offset; + } + + graphicBounds.put(); + return sprite; + } + + /** + * Centers `FlxObject` by hitbox size in this camera view, either by the x axis, y axis, or both. + * + * @param object The object to center. + * @param axes On what axes to center the object (e.g. `X`, `Y`, `XY`) - default is both. + * @return Centered object for chaining. + * @since 6.2.0 + */ + public function centerHitbox(object:T, axes:FlxAxes = XY):T + { + if (axes.x) + object.x = scroll.x + (width - object.width) / 2; + + if (axes.y) + object.y = scroll.y + (height - object.height) / 2; + + return object; + } + /** * The size and position of this camera's margins, via `viewMarginLeft`, `viewMarginTop`, `viewWidth` * and `viewHeight`. diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index baad5bdbb7..394b565017 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -20,6 +20,7 @@ import flixel.system.frontEnds.VCRFrontEnd; import flixel.system.frontEnds.WatchFrontEnd; import flixel.system.scaleModes.BaseScaleMode; import flixel.system.scaleModes.RatioScaleMode; +import flixel.util.FlxAxes; import flixel.util.FlxCollision; import flixel.util.FlxSave; import flixel.util.typeLimit.NextState; @@ -476,7 +477,54 @@ class FlxG { return overlap(objectOrGroup1, objectOrGroup2, notifyCallback, FlxObject.separate); } - + + /** + * Centers `FlxSprite` by graphic size in game space, either by the x axis, y axis, or both. + * + * @param sprite The sprite to center. + * @param axes On what axes to center the sprite (e.g. `X`, `Y`, `XY`) - default is both. + * @return Centered sprite for chaining. + * @since 6.2.0 + */ + public static function centerGraphic(sprite:T, axes:FlxAxes = XY):T + { + final graphicBounds = sprite.getAccurateGraphicBounds(); + + if (axes.x) + { + final offset = sprite.x - graphicBounds.x; + sprite.x = (FlxG.width - graphicBounds.width) / 2 + offset; + } + + if (axes.y) + { + final offset = sprite.y - graphicBounds.y; + sprite.y = (FlxG.height - graphicBounds.height) / 2 + offset; + } + + graphicBounds.put(); + return sprite; + } + + /** + * Centers `FlxObject` by hitbox size in game space, either by the x axis, y axis, or both. + * + * @param object The object to center. + * @param axes On what axes to center the object (e.g. `X`, `Y`, `XY`) - default is both. + * @return Centered object for chaining. + * @since 6.2.0 + */ + public static function centerHitbox(object:T, axes:FlxAxes = XY):T + { + if (axes.x) + object.x = (FlxG.width - object.width) / 2; + + if (axes.y) + object.y = (FlxG.height - object.height) / 2; + + return object; + } + /** * Regular `DisplayObject`s are normally displayed over the Flixel cursor and the Flixel debugger if simply * added to `stage`. This function simplifies things by adding a `DisplayObject` directly below mouse level. diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index bff324c41a..c484d2ba8f 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -1031,7 +1031,7 @@ class FlxObject extends FlxBasic { return (x + width > FlxG.worldBounds.x) && (x < FlxG.worldBounds.right) && (y + height > FlxG.worldBounds.y) && (y < FlxG.worldBounds.bottom); } - + /** * Returns the screen position of this object. * @@ -1040,20 +1040,30 @@ class FlxObject extends FlxBasic * @return The screen position of this object. */ public function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + { + return getScreenPositionHelper(result, camera, true); + } + + public function getAccurateScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + { + return getScreenPositionHelper(result, camera, false); + } + + public function getScreenPositionHelper(result:FlxPoint, camera:FlxCamera, honorPixelPerfect:Bool):FlxPoint { if (result == null) result = FlxPoint.get(); - + if (camera == null) camera = getDefaultCamera(); - + result.set(x, y); - if (pixelPerfectPosition) + if (honorPixelPerfect && pixelPerfectPosition) result.floor(); - + return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y); } - + /** * Returns the world position of this object. * @@ -1173,24 +1183,19 @@ class FlxObject extends FlxBasic kill(); } #end - + /** * Centers this `FlxObject` on the screen, either by the x axis, y axis, or both. - * + * * @param axes On what axes to center the object (e.g. `X`, `Y`, `XY`) - default is both. * @return This FlxObject for chaining */ + @:deprecated("screenCenter is deprecated, use FlxG.centerHitbox instead") public inline function screenCenter(axes:FlxAxes = XY):FlxObject { - if (axes.x) - x = (FlxG.width - width) / 2; - - if (axes.y) - y = (FlxG.height - height) / 2; - - return this; + return FlxG.centerHitbox(this, axes); } - + /** * Helper function to set the coordinates of this object. * Handy since it only requires one line of code. diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index cd4704bfff..f273bb2409 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1289,23 +1289,31 @@ class FlxSprite extends FlxObject * @since 5.9.0 */ public function getGraphicBounds(?rect:FlxRect):FlxRect + { + return getGraphicBoundsHelper(rect, true); + } + + public function getAccurateGraphicBounds(?rect:FlxRect):FlxRect + { + return getGraphicBoundsHelper(rect, false); + } + + public function getGraphicBoundsHelper(rect:FlxRect, honorPixelPerfect:Bool):FlxRect { if (rect == null) rect = FlxRect.get(); rect.set(x, y); - if (pixelPerfectPosition) + if (honorPixelPerfect && pixelPerfectPosition) rect.floor(); - _scaledOrigin.set(origin.x * scale.x, origin.y * scale.y); + final absoluteScale = _point.set(Math.abs(scale.x), Math.abs(scale.y)); + _scaledOrigin.set(origin.x * absoluteScale.x, origin.y * absoluteScale.y); rect.x += origin.x - offset.x - _scaledOrigin.x; rect.y += origin.y - offset.y - _scaledOrigin.y; - rect.setSize(frameWidth * scale.x, frameHeight * scale.y); + rect.setSize(frameWidth * absoluteScale.x, frameHeight * absoluteScale.y); - if (angle % 360 != 0) - rect.getRotatedBounds(angle, _scaledOrigin, rect); - - return rect; + return rect.getRotatedBounds(angle, _scaledOrigin, rect); } /** @@ -1359,7 +1367,7 @@ class FlxSprite extends FlxObject * @return A globally aligned `FlxRect` that fully contains the input object's width and height. * @since 4.11.0 */ - override function getRotatedBounds(?newRect:FlxRect) + override public function getRotatedBounds(?newRect:FlxRect):FlxRect { if (newRect == null) newRect = FlxRect.get(); @@ -1377,6 +1385,16 @@ class FlxSprite extends FlxObject * @since 4.11.0 */ public function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect + { + return getScreenBoundsHelper(newRect, camera, true); + } + + public function getAccurateScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect + { + return getScreenBoundsHelper(newRect, camera, false); + } + + function getScreenBoundsHelper(newRect:FlxRect, camera:FlxCamera, honorPixelPerfect:Bool):FlxRect { if (newRect == null) newRect = FlxRect.get(); @@ -1385,14 +1403,17 @@ class FlxSprite extends FlxObject camera = getDefaultCamera(); newRect.setPosition(x, y); - if (pixelPerfectPosition) + if (honorPixelPerfect && pixelPerfectPosition) newRect.floor(); - _scaledOrigin.set(origin.x * scale.x, origin.y * scale.y); - newRect.x += -Std.int(camera.scroll.x * scrollFactor.x) - offset.x + origin.x - _scaledOrigin.x; - newRect.y += -Std.int(camera.scroll.y * scrollFactor.y) - offset.y + origin.y - _scaledOrigin.y; - if (isPixelPerfectRender(camera)) + + final absoluteScale = _point.set(Math.abs(scale.x), Math.abs(scale.y)); + _scaledOrigin.set(origin.x * absoluteScale.x, origin.y * absoluteScale.y); + newRect.x += -camera.scroll.x * scrollFactor.x - offset.x + origin.x - _scaledOrigin.x; + newRect.y += -camera.scroll.y * scrollFactor.y - offset.y + origin.y - _scaledOrigin.y; + if (honorPixelPerfect && isPixelPerfectRender(camera)) newRect.floor(); - newRect.setSize(frameWidth * Math.abs(scale.x), frameHeight * Math.abs(scale.y)); + + newRect.setSize(frameWidth * absoluteScale.x, frameHeight * absoluteScale.y); return newRect.getRotatedBounds(angle, _scaledOrigin, newRect); } diff --git a/tests/unit/src/flixel/FlxCameraTest.hx b/tests/unit/src/flixel/FlxCameraTest.hx index 85459f6094..7fee0290b0 100644 --- a/tests/unit/src/flixel/FlxCameraTest.hx +++ b/tests/unit/src/flixel/FlxCameraTest.hx @@ -1,6 +1,8 @@ package flixel; +import flixel.math.FlxPoint; import flixel.util.FlxColor; +import haxe.PosInfos; import massive.munit.Assert; @:access(flixel.system.frontEnds.CameraFrontEnd) @@ -89,7 +91,79 @@ class FlxCameraTest extends FlxTest camera.follow(new FlxObject()); Assert.areEqual(defaultLerp, camera.followLerp); } - + + @Test // #3329 + function testCenterGraphic() + { + final cam = FlxG.camera; + cam.scroll.set(100.5, 100.5); + cam.zoom *= 2; + + Assert.areEqual(cam.width, 640); + Assert.areEqual(cam.height, 480); + + final sprite = new FlxSprite(); + sprite.makeGraphic(10, 10); + sprite.scrollFactor.set(2, 2); + sprite.origin.set(10, 10); + sprite.offset.set(10, 10); + sprite.scale.set(2, 4); + sprite.angle = 180; + sprite.pixelPerfectPosition = true; + sprite.pixelPerfectRender = true; + + sprite.setPosition(0, 0); + cam.centerGraphic(sprite, X); + assertCenter(sprite, 100.5 + 320 - 10 - (-110.5 + 10), 0); + + sprite.setPosition(0, 0); + cam.centerGraphic(sprite, Y); + assertCenter(sprite, 0, 100.5 + 240 - 20 - (-110.5 + 10)); + + sprite.setPosition(0, 0); + cam.centerGraphic(sprite, XY); + assertCenter(sprite, 100.5 + 320 - 10 - (-110.5 + 10), 100.5 + 240 - 20 - (-110.5 + 10)); + + sprite.setPosition(1640, 1480); + cam.centerGraphic(sprite); + assertCenter(sprite, 100.5 + 320 - 10 - (-110.5 + 10), 100.5 + 240 - 20 - (-110.5 + 10)); + } + + @Test // #3329 + function testCenterHitbox() + { + final cam = FlxG.camera; + cam.scroll.set(100.5, 100.5); + cam.zoom *= 2; + + Assert.areEqual(cam.width, 640); + Assert.areEqual(cam.height, 480); + + final object = new FlxObject(0, 0, 10, 10); + + object.setPosition(0, 0); + cam.centerHitbox(object, X); + assertCenter(object, 100.5 + 320 - 5, 0); + + object.setPosition(0, 0); + cam.centerHitbox(object, Y); + assertCenter(object, 0, 100.5 + 240 - 5); + + object.setPosition(0, 0); + cam.centerHitbox(object, XY); + assertCenter(object, 100.5 + 320 - 5, 100.5 + 240 - 5); + + object.setPosition(1640, 1480); + cam.centerHitbox(object); + assertCenter(object, 100.5 + 320 - 5, 100.5 + 240 - 5); + } + + function assertCenter(object:FlxObject, expectedX:Float, expectedY:Float, ?info:PosInfos) + { + FlxAssert.areNear(object.x, expectedX, info); + FlxAssert.areNear(object.y, expectedY, info); + } + @Test function testFadeInFadeOut() { diff --git a/tests/unit/src/flixel/FlxGTest.hx b/tests/unit/src/flixel/FlxGTest.hx index 58d3daac76..1090a4f77d 100644 --- a/tests/unit/src/flixel/FlxGTest.hx +++ b/tests/unit/src/flixel/FlxGTest.hx @@ -1,5 +1,7 @@ package flixel; +import flixel.math.FlxPoint; +import haxe.PosInfos; import massive.munit.Assert; @:access(flixel.FlxG) @@ -104,4 +106,66 @@ class FlxGTest extends FlxTest { Assert.areEqual(480, FlxG.height); } + + @Test // #3329 + function testCenterGraphic() + { + Assert.areEqual(FlxG.width, 640); + Assert.areEqual(FlxG.height, 480); + + final sprite = new FlxSprite(); + sprite.makeGraphic(10, 10); + sprite.origin.set(10, 10); + sprite.offset.set(10, 10); + sprite.scale.set(2, 4); + sprite.angle = 180; + sprite.pixelPerfectPosition = true; + + sprite.setPosition(0, 0); + FlxG.centerGraphic(sprite, X); + assertCenter(sprite, 320 - 10 - (-10 + 10), 0); + + sprite.setPosition(0, 0); + FlxG.centerGraphic(sprite, Y); + assertCenter(sprite, 0, 240 - 20 - (-10 + 10)); + + sprite.setPosition(0, 0); + FlxG.centerGraphic(sprite, XY); + assertCenter(sprite, 320 - 10 - (-10 + 10), 240 - 20 - (-10 + 10)); + + sprite.setPosition(1640, 1480); + FlxG.centerGraphic(sprite); + assertCenter(sprite, 320 - 10 - (-10 + 10), 240 - 20 - (-10 + 10)); + } + + @Test // #3329 + function testCenterHitbox() + { + Assert.areEqual(FlxG.width, 640); + Assert.areEqual(FlxG.height, 480); + + final object = new FlxObject(0, 0, 10, 10); + + object.setPosition(0, 0); + FlxG.centerHitbox(object, X); + assertCenter(object, 320 - 5, 0); + + object.setPosition(0, 0); + FlxG.centerHitbox(object, Y); + assertCenter(object, 0, 240 - 5); + + object.setPosition(0, 0); + FlxG.centerHitbox(object, XY); + assertCenter(object, 320 - 5, 240 - 5); + + object.setPosition(1640, 1480); + FlxG.centerHitbox(object); + assertCenter(object, 320 - 5, 240 - 5); + } + + function assertCenter(object:FlxObject, expectedX:Float, expectedY:Float, ?info:PosInfos) + { + FlxAssert.areNear(object.x, expectedX, info); + FlxAssert.areNear(object.y, expectedY, info); + } } diff --git a/tests/unit/src/flixel/FlxObjectTest.hx b/tests/unit/src/flixel/FlxObjectTest.hx index dc89530222..ac830f4420 100644 --- a/tests/unit/src/flixel/FlxObjectTest.hx +++ b/tests/unit/src/flixel/FlxObjectTest.hx @@ -340,36 +340,6 @@ class FlxObjectTest extends FlxTest assertNotOnScreen(0, FlxG.height); } - @Test - function testScreenCenter() - { - var center = FlxPoint.get((FlxG.width - object1.width) / 2, (FlxG.height - object1.height) / 2); - var offCenter = center.copyTo().add(1000, 1000); - - object1.setPosition(offCenter.x, offCenter.y); - object1.screenCenter(X); - Assert.areEqual(object1.x, center.x); - Assert.areEqual(object1.y, offCenter.y); - - object1.setPosition(offCenter.x, offCenter.y); - object1.screenCenter(Y); - Assert.areEqual(object1.x, offCenter.x); - Assert.areEqual(object1.y, center.y); - - object1.setPosition(offCenter.x, offCenter.y); - object1.screenCenter(XY); - Assert.areEqual(object1.x, center.x); - Assert.areEqual(object1.y, center.y); - - object1.setPosition(offCenter.x, offCenter.y); - object1.screenCenter(); - Assert.areEqual(object1.x, center.x); - Assert.areEqual(object1.y, center.y); - - offCenter.put(); - center.put(); - } - @Test function testgetRotatedBounds() {