Skip to content

Commit

Permalink
perf!: Pool CollisionProspects and remove some list creations from …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
spydon committed Jul 28, 2023
1 parent 781e898 commit e430b6c
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 111 deletions.
25 changes: 5 additions & 20 deletions examples/.metadata
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled.

version:
revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable

project_type: app
Expand All @@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: android
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- 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
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8

# User provided section

Expand Down
6 changes: 0 additions & 6 deletions examples/test/main_test.dart

This file was deleted.

1 change: 1 addition & 0 deletions packages/flame/lib/collisions.dart
@@ -1,4 +1,5 @@
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/quad_tree_broadphase.dart';
export 'src/collisions/broadphase/quadtree/quadtree.dart';
Expand Down
50 changes: 36 additions & 14 deletions packages/flame/lib/src/collisions/broadphase/broadphase.dart
@@ -1,5 +1,4 @@
import 'package:flame/collisions.dart';
import 'package:meta/meta.dart';

/// The [Broadphase] class is used to make collision detection more efficient
/// by doing a rough estimation of which hitboxes that can collide before their
Expand Down Expand Up @@ -32,33 +31,56 @@ abstract class Broadphase<T extends Hitbox<T>> {
/// detection system.
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
/// [CollisionDetection] class while removing a hitbox from its collision
/// detection system.
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
Set<CollisionProspect<T>> query();
Iterable<CollisionProspect<T>> query();
}

