perf!: Pool CollisionProspects and remove some list creations from the collision detection (#2625)

This change introduces a very simple pool for `CollisionProspect`s so
that those objects don't have to be re-created each tick.
It means that the `CollisionProspect` needs to be mutable though, so the
code becomes a little bit harder to read since sets can't be used
anymore.
This commit is contained in:
Lukas Klingsbo
2023-07-28 22:59:24 +02:00
committed by GitHub
parent 781e898315
commit e430b6cdf2
13 changed files with 181 additions and 112 deletions

View File

@ -4,7 +4,7 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: 2ad6cd72c040113b47ee9055e722606a490ef0da revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable channel: stable
project_type: app project_type: app
@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: android - platform: android
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: ios
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: linux
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: macos
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: web
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: windows
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
# User provided section # User provided section

View File

@ -1,6 +0,0 @@
import 'package:examples/main.dart' as examples;
import 'package:test/test.dart';
void main() {
test('main', examples.main);
}

View File

@ -1,4 +1,5 @@
export 'src/collisions/broadphase/broadphase.dart'; export 'src/collisions/broadphase/broadphase.dart';
export 'src/collisions/broadphase/prospect_pool.dart';
export 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart'; export 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart';
export 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart'; export 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart';
export 'src/collisions/broadphase/quadtree/quadtree.dart'; export 'src/collisions/broadphase/quadtree/quadtree.dart';

View File

@ -1,5 +1,4 @@
import 'package:flame/collisions.dart'; import 'package:flame/collisions.dart';
import 'package:meta/meta.dart';
/// The [Broadphase] class is used to make collision detection more efficient /// The [Broadphase] class is used to make collision detection more efficient
/// by doing a rough estimation of which hitboxes that can collide before their /// by doing a rough estimation of which hitboxes that can collide before their
@ -32,33 +31,56 @@ abstract class Broadphase<T extends Hitbox<T>> {
/// detection system. /// detection system.
void add(T item); void add(T item);
void addAll(Iterable<T> items) => items.forEach(add); void addAll(Iterable<T> items) {
for (final item in items) {
add(item);
}
}
/// Removes an item from the broadphase. Should be called in a /// Removes an item from the broadphase. Should be called in a
/// [CollisionDetection] class while removing a hitbox from its collision /// [CollisionDetection] class while removing a hitbox from its collision
/// detection system. /// detection system.
void remove(T item); void remove(T item);
void removeAll(Iterable<T> items) => items.forEach(remove); void removeAll(Iterable<T> items) {
for (final item in items) {
remove(item);
}
}
/// Returns the potential hitbox collisions /// Returns the potential hitbox collisions
Set<CollisionProspect<T>> query(); Iterable<CollisionProspect<T>> query();
} }
/// A [CollisionProspect] is a tuple that is used to contain two potentially /// A [CollisionProspect] is a tuple that is used to contain two potentially
/// colliding hitboxes. /// colliding hitboxes.
@immutable
class CollisionProspect<T> { class CollisionProspect<T> {
final T a; T _a;
final T b; T _b;
const CollisionProspect(this.a, this.b); T get a => _a;
T get b => _b;
@override int get hash => _hash;
bool operator ==(Object other) => int _hash;
other is CollisionProspect &&
((other.a == a && other.b == b) || (other.a == b && other.b == a));
@override CollisionProspect(this._a, this._b) : _hash = _a.hashCode ^ _b.hashCode;
int get hashCode => Object.hashAllUnordered([a, b]);
/// 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 = a.hashCode ^ b.hashCode;
}
/// Sets the prospect to contain the content of [other].
void setFrom(CollisionProspect<T> other) {
_a = other._a;
_b = other._b;
_hash = other._hash;
}
/// Creates a new prospect object with the same content.
CollisionProspect<T> clone() => CollisionProspect(_a, _b);
} }

View File

@ -0,0 +1,25 @@
import 'package:flame/src/collisions/broadphase/broadphase.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<T extends Hitbox<T>> {
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 = <CollisionProspect<T>>[];
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<T>(dummyItem, dummyItem));
}
}
CollisionProspect<T> operator [](int index) => _storage[index];
}

View File

