ShapeHitbox keeps stale bounds if re-added after its ancestors moved; it also grows its ancestors list on every mount
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
説明
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
- 主要言語
- Dart
- スター
- 10.8k
- フォーク
- 1.1k
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 60
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
flame-engine/flame のほかの issue
-
bug
難易度 4/5 3〜5日 初心者へのやさしさ 30/100
flame-engine/flame#4143 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
flame-engine/flame#4131 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement v2
難易度 4/5 3〜5日 初心者へのやさしさ 25/100
flame-engine/flame#4116 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
flame-engine/flame#4063 ·
メンテナーはふだん 1 日以内に返信
-
TextComponent shadows are not affected by OpacityEffect対応中かも @spydon が 49 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
flame-engine/flame#4013 ·
メンテナーはふだん 1 日以内に返信
flame-engine/flame の issue をすべて見る
似ている issue
-
good first issue help wanted translation
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
blankframe-tech/ThrottleIQ#23 ·
メンテナーはふだん 5 日以内に返信
-
good first issue mobile P0 size:S
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 69/100
trufi-association/trufi-core#1024 ·
メンテナーはふだん 14 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 83/100
-
@apps/catalog enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
ymm-oss/flutter-mobile-project-template#699 ·
メンテナーはふだん 15 日以内に返信