/// A [CollisionProspect] is a tuple that is used to contain two potentially
/// colliding hitboxes.
@immutable
class CollisionProspect<T> {
final T a;
final T b;
T _a;
T _b;

const CollisionProspect(this.a, this.b);
T get a => _a;
T get b => _b;

@override
bool operator ==(Object other) =>
other is CollisionProspect &&
((other.a == a && other.b == b) || (other.a == b && other.b == a));
int get hash => _hash;
int _hash;

@override
int get hashCode => Object.hashAllUnordered([a, b]);
CollisionProspect(this._a, this._b) : _hash = _a.hashCode ^ _b.hashCode;

/// 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);
}
25 changes: 25 additions & 0 deletions packages/flame/lib/src/collisions/broadphase/prospect_pool.dart
@@ -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];
}
Expand Up @@ -16,15 +16,15 @@ import 'package:flame/game.dart';
/// [initializeCollisionDetection] should be called in the game's [onLoad]
/// method.
mixin HasQuadTreeCollisionDetection on FlameGame
implements HasCollisionDetection<QuadTreeBroadphase<ShapeHitbox>> {
implements HasCollisionDetection<QuadTreeBroadphase> {
late QuadTreeCollisionDetection _collisionDetection;

@override
QuadTreeCollisionDetection get collisionDetection => _collisionDetection;

@override
set collisionDetection(
CollisionDetection<ShapeHitbox, QuadTreeBroadphase<ShapeHitbox>> cd,
CollisionDetection<ShapeHitbox, QuadTreeBroadphase> cd,
) {
if (cd is! QuadTreeCollisionDetection) {
throw 'Must be QuadTreeCollisionDetection!';
Expand Down
Expand Up @@ -17,44 +17,43 @@ typedef ExternalMinDistanceCheck = bool Function(
///
/// See [HasQuadTreeCollisionDetection.initializeCollisionDetection] for a
/// detailed description of its initialization parameters.
class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> {
class QuadTreeBroadphase extends Broadphase<ShapeHitbox> {
QuadTreeBroadphase({
required Rect mainBoxSize,
required this.broadphaseCheck,
required this.minimumDistanceCheck,
int maxObjects = 25,
int maxDepth = 10,
}) : tree = QuadTree<T>(
}) : tree = QuadTree<ShapeHitbox>(
mainBoxSize: mainBoxSize,
maxObjects: maxObjects,
maxDepth: maxDepth,
);

final QuadTree<T> tree;
final QuadTree<ShapeHitbox> tree;

final activeCollisions = HashSet<T>();
final activeHitboxes = HashSet<ShapeHitbox>();

ExternalBroadphaseCheck broadphaseCheck;
ExternalMinDistanceCheck minimumDistanceCheck;
final _broadphaseCheckCache = <T, Map<T, bool>>{};
final _broadphaseCheckCache = <ShapeHitbox, Map<ShapeHitbox, bool>>{};

final _cachedCenters = <ShapeHitbox, Vector2>{};

final _potentials = HashSet<CollisionProspect<T>>();
final _potentialsTmp = <List<ShapeHitbox>>[];
final _potentials = <int, CollisionProspect<ShapeHitbox>>{};
final _potentialsTmp = <ShapeHitbox>[];
final _prospectPool = ProspectPool<ShapeHitbox>();

@override
List<T> get items => tree.hitboxes;
List<ShapeHitbox> get items => tree.hitboxes;

@override
HashSet<CollisionProspect<T>> query() {
Iterable<CollisionProspect<ShapeHitbox>> query() {
_potentials.clear();
_potentialsTmp.clear();

for (final activeItem in activeCollisions) {
final asShapeItem = activeItem as ShapeHitbox;

if (asShapeItem.isRemoving || asShapeItem.parent == null) {
for (final activeItem in activeHitboxes) {
if (activeItem.isRemoving || !activeItem.isMounted) {
tree.remove(activeItem);
continue;
}
Expand All @@ -70,63 +69,69 @@ class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> {
continue;
}

final asShapePotential = potential as ShapeHitbox;

if (asShapePotential.parent == asShapeItem.parent &&
asShapeItem.parent != null) {
if (!potential.allowSiblingCollision &&
potential.hitboxParent == activeItem.hitboxParent &&
potential.isMounted) {
continue;
}

final distanceCloseEnough = minimumDistanceCheck.call(
itemCenter,
_cacheCenterOfHitbox(asShapePotential),
_cacheCenterOfHitbox(potential),
);
if (distanceCloseEnough == false) {
continue;
}

_potentialsTmp.add([asShapeItem, asShapePotential]);
_potentialsTmp
..add(activeItem)
..add(potential);
}
}

if (_potentialsTmp.isNotEmpty) {
for (var i = 0; i < _potentialsTmp.length; i++) {
final item0 = _potentialsTmp[i].first;
final item1 = _potentialsTmp[i].last;
for (var i = 0; i < _potentialsTmp.length; i += 2) {
final item0 = _potentialsTmp[i];
final item1 = _potentialsTmp[i + 1];
if (broadphaseCheck(item0, item1)) {
_potentials.add(CollisionProspect(item0 as T, item1 as T));
final CollisionProspect<ShapeHitbox> prospect;
if (_prospectPool.length <= i) {
_prospectPool.expand(item0);
}
prospect = _prospectPool[i]..set(item0, item1);
_potentials[prospect.hash] = prospect;
} else {
if (_broadphaseCheckCache[item0 as T] == null) {
_broadphaseCheckCache[item0 as T] = {};
if (_broadphaseCheckCache[item0] == null) {
_broadphaseCheckCache[item0] = {};
}
_broadphaseCheckCache[item0 as T]![item1 as T] = false;
_broadphaseCheckCache[item0]![item1] = false;
}
}
}
return _potentials;
return _potentials.values;
}

void updateTransform(T item) {
void updateTransform(ShapeHitbox item) {
tree.remove(item, keepOldPosition: true);
_cacheCenterOfHitbox(item as ShapeHitbox);
_cacheCenterOfHitbox(item);
tree.add(item);
}

@override
void add(T item) {
void add(ShapeHitbox item) {
tree.add(item);
if (item.collisionType == CollisionType.active) {
activeCollisions.add(item);
activeHitboxes.add(item);
}
_cacheCenterOfHitbox(item as ShapeHitbox);
_cacheCenterOfHitbox(item);
}

@override
void remove(T item) {
void remove(ShapeHitbox item) {
tree.remove(item);
_cachedCenters.remove(item);
if (item.collisionType == CollisionType.active) {
activeCollisions.remove(item);
activeHitboxes.remove(item);
}

final checkCache = _broadphaseCheckCache[item];
Expand All @@ -140,7 +145,7 @@ class QuadTreeBroadphase<T extends Hitbox<T>> extends Broadphase<T> {

void clear() {
tree.clear();
activeCollisions.clear();
activeHitboxes.clear();
_broadphaseCheckCache.clear();
_cachedCenters.clear();
}
Expand Down

0 comments on commit e430b6c

Please sign in to comment.