@ -16,7 +16,7 @@ import 'package:flame/game.dart';
/// [initializeCollisionDetection] should be called in the game's [onLoad] /// [initializeCollisionDetection] should be called in the game's [onLoad]
/// method. /// method.
mixin HasQuadTreeCollisionDetection on FlameGame mixin HasQuadTreeCollisionDetection on FlameGame
implements HasCollisionDetection<QuadTreeBroadphase<ShapeHitbox>> { implements HasCollisionDetection<QuadTreeBroadphase> {
late QuadTreeCollisionDetection _collisionDetection; late QuadTreeCollisionDetection _collisionDetection;
@override @override
@ -24,7 +24,7 @@ mixin HasQuadTreeCollisionDetection on FlameGame
@override @override
set collisionDetection( set collisionDetection(
CollisionDetection<ShapeHitbox, QuadTreeBroadphase<ShapeHitbox>> cd, CollisionDetection<ShapeHitbox, QuadTreeBroadphase> cd,
) { ) {
if (cd is! QuadTreeCollisionDetection) { if (cd is! QuadTreeCollisionDetection) {
throw 'Must be QuadTreeCollisionDetection!'; throw 'Must be QuadTreeCollisionDetection!';

View File

@ -17,44 +17,43 @@ typedef ExternalMinDistanceCheck = bool Function(
/// ///
/// See [HasQuadTreeCollisionDetection.initializeCollisionDetection] for a /// See [HasQuadTreeCollisionDetection.initializeCollisionDetection] for a
/// detailed description of its initialization parameters. /// detailed description of its initialization parameters.
class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> { class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
QuadTreeBroadphase({ QuadTreeBroadphase({
required Rect mainBoxSize, required Rect mainBoxSize,
required this.broadphaseCheck, required this.broadphaseCheck,
required this.minimumDistanceCheck, required this.minimumDistanceCheck,
int maxObjects = 25, int maxObjects = 25,
int maxDepth = 10, int maxDepth = 10,
}) : tree = QuadTree<T>( }) : tree = QuadTree<ShapeHitbox>(
mainBoxSize: mainBoxSize, mainBoxSize: mainBoxSize,
maxObjects: maxObjects, maxObjects: maxObjects,
maxDepth: maxDepth, maxDepth: maxDepth,
); );
final QuadTree<T> tree; final QuadTree<ShapeHitbox> tree;
final activeCollisions = HashSet<T>(); final activeHitboxes = HashSet<ShapeHitbox>();
ExternalBroadphaseCheck broadphaseCheck; ExternalBroadphaseCheck broadphaseCheck;
ExternalMinDistanceCheck minimumDistanceCheck; ExternalMinDistanceCheck minimumDistanceCheck;
final _broadphaseCheckCache = <T, Map<T, bool>>{}; final _broadphaseCheckCache = <ShapeHitbox, Map<ShapeHitbox, bool>>{};
final _cachedCenters = <ShapeHitbox, Vector2>{}; final _cachedCenters = <ShapeHitbox, Vector2>{};
final _potentials = HashSet<CollisionProspect<T>>(); final _potentials = <int, CollisionProspect<ShapeHitbox>>{};
final _potentialsTmp = <List<ShapeHitbox>>[]; final _potentialsTmp = <ShapeHitbox>[];
final _prospectPool = ProspectPool<ShapeHitbox>();
@override @override
List<T> get items => tree.hitboxes; List<ShapeHitbox> get items => tree.hitboxes;
@override @override
HashSet<CollisionProspect<T>> query() { Iterable<CollisionProspect<ShapeHitbox>> query() {
_potentials.clear(); _potentials.clear();
_potentialsTmp.clear(); _potentialsTmp.clear();
for (final activeItem in activeCollisions) { for (final activeItem in activeHitboxes) {
final asShapeItem = activeItem as ShapeHitbox; if (activeItem.isRemoving || !activeItem.isMounted) {
if (asShapeItem.isRemoving || asShapeItem.parent == null) {
tree.remove(activeItem); tree.remove(activeItem);
continue; continue;
} }
@ -70,63 +69,69 @@ class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> {
continue; continue;
} }
final asShapePotential = potential as ShapeHitbox; if (!potential.allowSiblingCollision &&
potential.hitboxParent == activeItem.hitboxParent &&
if (asShapePotential.parent == asShapeItem.parent && potential.isMounted) {
asShapeItem.parent != null) {
continue; continue;
} }
final distanceCloseEnough = minimumDistanceCheck.call( final distanceCloseEnough = minimumDistanceCheck.call(
itemCenter, itemCenter,
_cacheCenterOfHitbox(asShapePotential), _cacheCenterOfHitbox(potential),
); );
if (distanceCloseEnough == false) { if (distanceCloseEnough == false) {
continue; continue;
} }
_potentialsTmp.add([asShapeItem, asShapePotential]); _potentialsTmp
..add(activeItem)
..add(potential);
} }
} }
if (_potentialsTmp.isNotEmpty) { if (_potentialsTmp.isNotEmpty) {
for (var i = 0; i < _potentialsTmp.length; i++) { for (var i = 0; i < _potentialsTmp.length; i += 2) {
final item0 = _potentialsTmp[i].first; final item0 = _potentialsTmp[i];
final item1 = _potentialsTmp[i].last; final item1 = _potentialsTmp[i + 1];
if (broadphaseCheck(item0, item1)) { if (broadphaseCheck(item0, item1)) {
_potentials.add(CollisionProspect(item0 as T, item1 as T)); final CollisionProspect<ShapeHitbox> prospect;
} else { if (_prospectPool.length <= i) {
if (_broadphaseCheckCache[item0 as T] == null) { _prospectPool.expand(item0);
_broadphaseCheckCache[item0 as T] = {};
} }
_broadphaseCheckCache[item0 as T]![item1 as T] = false; prospect = _prospectPool[i]..set(item0, item1);
_potentials[prospect.hash] = prospect;
} else {
if (_broadphaseCheckCache[item0] == null) {
_broadphaseCheckCache[item0] = {};
}
_broadphaseCheckCache[item0]![item1] = false;
} }
} }
} }
return _potentials; return _potentials.values;
} }
void updateTransform(T item) { void updateTransform(ShapeHitbox item) {
tree.remove(item, keepOldPosition: true); tree.remove(item, keepOldPosition: true);
_cacheCenterOfHitbox(item as ShapeHitbox); _cacheCenterOfHitbox(item);
tree.add(item); tree.add(item);
} }
@override @override
void add(T item) { void add(ShapeHitbox item) {
tree.add(item); tree.add(item);
if (item.collisionType == CollisionType.active) { if (item.collisionType == CollisionType.active) {
activeCollisions.add(item); activeHitboxes.add(item);
} }
_cacheCenterOfHitbox(item as ShapeHitbox); _cacheCenterOfHitbox(item);
} }
@override @override
void remove(T item) { void remove(ShapeHitbox item) {
tree.remove(item); tree.remove(item);
_cachedCenters.remove(item); _cachedCenters.remove(item);
if (item.collisionType == CollisionType.active) { if (item.collisionType == CollisionType.active) {
activeCollisions.remove(item); activeHitboxes.remove(item);
} }
final checkCache = _broadphaseCheckCache[item]; final checkCache = _broadphaseCheckCache[item];
@ -140,7 +145,7 @@ class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> {
void clear() { void clear() {
tree.clear(); tree.clear();
activeCollisions.clear(); activeHitboxes.clear();
_broadphaseCheckCache.clear(); _broadphaseCheckCache.clear();
_cachedCenters.clear(); _cachedCenters.clear();
} }

View File

@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart';
/// Do not use standard [items] list for components. Instead adds all components /// Do not use standard [items] list for components. Instead adds all components
/// into [QuadTreeBroadphase] class. /// into [QuadTreeBroadphase] class.
class QuadTreeCollisionDetection class QuadTreeCollisionDetection
extends StandardCollisionDetection<QuadTreeBroadphase<ShapeHitbox>> { extends StandardCollisionDetection<QuadTreeBroadphase> {
QuadTreeCollisionDetection({ QuadTreeCollisionDetection({
required Rect mapDimensions, required Rect mapDimensions,
required ExternalBroadphaseCheck onComponentTypeCheck, required ExternalBroadphaseCheck onComponentTypeCheck,
@ -14,7 +14,7 @@ class QuadTreeCollisionDetection
int maxObjects = 25, int maxObjects = 25,
int maxDepth = 10, int maxDepth = 10,
}) : super( }) : super(
broadphase: QuadTreeBroadphase<ShapeHitbox>( broadphase: QuadTreeBroadphase(
mainBoxSize: mapDimensions, mainBoxSize: mapDimensions,
maxObjects: maxObjects, maxObjects: maxObjects,
maxDepth: maxDepth, maxDepth: maxDepth,
@ -29,16 +29,16 @@ class QuadTreeCollisionDetection
@override @override
void add(ShapeHitbox item) { void add(ShapeHitbox item) {
item.onAabbChanged = () => _scheduledUpdate.add(item); item.onAabbChanged = () => _scheduledUpdate.add(item);
// ignore: prefer_function_declarations_over_variables void listenerCollisionType() {
final listenerCollisionType = () {
if (item.isMounted) { if (item.isMounted) {
if (item.collisionType == CollisionType.active) { if (item.collisionType == CollisionType.active) {
broadphase.activeCollisions.add(item); broadphase.activeHitboxes.add(item);
} else { } else {
broadphase.activeCollisions.remove(item); broadphase.activeHitboxes.remove(item);
} }
} }
}; }
item.collisionTypeNotifier.addListener(listenerCollisionType); item.collisionTypeNotifier.addListener(listenerCollisionType);
_listenerCollisionType[item] = listenerCollisionType; _listenerCollisionType[item] = listenerCollisionType;
@ -47,7 +47,9 @@ class QuadTreeCollisionDetection
@override @override
void addAll(Iterable<ShapeHitbox> items) { void addAll(Iterable<ShapeHitbox> items) {
items.forEach(add); for (final item in items) {
add(item);
}
} }
@override @override
@ -65,14 +67,16 @@ class QuadTreeCollisionDetection
@override @override
void removeAll(Iterable<ShapeHitbox> items) { void removeAll(Iterable<ShapeHitbox> items) {
broadphase.clear(); broadphase.clear();
items.forEach(remove); for (final item in items) {
remove(item);
}
} }
@override @override
void run() { void run() {
_scheduledUpdate.forEach( for (final hitbox in _scheduledUpdate) {
broadphase.updateTransform, broadphase.updateTransform(hitbox);
); }
_scheduledUpdate.clear(); _scheduledUpdate.clear();
super.run(); super.run();
} }

View File

@ -6,8 +6,9 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
@override @override
final List<T> items; final List<T> items;
late final List<T> _active = []; final _active = <T>[];
late final Set<CollisionProspect<T>> _potentials = {}; final _potentials = <int, CollisionProspect<T>>{};
final _prospectPool = ProspectPool<T>();
@override @override
void add(T item) => items.add(item); void add(T item) => items.add(item);
@ -21,9 +22,10 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
} }
@override @override
Set<CollisionProspect<T>> query() { Iterable<CollisionProspect<T>> query() {
_active.clear(); _active.clear();
_potentials.clear(); _potentials.clear();
for (final item in items) { for (final item in items) {
if (item.collisionType == CollisionType.inactive) { if (item.collisionType == CollisionType.inactive) {
continue; continue;
@ -40,7 +42,12 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
if (activeBox.max.x >= currentMin) { if (activeBox.max.x >= currentMin) {
if (item.collisionType == CollisionType.active || if (item.collisionType == CollisionType.active ||
activeItem.collisionType == CollisionType.active) { activeItem.collisionType == CollisionType.active) {
_potentials.add(CollisionProspect<T>(item, activeItem)); if (_prospectPool.length <= _potentials.length) {
_prospectPool.expand(item);
}
final prospect = _prospectPool[_potentials.length]
..set(item, activeItem);
_potentials[prospect.hash] = prospect;
} }
} else { } else {
_active.remove(activeItem); _active.remove(activeItem);
@ -48,6 +55,6 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
} }
_active.add(item); _active.add(item);
} }
return _potentials; return _potentials.values;
} }
} }

View File

@ -12,7 +12,7 @@ abstract class CollisionDetection<T extends Hitbox<T>,
final B broadphase; final B broadphase;
List<T> get items => broadphase.items; List<T> get items => broadphase.items;
final Set<CollisionProspect<T>> _lastPotentials = {}; final _lastPotentials = <CollisionProspect<T>>[];
CollisionDetection({required this.broadphase}); CollisionDetection({required this.broadphase});
@ -32,9 +32,11 @@ abstract class CollisionDetection<T extends Hitbox<T>,
void run() { void run() {
broadphase.update(); broadphase.update();
final potentials = broadphase.query(); final potentials = broadphase.query();
potentials.forEach((tuple) { final hashes = Set.unmodifiable(potentials.map((p) => p.hash));
final itemA = tuple.a;
final itemB = tuple.b; for (final potential in potentials) {
final itemA = potential.a;
final itemB = potential.b;
if (itemA.possiblyIntersects(itemB)) { if (itemA.possiblyIntersects(itemB)) {
final intersectionPoints = intersections(itemA, itemB); final intersectionPoints = intersections(itemA, itemB);
@ -49,18 +51,33 @@ abstract class CollisionDetection<T extends Hitbox<T>,
} else if (itemA.collidingWith(itemB)) { } else if (itemA.collidingWith(itemB)) {
handleCollisionEnd(itemA, itemB); handleCollisionEnd(itemA, itemB);
} }
}); }
// Handles callbacks for an ended collision that the broadphase didn't // Handles callbacks for an ended collision that the broadphase didn't
// reports as a potential collision anymore. // report as a potential collision anymore.
_lastPotentials.difference(potentials).forEach((tuple) { for (final prospect in _lastPotentials) {
if (tuple.a.collidingWith(tuple.b)) { if (!hashes.contains(prospect.hash) &&
handleCollisionEnd(tuple.a, tuple.b); prospect.a.collidingWith(prospect.b)) {
handleCollisionEnd(prospect.a, prospect.b);
} }
}); }
_lastPotentials _updateLastPotentials(potentials);
..clear() }
..addAll(potentials);
final _lastPotentialsPool = <CollisionProspect<T>>[];
void _updateLastPotentials(Iterable<CollisionProspect<T>> potentials) {
_lastPotentials.clear();
for (final potential in potentials) {
final CollisionProspect<T> lastPotential;
if (_lastPotentialsPool.length > _lastPotentials.length) {
lastPotential = _lastPotentialsPool[_lastPotentials.length]
..setFrom(potential);
} else {
lastPotential = potential.clone();
_lastPotentialsPool.add(lastPotential);
}
_lastPotentials.add(lastPotential);
}
} }
/// Check what the intersection points of two items are, /// Check what the intersection points of two items are,

View File

@ -405,14 +405,16 @@ void main() {
}, },
'component collision callbacks are not called with hitbox ' 'component collision callbacks are not called with hitbox '
'triggersParentCollision option': (game) async { 'triggersParentCollision option': (game) async {
final utilityHitboxA = TestHitbox()..triggersParentCollision = false; final utilityHitboxA = TestHitbox('hitboxA')
..triggersParentCollision = false;
final blockA = TestBlock( final blockA = TestBlock(
Vector2.all(10), Vector2.all(10),
Vector2.all(10), Vector2.all(10),
); );
blockA.add(utilityHitboxA); blockA.add(utilityHitboxA);
final utilityHitboxB = TestHitbox()..triggersParentCollision = false; final utilityHitboxB = TestHitbox('hitboxB')
..triggersParentCollision = false;
final blockB = TestBlock( final blockB = TestBlock(
Vector2.all(15), Vector2.all(15),
Vector2.all(10), Vector2.all(10),

View File

@ -53,8 +53,9 @@ class TestHitbox extends RectangleHitbox {
int startCounter = 0; int startCounter = 0;
int onCollisionCounter = 0; int onCollisionCounter = 0;
int endCounter = 0; int endCounter = 0;
String? name;
TestHitbox() { TestHitbox([this.name]) {
onCollisionCallback = (_, __) { onCollisionCallback = (_, __) {
onCollisionCounter++; onCollisionCounter++;
}; };
@ -65,6 +66,13 @@ class TestHitbox extends RectangleHitbox {
endCounter++; endCounter++;
}; };
} }
@override
String toString() {
return name == null
? '_TestHitbox[${identityHashCode(this)}]'
: '_TestHitbox[$name]';
}
} }
class CompositeTestHitbox extends CompositeHitbox { class CompositeTestHitbox extends CompositeHitbox {

View File

@ -104,7 +104,6 @@ linter:
- prefer_final_in_for_each - prefer_final_in_for_each
- prefer_final_locals - prefer_final_locals
- prefer_for_elements_to_map_fromIterable - prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables - prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases - prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions - prefer_if_elements_to_conditional_expressions