What happened?
In my game the whole screen goes black after a few taps. The world and everything drawn by the camera disappears, and only the background color stays. It happens after around four taps on components.
The strange part is that nothing is thrown. There is no exception and no error in the logs. The game loop keeps running normally, but nothing is painted anymore. It never recovers by itself. Even if I remove my FlameGame and create a completely new one, it is still black. Only a full app restart brings it back.
I spent a long time on this because it looks exactly like a bug in my own game, but it is not.
After debugging inside Flame I found the reason. CameraComponent.componentsAtLocation is a sync* generator. It adds the camera to the static list CameraComponent.currentCameras at the start, and removes it only after the last yield:
if (checkContains(viewport, viewportPoint)) {
currentCameras.add(this);
...
yield* world!.componentsAtLocation(...);
currentCameras.removeLast(); // only runs if the caller reads to the end
}
If the caller stops reading early, the generator stays paused on a yield and the line that removes the camera never runs. Reading only until you find what you want is very normal:
for (final component in componentsAtPoint(tapPosition)) {
if (component is Chair) {
sitOn(component);
return; // the camera is leaked here
}
}
Because currentCameras is static, every leak stays forever. After maxCamerasDepth (4) leaks, CameraComponent.renderTree sees the list is full and skips its whole body, so the world, the viewfinder and the viewport all stop drawing, in every camera and every game, until the process is restarted.
There is also a smaller leak just below, on the if (worldPoint == null) return; early return, which skips the remove too.
I also see that FlameGame.containsEventHandlerAt uses the same early return inside a loop over componentsAtPoint, so a game using HitTestBehavior.translucent or deferToChild can hit this on normal pointer events.
What do you expect?
Stopping early when reading componentsAtPoint should be allowed. The camera should be put back to its normal state after the call, so the rendering keeps working no matter how many times you tap.
How can we reproduce this?
Flame 1.38.0, Flutter 3.44.7. I saw it on both iOS and Android, with Impeller and with Skia.
- Make a
FlameGame with one component in the world and one component in the viewport.
- Call
componentsAtPoint and stop the loop as soon as you find the world component (this is what a normal tap handler does).
- Do this four times.
- After the fourth time nothing renders anymore. The world component and the viewport component are both gone and never come back.
Full runnable example is below.
What steps should take to fix this?
The camera should be added and removed around each step of the iteration instead of around the whole iteration, inside a try/finally. This way a nested camera still sees the correct depth while the tree is walked, but if the caller stops early the camera is still removed and nothing leaks. The worldPoint == null early return leak goes away with the same change.
I already prepared a fix and opened a PR for it: #3971. It also adds a test that fails before the change and passes after.
Do have an example of where the bug occurs?
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
final game = _ReproGame();
void main() => runApp(const MaterialApp(home: _ReproApp()));
class _ReproApp extends StatelessWidget {
const _ReproApp();
@override
Widget build(BuildContext context) {
return Scaffold(
body: GameWidget(game: game),
floatingActionButton: FloatingActionButton.extended(
onPressed: game.findWhatWasTapped,
label: const Text('find'),
),
);
}
}
class _ReproGame extends FlameGame {
@override
Color backgroundColor() => const Color(0xFF2244AA);
late final Component target;
@override
Future<void> onLoad() async {
target = RectangleComponent(
size: Vector2(200, 200),
anchor: Anchor.center,
paint: Paint()..color = const Color(0xFFFFFFFF),
);
world.add(target);
camera.viewport.add(
RectangleComponent(
size: Vector2(60, 60),
position: Vector2(24, 24),
paint: Paint()..color = const Color(0xFFFF6600),
),
);
}
void findWhatWasTapped() {
for (final component in componentsAtPoint(size / 2)) {
if (component == target) {
return;
}
}
}
}
Press the "find" button four times. The white square and the orange square both disappear and do not come back.
Relevant log output
(nothing — no exception is thrown and no error is logged, the screen just stops rendering)
Output of: flutter doctor -v
[✓] Flutter (Channel stable, 3.44.7, on macOS 26.5.1 25F80 darwin-arm64, locale en-LB)
• Flutter version 3.44.7 on channel stable
• Dart version 3.12.2
[✓] Xcode - develop for iOS and macOS (Xcode 26.3)
[✓] Connected device (iOS device, iOS simulator, Android emulator)
[✓] Network resources
Affected platforms
All
Other information
The bug does not depend on any specific art or content. It is only about how componentsAtPoint is used. Reading the whole iterable (for example with .toList()) hides the problem, but that should not be required.
What happened?
In my game the whole screen goes black after a few taps. The world and everything drawn by the camera disappears, and only the background color stays. It happens after around four taps on components.
The strange part is that nothing is thrown. There is no exception and no error in the logs. The game loop keeps running normally, but nothing is painted anymore. It never recovers by itself. Even if I remove my
FlameGameand create a completely new one, it is still black. Only a full app restart brings it back.I spent a long time on this because it looks exactly like a bug in my own game, but it is not.
After debugging inside Flame I found the reason.
CameraComponent.componentsAtLocationis async*generator. It adds the camera to the static listCameraComponent.currentCamerasat the start, and removes it only after the lastyield:If the caller stops reading early, the generator stays paused on a
yieldand the line that removes the camera never runs. Reading only until you find what you want is very normal:Because
currentCamerasis static, every leak stays forever. AftermaxCamerasDepth(4) leaks,CameraComponent.renderTreesees the list is full and skips its whole body, so the world, the viewfinder and the viewport all stop drawing, in every camera and every game, until the process is restarted.There is also a smaller leak just below, on the
if (worldPoint == null) return;early return, which skips the remove too.I also see that
FlameGame.containsEventHandlerAtuses the same earlyreturninside a loop overcomponentsAtPoint, so a game usingHitTestBehavior.translucentordeferToChildcan hit this on normal pointer events.What do you expect?
Stopping early when reading
componentsAtPointshould be allowed. The camera should be put back to its normal state after the call, so the rendering keeps working no matter how many times you tap.How can we reproduce this?
Flame 1.38.0, Flutter 3.44.7. I saw it on both iOS and Android, with Impeller and with Skia.
FlameGamewith one component in the world and one component in the viewport.componentsAtPointand stop the loop as soon as you find the world component (this is what a normal tap handler does).Full runnable example is below.
What steps should take to fix this?
The camera should be added and removed around each step of the iteration instead of around the whole iteration, inside a
try/finally. This way a nested camera still sees the correct depth while the tree is walked, but if the caller stops early the camera is still removed and nothing leaks. TheworldPoint == nullearly return leak goes away with the same change.I already prepared a fix and opened a PR for it: #3971. It also adds a test that fails before the change and passes after.
Do have an example of where the bug occurs?
Press the "find" button four times. The white square and the orange square both disappear and do not come back.
Relevant log output
Output of: flutter doctor -v
[✓] Flutter (Channel stable, 3.44.7, on macOS 26.5.1 25F80 darwin-arm64, locale en-LB) • Flutter version 3.44.7 on channel stable • Dart version 3.12.2 [✓] Xcode - develop for iOS and macOS (Xcode 26.3) [✓] Connected device (iOS device, iOS simulator, Android emulator) [✓] Network resourcesAffected platforms
All
Other information
The bug does not depend on any specific art or content. It is only about how
componentsAtPointis used. Reading the whole iterable (for example with.toList()) hides the problem, but that should not be required.