Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

ShapeHitbox keeps stale bounds if re-added after its ancestors moved; it also grows its ancestors list on every mount

Đã đóng
#4,141 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@adario đang làm issue này rồi.

Từ ngày 24/10/2026.

  • #4142 của @adario — đang mở

Đá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
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
flutter
Lĩnh vực
game-dev

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ả

bug
What happened?

A ShapeHitbox that is removed and later re-added has the following issues:

  1. Stale bounds: If any of its ancestors moved while the hitbox was removed, the hitbox keeps
    its old cached aabb once it is added again. Collision detection then checks it at its old position:
    raycasts miss it, collisions with it never start, and containsPoint fails. This lasts until the hitbox
    or one of its ancestors moves again while mounted.
  2. Growing ancestors list: Each onMount appends the transforms of the hitbox's ancestors
    to _transformAncestors, and nothing ever clears it. Every removal walks the whole list and
    calls removeListener once 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?
  1. A re-added hitbox has the bounds of its current position, and collides and gets hit by rays
    there.
  2. A hitbox keeps one entry for each of its current ancestors, however many times it is mounted.
How can we reproduce this?
  1. Stale bounds:
    1. A RectangleHitbox with an explicit size sits in a parent at the origin.
    2. Reading aabb caches its bounding box at the origin.
    3. Remove the hitbox, move the parent to x = 100, then add the hitbox back.
    4. Its aabb is still at x = 0, a raycast at x = 102 returns null, and a collider placed at
      x = 105 gets no onCollisionStart. 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.
  2. Growing ancestors list:
    1. The parent uses a Transform2D subclass that counts the calls to removeListener.
    2. Each removal of the hitbox should remove its listener from the parent once.
    3. Instead, the n-th removal calls removeListener n times. The test stops at the second
      removal, which makes 2 calls.
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

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của flame-engine/flame

Tất cả issue của flame-engine/flame

Issue tương tự

Thêm issue về Dart

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.