Skip to content

Commit 51ace92

Browse files
committed
fix: Fix collision prospect checking to be fully reliable
1 parent 57b54d8 commit 51ace92

7 files changed

Lines changed: 145 additions & 89 deletions

File tree

packages/flame/lib/collisions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export 'src/collisions/broadphase/broadphase.dart';
2+
export 'src/collisions/broadphase/collision_prospect.dart';
23
export 'src/collisions/broadphase/prospect_pool.dart';
34
export 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart';
45
export 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart';

packages/flame/lib/src/collisions/broadphase/broadphase.dart

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:math';
2-
31
import 'package:flame/collisions.dart';
42

53
/// The [Broadphase] class is used to make collision detection more efficient
@@ -53,43 +51,3 @@ abstract class Broadphase<T extends Hitbox<T>> {
5351
/// Returns the potential hitbox collisions
5452
Iterable<CollisionProspect<T>> query();
5553
}
56-
57-
/// A [CollisionProspect] is a tuple that is used to contain two potentially
58-
/// colliding hitboxes.
59-
class CollisionProspect<T> {
60-
T _a;
61-
T _b;
62-
63-
T get a => _a;
64-
T get b => _b;
65-
66-
int get hash => _hash;
67-
int _hash;
68-
69-
CollisionProspect(this._a, this._b)
70-
: _hash = _pairHash(_a.hashCode, _b.hashCode);
71-
72-
/// Computes a hash for an unordered pair of hash codes that is much less
73-
/// likely to collide than a simple XOR.
74-
static int _pairHash(int h1, int h2) {
75-
return Object.hash(min(h1, h2), max(h1, h2));
76-
}
77-
78-
/// Sets the prospect to contain [a] and [b] instead of what it previously
79-
/// contained.
80-
void set(T a, T b) {
81-
_a = a;
82-
_b = b;
83-
_hash = _pairHash(a.hashCode, b.hashCode);
84-
}
85-
86-
/// Sets the prospect to contain the content of [other].
87-
void setFrom(CollisionProspect<T> other) {
88-
_a = other._a;
89-
_b = other._b;
90-
_hash = other._hash;
91-
}
92-
93-
/// Creates a new prospect object with the same content.
94-
CollisionProspect<T> clone() => CollisionProspect(_a, _b);
95-
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'dart:math';
2+
3+
import 'package:meta/meta.dart';
4+
5+
/// A [CollisionProspect] is an immutable view of a pair of two potentially
6+
/// colliding hitboxes.
7+
///
8+
/// Equality is based on unordered pair identity: {A, B} == {B, A}.
9+
@immutable
10+
abstract class CollisionProspect<T> {
11+
T get a;
12+
T get b;
13+
14+
@override
15+
int get hashCode {
16+
final h1 = a.hashCode;
17+
final h2 = b.hashCode;
18+
return Object.hash(min(h1, h2), max(h1, h2));
19+
}
20+
21+
@override
22+
bool operator ==(Object other) {
23+
if (other is! CollisionProspect) {
24+
return false;
25+
}
26+
return (identical(a, other.a) && identical(b, other.b)) ||
27+
(identical(a, other.b) && identical(b, other.a));
28+
}
29+
}
Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,111 @@
1-
import 'package:flame/src/collisions/broadphase/broadphase.dart';
1+
import 'dart:collection';
2+
import 'dart:math';
3+
4+
import 'package:flame/src/collisions/broadphase/collision_prospect.dart';
25
import 'package:flame/src/collisions/hitboxes/hitbox.dart';
36

4-
/// This pool is used to not create unnecessary [CollisionProspect] objects
5-
/// during collision detection, but to re-use the ones that have already been
6-
/// created.
7-
class ProspectPool<T extends Hitbox<T>> {
7+
/// A pool of [CollisionProspect] objects that are reused across frames to avoid
8+
/// per-frame allocations.
9+
///
10+
/// Internally uses a private mutable subclass but only exposes the immutable
11+
/// [CollisionProspect] interface. Implements [Iterable] over acquired entries.
12+
class ProspectPool<T extends Hitbox<T>>
13+
extends IterableBase<CollisionProspect<T>> {
814
ProspectPool({this.incrementSize = 1000});
915

1016
/// How much the pool should increase in size every time it needs to be made
1117
/// larger.
1218
final int incrementSize;
13-
final _storage = <CollisionProspect<T>>[];
14-
int get length => _storage.length;
15-
16-
/// The size of the pool will expand with [incrementSize] amount of
17-
/// [CollisionProspect]s that are initially populated with two [dummyItem]s.
18-
void expand(T dummyItem) {
19-
for (var i = 0; i < incrementSize; i++) {
20-
_storage.add(CollisionProspect<T>(dummyItem, dummyItem));
19+
final _storage = <_MutableCollisionProspect<T>>[];
20+
21+
/// The number of prospects currently acquired this frame.
22+
@override
23+
int get length => _count;
24+
int _count = 0;
25+
26+
@override
27+
Iterator<CollisionProspect<T>> get iterator =>
28+
_ProspectPoolIterator<T>(_storage, _count);
29+
30+
/// Returns a [CollisionProspect] populated with [a] and [b], reusing a
31+
/// pooled object or expanding the pool as needed.
32+
CollisionProspect<T> acquire(T a, T b) {
33+
if (_storage.length <= _count) {
34+
_expand(a);
35+
}
36+
final prospect = _storage[_count]..set(a, b);
37+
_count++;
38+
return prospect;
39+
}
40+
41+
/// Copies all prospects from [source] into the pool, expanding capacity at
42+
/// most once.
43+
void acquireAll(Iterable<CollisionProspect<T>> source) {
44+
final needed = source is List ? source.length : null;
45+
if (needed != null && _storage.length < _count + needed) {
46+
_expand(source.first.a, needed: needed);
47+
}
48+
for (final prospect in source) {
49+
acquire(prospect.a, prospect.b);
2150
}
2251
}
2352

53+
/// Resets the pool for the next frame. Previously acquired prospects should
54+
/// not be accessed after this call.
55+
void reset() {
56+
_count = 0;
57+
}
58+
59+
/// Returns the [CollisionProspect] at [index] (must be < [length]).
2460
CollisionProspect<T> operator [](int index) => _storage[index];
61+
62+
void _expand(T dummyItem, {int? needed}) {
63+
final actualIncrement = needed == null
64+
? incrementSize
65+
: max(needed, incrementSize);
66+
final target = _storage.length + actualIncrement;
67+
while (_storage.length < target) {
68+
_storage.add(_MutableCollisionProspect<T>(dummyItem, dummyItem));
69+
}
70+
}
71+
}
72+
73+
class _ProspectPoolIterator<T extends Hitbox<T>>
74+
implements Iterator<CollisionProspect<T>> {
75+
_ProspectPoolIterator(this._storage, this._length);
76+
77+
final List<_MutableCollisionProspect<T>> _storage;
78+
final int _length;
79+
int _index = -1;
80+
81+
@override
82+
CollisionProspect<T> get current => _storage[_index];
83+
84+
@override
85+
bool moveNext() => ++_index < _length;
86+
}
87+
88+
/// Private mutable implementation of [CollisionProspect] used exclusively by
89+
/// [ProspectPool] to reuse objects across frames.
90+
///
91+
/// Safety: mutation only happens inside [ProspectPool.acquire] and
92+
/// [ProspectPool.reset], which are never called while prospects are stored in
93+
/// hash-based collections. All access outside this file is through the
94+
/// immutable [CollisionProspect] interface.
95+
// ignore: must_be_immutable
96+
class _MutableCollisionProspect<T> extends CollisionProspect<T> {
97+
_MutableCollisionProspect(this._a, this._b);
98+
99+
T _a;
100+
T _b;
101+
102+
@override
103+
T get a => _a;
104+
@override
105+
T get b => _b;
106+
107+
void set(T a, T b) {
108+
_a = a;
109+
_b = b;
110+
}
25111
}

packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
4242

4343
final _cachedCenters = <ShapeHitbox, Vector2>{};
4444

45-
final _potentials = <int, CollisionProspect<ShapeHitbox>>{};
45+
final _potentials = <CollisionProspect<ShapeHitbox>>{};
4646
final _potentialsTmp = <ShapeHitbox>[];
4747
final _prospectPool = ProspectPool<ShapeHitbox>();
4848

@@ -53,6 +53,7 @@ class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
5353
Iterable<CollisionProspect<ShapeHitbox>> query() {
5454
_potentials.clear();
5555
_potentialsTmp.clear();
56+
_prospectPool.reset();
5657

5758
for (final activeItem in activeHitboxes) {
5859
if (activeItem.isRemoving || !activeItem.isMounted) {
@@ -96,12 +97,8 @@ class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
9697
final item0 = _potentialsTmp[i];
9798
final item1 = _potentialsTmp[i + 1];
9899
if (broadphaseCheck(item0, item1)) {
99-
final CollisionProspect<ShapeHitbox> prospect;
100-
if (_prospectPool.length <= i) {
101-
_prospectPool.expand(item0);
102-
}
103-
prospect = _prospectPool[i]..set(item0, item1);
104-
_potentials[prospect.hash] = prospect;
100+
final prospect = _prospectPool.acquire(item0, item1);
101+
_potentials.add(prospect);
105102
} else {
106103
if (_broadphaseCheckCache[item0] == null) {
107104
_broadphaseCheckCache[item0] = {};
@@ -110,7 +107,7 @@ class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
110107
}
111108
}
112109
}
113-
return _potentials.values;
110+
return _potentials;
114111
}
115112

116113
void updateTransform(ShapeHitbox item) {

packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
77
final List<T> items;
88

99
final _active = <T>[];
10-
final _potentials = <int, CollisionProspect<T>>{};
1110
final _prospectPool = ProspectPool<T>();
1211

1312
@override
@@ -24,7 +23,7 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
2423
@override
2524
Iterable<CollisionProspect<T>> query() {
2625
_active.clear();
27-
_potentials.clear();
26+
_prospectPool.reset();
2827

2928
for (final item in items) {
3029
if (item.collisionType == CollisionType.inactive) {
@@ -42,19 +41,14 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
4241
if (activeBox.max.x >= currentMin) {
4342
if (item.collisionType == CollisionType.active ||
4443
activeItem.collisionType == CollisionType.active) {
45-
if (_prospectPool.length <= _potentials.length) {
46-
_prospectPool.expand(item);
47-
}
48-
final prospect = _prospectPool[_potentials.length]
49-
..set(item, activeItem);
50-
_potentials[prospect.hash] = prospect;
44+
_prospectPool.acquire(item, activeItem);
5145
}
5246
} else {
5347
_active.remove(activeItem);
5448
}
5549
}
5650
_active.add(item);
5751
}
58-
return _potentials.values;
52+
return _prospectPool;
5953
}
6054
}

packages/flame/lib/src/collisions/collision_detection.dart

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ abstract class CollisionDetection<
1515
final B broadphase;
1616

1717
List<T> get items => broadphase.items;
18-
final _lastPotentials = <CollisionProspect<T>>[];
18+
final _lastProspectPool = ProspectPool<T>();
19+
final _currentPotentials = <CollisionProspect<T>>{};
1920
final collisionsCompletedNotifier = CollisionDetectionCompletionNotifier();
2021

2122
CollisionDetection({required this.broadphase});
@@ -36,9 +37,10 @@ abstract class CollisionDetection<
3637
void run() {
3738
broadphase.update();
3839
final potentials = broadphase.query();
39-
final hashes = Set.unmodifiable(potentials.map((p) => p.hash));
40+
_currentPotentials.clear();
4041

4142
for (final potential in potentials) {
43+
_currentPotentials.add(potential);
4244
final itemA = potential.a;
4345
final itemB = potential.b;
4446

@@ -59,8 +61,8 @@ abstract class CollisionDetection<
5961

6062
// Handles callbacks for an ended collision that the broadphase didn't
6163
// report as a potential collision anymore.
62-
for (final prospect in _lastPotentials) {
63-
if (!hashes.contains(prospect.hash) &&
64+
for (final prospect in _lastProspectPool) {
65+
if (!_currentPotentials.contains(prospect) &&
6466
prospect.a.collidingWith(prospect.b)) {
6567
handleCollisionEnd(prospect.a, prospect.b);
6668
}
@@ -71,20 +73,9 @@ abstract class CollisionDetection<
7173
collisionsCompletedNotifier.notifyListeners();
7274
}
7375

74-
final _lastPotentialsPool = <CollisionProspect<T>>[];
7576
void _updateLastPotentials(Iterable<CollisionProspect<T>> potentials) {
76-
_lastPotentials.clear();
77-
for (final potential in potentials) {
78-
final CollisionProspect<T> lastPotential;
79-
if (_lastPotentialsPool.length > _lastPotentials.length) {
80-
lastPotential = _lastPotentialsPool[_lastPotentials.length]
81-
..setFrom(potential);
82-
} else {
83-
lastPotential = potential.clone();
84-
_lastPotentialsPool.add(lastPotential);
85-
}
86-
_lastPotentials.add(lastPotential);
87-
}
77+
_lastProspectPool.reset();
78+
_lastProspectPool.acquireAll(potentials);
8879
}
8980

9081
/// Check what the intersection points of two items are,

0 commit comments

Comments
 (0)