ShapeHitbox keeps stale bounds if re-added after its ancestors moved; it also grows its ancestors list on every mount
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 35/100
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
- Ngôn ngữ chính
- Dart
- Star
- 10.8k
- Fork
- 1.1k
- Merge trung bình
- 1 ngày 20 giờ
- Pull request đã merge (30 ngày)
- 63
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của flame-engine/flame
-
bug
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 30/100
flame-engine/flame#4143 · 5 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
flame-engine/flame#4131 · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement v2
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
flame-engine/flame#4116 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
flame-engine/flame#4063 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
TextComponent shadows are not affected by OpacityEffectCó thể đã có người làm @spydon đã nhận 47 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 72/100
flame-engine/flame#4013 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của flame-engine/flame
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
linagora/tmail-flutter#5034 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`printf "%o"` prints decimalĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
fable-compiler/Fable#5157 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
bannzai/medicalarm#430 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
fossasia/badgemagic-app#2016 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area-vm
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
dart-lang/sdk#64727 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 4 ngày