Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Cerrado
#4,141 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@adario ya está trabajando en esto.

Desde el 24/10/2026.

  • #4142 de @adario — abierto

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
flutter
Área
game-dev

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

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
Lenguaje dominante
Dart
Estrellas
10.8k
Forks
1.1k
Merge medio
1 d 16 h
PR fusionados (30 d)
60

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de flame-engine/flame

Todos los issues de flame-engine/flame

Issues similares

Más issues de Dart

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.