ShapeHitbox keeps stale bounds if re-added after its ancestors moved; it also grows its ancestors list on every mount
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 35/100
Línea de trabajo
Work is already under way: PR #4142 contains the fix, so a newcomer should not start here. The bug lives in ShapeHitbox's onMount/onRemove logic, where cached aabb bounds and the _transformAncestors list are managed; the issue includes a full repro test covering stale bounds and the growing listener list. Reading the PR and the hitbox lifecycle code is the map; done means a re-added hitbox recomputes bounds at its current position and keeps one ancestor entry per mount.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
What happened?
A ShapeHitbox that is removed and later re-added has the following issues:
- Stale bounds: If any of its ancestors moved while the hitbox was removed, the hitbox keeps
its old cachedaabbonce it is added again. Collision detection then checks it at its old position:
raycasts miss it, collisions with it never start, andcontainsPointfails. This lasts until the hitbox
or one of its ancestors moves again while mounted. - Growing ancestors list: Each
onMountappends the transforms of the hitbox's ancestors
to_transformAncestors, and nothing ever clears it. Every removal walks the whole list and
callsremoveListeneronce per entry, so a hitbox that is removed and added again many times
(for example the hitbox of an animation frame, shown and hidden repeatedly) uses more memory
and takes longer to remove each time.
What do you expect?
- A re-added hitbox has the bounds of its current position, and collides and gets hit by rays
there. - A hitbox keeps one entry for each of its current ancestors, however many times it is mounted.
How can we reproduce this?
- Stale bounds:
- A
RectangleHitboxwith an explicit size sits in a parent at the origin. - Reading
aabbcaches its bounding box at the origin. - Remove the hitbox, move the parent to x = 100, then add the hitbox back.
- Its
aabbis still at x = 0, a raycast at x = 102 returnsnull, and a collider placed at
x = 105 gets noonCollisionStart. Each of these three checks fails on its own.
- The hitbox needs an explicit size: a hitbox without one resizes itself to its parent when
mounted, which happens to recompute its bounds and hides the bug.
- A
- Growing ancestors list:
- The parent uses a
Transform2Dsubclass that counts the calls toremoveListener. - Each removal of the hitbox should remove its listener from the parent once.
- Instead, the n-th removal calls
removeListenern times. The test stops at the second
removal, which makes 2 calls.
- The parent uses a
What steps should take to fix this?
The fix is available in PR #4142
Do have an example of where the bug occurs?
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class _Game extends FlameGame with HasCollisionDetection {}
class _Collider extends PositionComponent with CollisionCallbacks {
_Collider({super.position})
: super(size: Vector2.all(10), children: [RectangleHitbox()]);
int starts = 0;
@override
void onCollisionStart(List<Vector2> points, PositionComponent other) {
super.onCollisionStart(points, other);
starts++;
}
}
/// Counts the listeners removed from the transform of the component.
class _CountingTransform2D extends Transform2D {
int removed = 0;
@override
void removeListener(VoidCallback listener) {
removed++;
super.removeListener(listener);
}
}
class _CountingParent extends PositionComponent {
_CountingParent({super.children}) : super(size: Vector2.all(10));
final _transform = _CountingTransform2D();
@override
Transform2D get transform => _transform;
}
void main() {
testWithGame<_Game>(
'a re-added hitbox follows the moves of its parent while it was removed',
_Game.new,
(game) async {
// With a size, so that the hitbox does not fill its parent when mounted,
// which would compute its bounds again as a side effect.
final hitbox = RectangleHitbox(size: Vector2.all(10));
final parent = PositionComponent(
size: Vector2.all(10),
children: [hitbox],
);
final collider = _Collider(position: Vector2(105, 0));
await game.ensureAddAll([parent, collider]);
// Any query computes and caches the bounds of the hitbox, at the origin.
expect(hitbox.aabb.min.x, closeTo(0, 1e-9));
parent.remove(hitbox);
await game.ready();
// The hitbox stops following the transforms of its ancestors while it
// is removed, and keeps its cached bounds when it is added again.
parent.position.setValues(100, 0);
parent.add(hitbox);
await game.ready();
game.update(0);
expect(hitbox.aabb.min.x, closeTo(100, 1e-9)); // Actual: 0.
final ray = Ray2(origin: Vector2(102, -10), direction: Vector2(0, 1));
expect(game.collisionDetection.raycast(ray)?.hitbox, hitbox); // null.
expect(collider.starts, 1); // Actual: 0.
},
);
testWithGame<_Game>(
'a hitbox does not grow its list of ancestors each time it is mounted',
_Game.new,
(game) async {
final hitbox = RectangleHitbox();
final parent = _CountingParent(children: [hitbox]);
await game.ensureAdd(parent);
final transform = parent.transform as _CountingTransform2D;
for (var mounts = 1; mounts <= 3; mounts++) {
transform.removed = 0;
parent.remove(hitbox);
await game.ready();
// The listener of the hitbox is removed once, but onRemove walks all
// the ancestors collected by every onMount so far.
expect(transform.removed, 1, reason: 'removal $mounts'); // mounts.
parent.add(hitbox);
await game.ready();
}
},
);
}
Relevant log output
Execute in a terminal and put output into the code block below
[✓] Flutter (Channel stable, 3.47.8, on macOS 26.7.1 25G241 darwin-arm64, locale en-IT) [681ms]
• Flutter version 3.47.8 on channel stable at /Users/d.accornero/Applications/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 737ba8f885 (34 hours ago), 2026-10-22 21:37:31 -0700
• Engine revision cf636b1dfb
• Dart version 3.13.7
• DevTools version 2.60.0
• Feature flags: enable-web, no-enable-linux-desktop, enable-macos-desktop, no-enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use,
enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [1,137ms]
• Android SDK at /Users/d.accornero/Library/Android/sdk
• Emulator version 37.2.12.0 (build_id 16428233) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• ANDROID_HOME = /Users/d.accornero/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: flutter config --jdk-dir="path/to/jdk".
• Java version OpenJDK Runtime Environment (build 25.0.3+-15898627-b508.16)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 27.1) [1,318ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 27A9275
• CocoaPods version 1.17.0
[✓] Chrome - develop for the web [6ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [6.6s]
• macOS (desktop) • macos • darwin-arm64 • macOS 26.7.1 25G241 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 156.0.8078.48
[✓] Network resources [571ms]
• All expected network resources are available.
Affected platforms
All
Other information
No response
Are you interested in working on a PR for this?
- I want to work on this
- Lenguaje dominante
- Dart
- Estrellas
- 10.8k
- Forks
- 1.1k
- Merge medio
- 1 d 16 h
- PR fusionados (30 d)
- 60
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de flame-engine/flame
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 30/100
flame-engine/flame#4143 · 5 comentarios ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
flame-engine/flame#4131 · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
enhancement v2
Dificultad 4/5 3-5 días Aptitud para principiantes 25/100
flame-engine/flame#4116 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
flame-engine/flame#4063 ·
Los mantenedores suelen responder en 1 día
-
TextComponent shadows are not affected by OpacityEffectPosiblemente ocupada @spydon la tomó hace 49 días. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 72/100
flame-engine/flame#4013 ·
Los mantenedores suelen responder en 1 día
Todos los issues de flame-engine/flame
Issues similares
-
good first issue help wanted translation
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
blankframe-tech/ThrottleIQ#23 ·
Los mantenedores suelen responder en 5 días
-
good first issue mobile P0 size:S
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 69/100
trufi-association/trufi-core#1024 ·
Los mantenedores suelen responder en 14 días
-
Android: NV21 conversion allocates ~8x the frame size and makes an unused copy on every frameAbiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 83/100
-
@apps/catalog enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
ymm-oss/flutter-mobile-project-template#699 ·
Los mantenedores suelen responder en 15 días