Skip to content

Commit

Permalink
fix: Properly resize ScreenHitbox when needed (#2826)
Browse files Browse the repository at this point in the history
Resizes the `ScreenHitbox` on zoom changes.
  • Loading branch information
spydon committed Oct 29, 2023
1 parent 9faae8a commit 24fed75
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 17 deletions.
33 changes: 21 additions & 12 deletions packages/flame/lib/src/camera/viewfinder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ class Viewfinder extends Component
}
}

final Vector2 _zeroVector = Vector2.zero();
final Vector2 _topLeft = Vector2.zero();
final Vector2 _bottomRight = Vector2.zero();
final Vector2 _topRight = Vector2.zero();
final Vector2 _bottomLeft = Vector2.zero();

/// See [CameraComponent.visibleWorldRect].
@internal
Rect get visibleWorldRect => visibleRect ??= computeVisibleRect();
Expand All @@ -144,19 +150,22 @@ class Viewfinder extends Component
@protected
Rect computeVisibleRect() {
final viewportSize = camera.viewport.size;
final topLeft = transform.globalToLocal(Vector2.zero());
final bottomRight = transform.globalToLocal(viewportSize);
var minX = min(topLeft.x, bottomRight.x);
var minY = min(topLeft.y, bottomRight.y);
var maxX = max(topLeft.x, bottomRight.x);
var maxY = max(topLeft.y, bottomRight.y);
final currentTransform = transform;
currentTransform.globalToLocal(_zeroVector, output: _topLeft);
currentTransform.globalToLocal(viewportSize, output: _bottomRight);
var minX = min(_topLeft.x, _bottomRight.x);
var minY = min(_topLeft.y, _bottomRight.y);
var maxX = max(_topLeft.x, _bottomRight.x);
var maxY = max(_topLeft.y, _bottomRight.y);
if (angle != 0) {
final topRight = transform.globalToLocal(Vector2(viewportSize.x, 0));
final bottomLeft = transform.globalToLocal(Vector2(0, viewportSize.y));
minX = min(minX, min(topRight.x, bottomLeft.x));
minY = min(minY, min(topRight.y, bottomLeft.y));
maxX = max(maxX, max(topRight.x, bottomLeft.x));
maxY = max(maxY, max(topRight.y, bottomLeft.y));
_topRight.setValues(viewportSize.x, 0);
_bottomLeft.setValues(0, viewportSize.y);
currentTransform.globalToLocal(_topRight, output: _topRight);
currentTransform.globalToLocal(_bottomLeft, output: _bottomLeft);
minX = min(minX, min(_topRight.x, _bottomLeft.x));
minY = min(minY, min(_topRight.y, _bottomLeft.y));
maxX = max(maxX, max(_topRight.x, _bottomLeft.x));
maxY = max(maxY, max(_topRight.y, _bottomLeft.y));
}
return Rect.fromLTRB(minX, minY, maxX, maxY);
}
Expand Down
34 changes: 29 additions & 5 deletions packages/flame/lib/src/collisions/hitboxes/screen_hitbox.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/src/collisions/collision_callbacks.dart';
Expand All @@ -14,24 +16,46 @@ class ScreenHitbox<T extends FlameGame> extends PositionComponent
add(RectangleHitbox());
_hasWorldAncestor = findParent<World>() != null;
if (_hasWorldAncestor) {
game.camera.viewfinder.transform.addListener(_updatePosition);
_updatePosition();
game.camera.viewfinder.transform.addListener(_updateTransform);
_updateTransform();
}
}

