Skip to content
Open
47 changes: 47 additions & 0 deletions flixel/FlxCamera.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T:FlxSprite>(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<T:FlxObject>(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`.
Expand Down
50 changes: 49 additions & 1 deletion flixel/FlxG.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<T:FlxSprite>(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<T:FlxObject>(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.
Expand Down
37 changes: 21 additions & 16 deletions flixel/FlxObject.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 35 additions & 14 deletions flixel/FlxSprite.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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);
}

Expand Down
76 changes: 75 additions & 1 deletion tests/unit/src/flixel/FlxCameraTest.hx
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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()
{
Expand Down
Loading