Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions packages/flame/lib/src/camera/camera_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,55 @@ class CameraComponent extends Component {
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
if (checkContains(viewport, viewportPoint)) {
currentCameras.add(this);
final worldPoint = transformContext(viewfinder, viewportPoint);
if (worldPoint == null) {
return;
}
yield* viewfinder.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
yield* _onCameraStack(
viewfinder.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
),
);
yield* world!.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
yield* _onCameraStack(
world!.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
),
);
}
}
}

/// Iterates [components] with this camera pushed onto [currentCameras], so
/// that a camera nested within the world still sees the correct depth.
///
/// The camera is pushed and popped around every step rather than around the
/// whole iteration. This is a lazy iterable, so the caller is free to stop
/// early, for example by breaking out of a loop over [componentsAtPoint] as
/// soon as the tapped component is found. That leaves the generator
/// suspended at a `yield`, and the code after it never runs. Popping there
/// would therefore be skipped, and since [currentCameras] is static the
/// entry would be leaked for the rest of the process; after
/// [maxCamerasDepth] such leaks no camera renders its world any more.
Iterable<Component> _onCameraStack(Iterable<Component> components) sync* {
final iterator = components.iterator;
while (true) {
currentCameras.add(this);
final bool hasNext;
try {
hasNext = iterator.moveNext();
} finally {
currentCameras.removeLast();
}
if (!hasNext) {
return;
}
yield iterator.current;
}
}

Expand Down
41 changes: 41 additions & 0 deletions packages/flame/test/camera/camera_component_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:math';
import 'dart:ui' show PictureRecorder;

import 'package:flame/camera.dart';
import 'package:flame/components.dart';
Expand Down Expand Up @@ -240,6 +241,39 @@ void main() {
);
});

testWithFlameGame(
'componentsAtPoint that is not fully iterated keeps the camera '
'rendering',
(game) async {
game.camera.viewport = FixedSizeViewport(600, 400)
..anchor = Anchor.center
..position = Vector2(400, 300)
..priority = -1;
game.camera.viewfinder.position = Vector2(100, 50);
final component = _RenderCounter(
size: Vector2(300, 100),
position: Vector2(50, 30),
);
game.world.add(component);
await game.ready();

// Stopping as soon as the wanted component turns up is what a game
// acting on whatever was tapped does.
for (var i = 0; i < CameraComponent.maxCamerasDepth * 2; i++) {
for (final candidate in game.componentsAtPoint(Vector2(400, 300))) {
if (candidate == component) {
break;
}
}
expect(CameraComponent.currentCameras, isEmpty);
}

component.renderCount = 0;
game.render(Canvas(PictureRecorder()));
expect(component.renderCount, 1);
},
);

testWithFlameGame('visibleWorldRect', (game) async {
await game.ready();
final camera = game.camera;
Expand Down Expand Up @@ -613,6 +647,13 @@ class _SolidBackground extends Component with HasPaint {
void render(Canvas canvas) => canvas.drawColor(color, BlendMode.src);
}

class _RenderCounter extends PositionComponent {
_RenderCounter({super.size, super.position});
int renderCount = 0;
@override
void render(Canvas canvas) => renderCount++;
}

class _PostProcessChecker extends PostProcess {
bool isLoaded = false;

Expand Down
Loading