void _updatePosition() {
final Vector2 _tmpPosition = Vector2.zero();

void _updateTransform() {
final viewfinder = game.camera.viewfinder;
position.setFrom(viewfinder.position);
final visibleRect = game.camera.visibleWorldRect;
size.setValues(visibleRect.width, visibleRect.height);
_tmpPosition.setValues(visibleRect.topLeft.dx, visibleRect.topLeft.dy);
position = Anchor.topLeft.toOtherAnchorPosition(
_tmpPosition,
viewfinder.anchor,
size,
);
anchor = viewfinder.anchor;
angle = viewfinder.angle;
if (angle != 0) {
final cosTheta = cos(angle).abs();
final sinTheta = sin(angle).abs();
final newWidth = (size.x * cosTheta) + (size.y * sinTheta);
final newHeight = (size.x * sinTheta) + (size.y * cosTheta);

// Shrink the new dimensions to keep the original AABB size before the
// rotation.
final scaleWidth = size.x / newWidth;
final scaleHeight = size.y / newHeight;

size.setValues(newWidth * scaleWidth, newHeight * scaleHeight);
}
}

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
this.size = size;
if (_hasWorldAncestor) {
_updatePosition();
_updateTransform();
}
}
}
119 changes: 119 additions & 0 deletions packages/flame/test/collisions/screen_hibox_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:test/test.dart';

import 'collision_test_helpers.dart';

void main() {
group('ScreenHitbox', () {
runCollisionTestRegistry({
'collides': (hasCollisionDetection) async {
final game = hasCollisionDetection as FlameGame;
final visibleRect = game.camera.visibleWorldRect;
final testBlock = TestBlock(
visibleRect.topLeft.toVector2(),
Vector2.all(10),
)..anchor = Anchor.center;
final screenHitbox = ScreenHitbox();
await game.world.addAll([screenHitbox, testBlock]);
await game.ready();
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 0);

testBlock.position = Vector2.zero();
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 1);
},
});

runCollisionTestRegistry({
'collides when zoom is not 1.0': (hasCollisionDetection) async {
final game = hasCollisionDetection as FlameGame;
game.camera.viewfinder.zoom = 2.5;
final visibleRect = game.camera.visibleWorldRect;
final testBlock = TestBlock(
visibleRect.topLeft.toVector2(),
Vector2.all(10),
)..anchor = Anchor.center;
final screenHitbox = ScreenHitbox();
await game.world.addAll([screenHitbox, testBlock]);
await game.ready();
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 0);

testBlock.position = Vector2.zero();
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 1);
},
});

runCollisionTestRegistry({
'collides when game size changes': (hasCollisionDetection) async {
final game = hasCollisionDetection as FlameGame;
final visibleRect = game.camera.visibleWorldRect;
final testBlock = TestBlock(
visibleRect.topLeft.toVector2() / 2,
Vector2.all(10),
)..anchor = Anchor.center;
final screenHitbox = ScreenHitbox();
await game.world.addAll([screenHitbox, testBlock]);
await game.ready();
game.update(0);

expect(testBlock.startCounter, 0);
expect(testBlock.onCollisionCounter, 0);
expect(testBlock.endCounter, 0);

testBlock.position = visibleRect.topLeft.toVector2() / 2;
game.onGameResize(game.size / 2);
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 0);
},
});

runCollisionTestRegistry({
'collides when angle is not 0.0': (hasCollisionDetection) async {
final game = hasCollisionDetection as FlameGame;
final visibleRectBeforeRotation = game.camera.visibleWorldRect;
game.camera.viewfinder.angle = tau / 8;
final visibleRect = game.camera.visibleWorldRect;
final testBlock = TestBlock(
visibleRect.topLeft.toVector2(),
Vector2.all(10),
)..anchor = Anchor.center;
final screenHitbox = ScreenHitbox();
await game.world.addAll([screenHitbox, testBlock]);
await game.ready();
game.update(0);

expect(testBlock.startCounter, 0);
expect(testBlock.onCollisionCounter, 0);
expect(testBlock.endCounter, 0);

testBlock.position = visibleRectBeforeRotation.topLeft.toVector2();
game.update(0);

expect(testBlock.startCounter, 1);
expect(testBlock.onCollisionCounter, 1);
expect(testBlock.endCounter, 0);
},
});
});
}

0 comments on commit 24fed75

Please sign in to comment.