diff --git a/packages/flame/lib/collisions.dart b/packages/flame/lib/collisions.dart index 9a0dd436bdb..d7c3a21c2d2 100644 --- a/packages/flame/lib/collisions.dart +++ b/packages/flame/lib/collisions.dart @@ -1,4 +1,5 @@ export 'src/collisions/broadphase/broadphase.dart'; +export 'src/collisions/broadphase/collision_prospect.dart'; export 'src/collisions/broadphase/prospect_pool.dart'; export 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart'; export 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart'; diff --git a/packages/flame/lib/src/collisions/broadphase/broadphase.dart b/packages/flame/lib/src/collisions/broadphase/broadphase.dart index adcc62df0c6..571c28a0661 100644 --- a/packages/flame/lib/src/collisions/broadphase/broadphase.dart +++ b/packages/flame/lib/src/collisions/broadphase/broadphase.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flame/collisions.dart'; /// The [Broadphase] class is used to make collision detection more efficient @@ -53,43 +51,3 @@ abstract class Broadphase> { /// Returns the potential hitbox collisions Iterable> query(); } - -/// A [CollisionProspect] is a tuple that is used to contain two potentially -/// colliding hitboxes. -class CollisionProspect { - T _a; - T _b; - - T get a => _a; - T get b => _b; - - int get hash => _hash; - int _hash; - - CollisionProspect(this._a, this._b) - : _hash = _pairHash(_a.hashCode, _b.hashCode); - - /// Computes a hash for an unordered pair of hash codes that is much less - /// likely to collide than a simple XOR. - static int _pairHash(int h1, int h2) { - return Object.hash(min(h1, h2), max(h1, h2)); - } - - /// Sets the prospect to contain [a] and [b] instead of what it previously - /// contained. - void set(T a, T b) { - _a = a; - _b = b; - _hash = _pairHash(a.hashCode, b.hashCode); - } - - /// Sets the prospect to contain the content of [other]. - void setFrom(CollisionProspect other) { - _a = other._a; - _b = other._b; - _hash = other._hash; - } - - /// Creates a new prospect object with the same content. - CollisionProspect clone() => CollisionProspect(_a, _b); -} diff --git a/packages/flame/lib/src/collisions/broadphase/collision_prospect.dart b/packages/flame/lib/src/collisions/broadphase/collision_prospect.dart new file mode 100644 index 00000000000..38b7e53edb4 --- /dev/null +++ b/packages/flame/lib/src/collisions/broadphase/collision_prospect.dart @@ -0,0 +1,29 @@ +import 'dart:math'; + +import 'package:meta/meta.dart'; + +/// A [CollisionProspect] is an immutable view of a pair of two potentially +/// colliding hitboxes. +/// +/// Equality is based on unordered pair identity: {A, B} == {B, A}. +@immutable +abstract class CollisionProspect { + T get a; + T get b; + + @override + int get hashCode { + final h1 = a.hashCode; + final h2 = b.hashCode; + return Object.hash(min(h1, h2), max(h1, h2)); + } + + @override + bool operator ==(Object other) { + if (other is! CollisionProspect) { + return false; + } + return (identical(a, other.a) && identical(b, other.b)) || + (identical(a, other.b) && identical(b, other.a)); + } +} diff --git a/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart b/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart index 9dcf9e303bd..bf67d2b2085 100644 --- a/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart +++ b/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart @@ -1,25 +1,111 @@ -import 'package:flame/src/collisions/broadphase/broadphase.dart'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:flame/src/collisions/broadphase/collision_prospect.dart'; import 'package:flame/src/collisions/hitboxes/hitbox.dart'; -/// This pool is used to not create unnecessary [CollisionProspect] objects -/// during collision detection, but to re-use the ones that have already been -/// created. -class ProspectPool> { +/// A pool of [CollisionProspect] objects that are reused across frames to avoid +/// per-frame allocations. +/// +/// Internally uses a private mutable subclass but only exposes the immutable +/// [CollisionProspect] interface. Implements [Iterable] over acquired entries. +class ProspectPool> + extends IterableBase> { ProspectPool({this.incrementSize = 1000}); /// How much the pool should increase in size every time it needs to be made /// larger. final int incrementSize; - final _storage = >[]; - int get length => _storage.length; - - /// The size of the pool will expand with [incrementSize] amount of - /// [CollisionProspect]s that are initially populated with two [dummyItem]s. - void expand(T dummyItem) { - for (var i = 0; i < incrementSize; i++) { - _storage.add(CollisionProspect(dummyItem, dummyItem)); + final _storage = <_MutableCollisionProspect>[]; + + /// The number of prospects currently acquired this frame. + @override + int get length => _count; + int _count = 0; + + @override + Iterator> get iterator => + _ProspectPoolIterator(_storage, _count); + + /// Returns a [CollisionProspect] populated with [a] and [b], reusing a + /// pooled object or expanding the pool as needed. + CollisionProspect acquire(T a, T b) { + if (_storage.length <= _count) { + _expand(a); + } + final prospect = _storage[_count]..set(a, b); + _count++; + return prospect; + } + + /// Copies all prospects from [source] into the pool, expanding capacity at + /// most once. + void acquireAll(Iterable> source) { + final needed = source is List ? source.length : null; + if (needed != null && _storage.length < _count + needed) { + _expand(source.first.a, needed: needed); + } + for (final prospect in source) { + acquire(prospect.a, prospect.b); } } + /// Resets the pool for the next frame. Previously acquired prospects should + /// not be accessed after this call. + void reset() { + _count = 0; + } + + /// Returns the [CollisionProspect] at [index] (must be < [length]). CollisionProspect operator [](int index) => _storage[index]; + + void _expand(T dummyItem, {int? needed}) { + final actualIncrement = needed == null + ? incrementSize + : max(needed, incrementSize); + final target = _storage.length + actualIncrement; + while (_storage.length < target) { + _storage.add(_MutableCollisionProspect(dummyItem, dummyItem)); + } + } +} + +class _ProspectPoolIterator> + implements Iterator> { + _ProspectPoolIterator(this._storage, this._length); + + final List<_MutableCollisionProspect> _storage; + final int _length; + int _index = -1; + + @override + CollisionProspect get current => _storage[_index]; + + @override + bool moveNext() => ++_index < _length; +} + +/// Private mutable implementation of [CollisionProspect] used exclusively by +/// [ProspectPool] to reuse objects across frames. +/// +/// Safety: mutation only happens inside [ProspectPool.acquire] and +/// [ProspectPool.reset], which are never called while prospects are stored in +/// hash-based collections. All access outside this file is through the +/// immutable [CollisionProspect] interface. +// ignore: must_be_immutable +class _MutableCollisionProspect extends CollisionProspect { + _MutableCollisionProspect(this._a, this._b); + + T _a; + T _b; + + @override + T get a => _a; + @override + T get b => _b; + + void set(T a, T b) { + _a = a; + _b = b; + } } diff --git a/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart b/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart index 2c93c01dc72..a2789d938b5 100644 --- a/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart +++ b/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart @@ -42,7 +42,7 @@ class QuadTreeBroadphase extends Broadphase { final _cachedCenters = {}; - final _potentials = >{}; + final _potentials = >{}; final _potentialsTmp = []; final _prospectPool = ProspectPool(); @@ -53,6 +53,7 @@ class QuadTreeBroadphase extends Broadphase { Iterable> query() { _potentials.clear(); _potentialsTmp.clear(); + _prospectPool.reset(); for (final activeItem in activeHitboxes) { if (activeItem.isRemoving || !activeItem.isMounted) { @@ -96,12 +97,8 @@ class QuadTreeBroadphase extends Broadphase { final item0 = _potentialsTmp[i]; final item1 = _potentialsTmp[i + 1]; if (broadphaseCheck(item0, item1)) { - final CollisionProspect prospect; - if (_prospectPool.length <= i) { - _prospectPool.expand(item0); - } - prospect = _prospectPool[i]..set(item0, item1); - _potentials[prospect.hash] = prospect; + final prospect = _prospectPool.acquire(item0, item1); + _potentials.add(prospect); } else { if (_broadphaseCheckCache[item0] == null) { _broadphaseCheckCache[item0] = {}; @@ -110,7 +107,7 @@ class QuadTreeBroadphase extends Broadphase { } } } - return _potentials.values; + return _potentials; } void updateTransform(ShapeHitbox item) { diff --git a/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart b/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart index 86b2e33aa68..f79cc6e33e0 100644 --- a/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart +++ b/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart @@ -7,7 +7,6 @@ class Sweep> extends Broadphase { final List items; final _active = []; - final _potentials = >{}; final _prospectPool = ProspectPool(); @override @@ -24,7 +23,7 @@ class Sweep> extends Broadphase { @override Iterable> query() { _active.clear(); - _potentials.clear(); + _prospectPool.reset(); for (final item in items) { if (item.collisionType == CollisionType.inactive) { @@ -42,12 +41,7 @@ class Sweep> extends Broadphase { if (activeBox.max.x >= currentMin) { if (item.collisionType == CollisionType.active || activeItem.collisionType == CollisionType.active) { - if (_prospectPool.length <= _potentials.length) { - _prospectPool.expand(item); - } - final prospect = _prospectPool[_potentials.length] - ..set(item, activeItem); - _potentials[prospect.hash] = prospect; + _prospectPool.acquire(item, activeItem); } } else { _active.remove(activeItem); @@ -55,6 +49,6 @@ class Sweep> extends Broadphase { } _active.add(item); } - return _potentials.values; + return _prospectPool; } } diff --git a/packages/flame/lib/src/collisions/collision_detection.dart b/packages/flame/lib/src/collisions/collision_detection.dart index c4aef62e1c6..d1669198fec 100644 --- a/packages/flame/lib/src/collisions/collision_detection.dart +++ b/packages/flame/lib/src/collisions/collision_detection.dart @@ -15,7 +15,8 @@ abstract class CollisionDetection< final B broadphase; List get items => broadphase.items; - final _lastPotentials = >[]; + final _lastProspectPool = ProspectPool(); + final _currentPotentials = >{}; final collisionsCompletedNotifier = CollisionDetectionCompletionNotifier(); CollisionDetection({required this.broadphase}); @@ -36,9 +37,10 @@ abstract class CollisionDetection< void run() { broadphase.update(); final potentials = broadphase.query(); - final hashes = Set.unmodifiable(potentials.map((p) => p.hash)); + _currentPotentials.clear(); for (final potential in potentials) { + _currentPotentials.add(potential); final itemA = potential.a; final itemB = potential.b; @@ -59,8 +61,8 @@ abstract class CollisionDetection< // Handles callbacks for an ended collision that the broadphase didn't // report as a potential collision anymore. - for (final prospect in _lastPotentials) { - if (!hashes.contains(prospect.hash) && + for (final prospect in _lastProspectPool) { + if (!_currentPotentials.contains(prospect) && prospect.a.collidingWith(prospect.b)) { handleCollisionEnd(prospect.a, prospect.b); } @@ -71,20 +73,9 @@ abstract class CollisionDetection< collisionsCompletedNotifier.notifyListeners(); } - final _lastPotentialsPool = >[]; void _updateLastPotentials(Iterable> potentials) { - _lastPotentials.clear(); - for (final potential in potentials) { - final CollisionProspect lastPotential; - if (_lastPotentialsPool.length > _lastPotentials.length) { - lastPotential = _lastPotentialsPool[_lastPotentials.length] - ..setFrom(potential); - } else { - lastPotential = potential.clone(); - _lastPotentialsPool.add(lastPotential); - } - _lastPotentials.add(lastPotential); - } + _lastProspectPool.reset(); + _lastProspectPool.acquireAll(potentials); } /// Check what the intersection points of two items are,