Skip to content

Commit

Permalink
feat: collision optimizer isolate version
Browse files Browse the repository at this point in the history
ASGAlex committed Oct 16, 2023
1 parent 8bebd88 commit 503cc7d
Showing 5 changed files with 289 additions and 28 deletions.
34 changes: 20 additions & 14 deletions example/lib/game.dart
Original file line number Diff line number Diff line change
@@ -121,19 +121,19 @@ all collisions are disabled.
buildCellsPerUpdate: buildCellsPerUpdate,
cleanupCellsPerUpdate: cleanupCellsPerUpdate,
suspendedCellLifetime: const Duration(minutes: 10),
scheduledLayerOperationLimit: 1,
scheduledLayerOperationLimit: 5,
cellBuilderNoMap: noMapCellBuilder,
maps: [
DemoMapLoader(Vector2(600, 0)),
],
worldLoader: WorldLoader(
fileName: 'example.world',
loadWholeMap: false,
mapLoader: {
'example': DemoMapLoader.new,
'another_map': DemoMapLoader.new,
},
),
// maps: [
// DemoMapLoader(Vector2(600, 0)),
// ],
// worldLoader: WorldLoader(
// fileName: 'example.world',
// loadWholeMap: false,
// mapLoader: {
// 'example': DemoMapLoader.new,
// 'another_map': DemoMapLoader.new,
// },
// ),
);
// await demoMapLoader.init(this);

@@ -299,15 +299,21 @@ all collisions are disabled.
hideLoadingComponent();
}

bool nomapFinished = false;

Future<void> noMapCellBuilder(
Cell cell,
Component rootComponent,
Iterable<Rect> mapRects,
) async {
// return;
if (mapRects.isNotEmpty) {

if (mapRects.isNotEmpty || nomapFinished) {
return;
}
// if (!nomapFinished) {
// nomapFinished = true;
// }

final brickTile = tilesetManager.getTile('tileset', 'Brick');
final waterTile = tilesetManager.getTile('tileset', 'Water');
@@ -432,7 +438,7 @@ class MyWorld extends World with HasGameRef<SpatialGridExample> {
}

void spawnNpcTeam([bool aiEnabled = false]) {
// return;
return;
for (var i = 1; i <= 80; i++) {
final x = i <= 40 ? 10.0 * i : 10.0 * (i - 40);
final y = i <= 40 ? 0.0 : -20.0;
267 changes: 257 additions & 10 deletions lib/src/collisions/optimizer/collision_optimizer.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,145 @@
import 'dart:collection';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_spatial_grid/flame_spatial_grid.dart';
import 'package:flame_spatial_grid/src/collisions/optimizer/extensions.dart';
import 'package:flame_spatial_grid/src/collisions/optimizer/optimized_collisions_list.dart';
import 'package:isolate_manager/isolate_manager.dart';
import 'package:meta/meta.dart';

@immutable
class OverlappingSearchRequest {
const OverlappingSearchRequest({
required this.hitboxes,
required this.target,
});

final List<BoundingHitboxHydrated> hitboxes;
final BoundingHitboxHydrated target;
}

class OverlappedSearchResponse {
OverlappedSearchResponse(this.index, this.hydratedRect);

final int index;
final Float64List hydratedRect;

Rect get rect => Rect.fromLTRB(
hydratedRect[0],
hydratedRect[1],
hydratedRect[2],
hydratedRect[3],
);
}

class BoundingHitboxHydrated {
BoundingHitboxHydrated(BoundingHitbox hitbox, this.index) {
aabb = hitbox.aabb;
positionStorage = hitbox.position.storage;

parentPositionStorage =
(hitbox.parent as PositionComponent?)?.position.storage;
sizeStorage = hitbox.size.storage;
skip = hitbox.collisionType == CollisionType.inactive;
}

late final Float64List sizeStorage;
late final Float64List positionStorage;
late final Float64List? parentPositionStorage;
late final Aabb2 aabb;
final int index;
late final bool skip;

Rect? _rectCache;
late Float64List _hydratedRect;

Float64List get hydratedRect {
toRectSpecial();
return _hydratedRect;
}

Rect toRectSpecial() {
final cache = _rectCache;
if (cache != null) {
return cache;
} else {
if (parentPositionStorage == null) {
return Rect.zero;
} else {
final parentPosition = Vector2.fromFloat64List(parentPositionStorage!);
final position = Vector2.fromFloat64List(positionStorage);
final size = Vector2.fromFloat64List(sizeStorage);
final cache = Rect.fromLTWH(
parentPosition.x + position.x,
parentPosition.y + position.y,
size.x,
size.y,
);

_hydratedRect = Float64List.fromList([
cache.left,
cache.top,
cache.right,
cache.bottom,
]);

_rectCache = cache;
return cache;
}
}
}
}

@pragma('vm:entry-point')
Iterable<OverlappedSearchResponse> findOverlappingRectsIsolated(
OverlappingSearchRequest parameters,
) =>
_findOverlappingRects(parameters.target, parameters.hitboxes);

Iterable<OverlappedSearchResponse> _findOverlappingRects(
BoundingHitboxHydrated target,
List<BoundingHitboxHydrated> hitboxesForOptimization, [
Set<int>? excludedIndices,
]) {
final hitboxes = <OverlappedSearchResponse>[];
hitboxes.add(OverlappedSearchResponse(target.index, target.hydratedRect));
if (excludedIndices != null) {
excludedIndices.add(target.index);
} else {
excludedIndices = <int>{};
}
for (final otherHitbox in hitboxesForOptimization) {
if (otherHitbox.skip ||
otherHitbox.index == target.index ||
excludedIndices.contains(otherHitbox.index)) {
continue;
}
if (target.toRectSpecial().overlapsSpecial(otherHitbox.toRectSpecial())) {
hitboxes.addAll(
_findOverlappingRects(
otherHitbox,
hitboxesForOptimization,
excludedIndices,
),
);
}
}
return hitboxes;
}

class CollisionOptimizer {
CollisionOptimizer(this.parentLayer);
CollisionOptimizer(this.parentLayer) {
_isolateManager ??= IsolateManager.create(
findOverlappingRectsIsolated,
concurrent: 4,
);
}

static IsolateManager<Iterable<OverlappedSearchResponse>,
OverlappingSearchRequest>? _isolateManager;

final CellLayer parentLayer;
final _createdCollisionLists = <OptimizedCollisionList>[];
@@ -33,7 +162,118 @@ class CollisionOptimizer {
@internal
static final rectCache = <PositionComponent, Rect>{};

void optimize() {
Future optimizeIsolated() async {
final cell = clear();
if (cell == null) {
return;
}

final optimizedCollisionsByGroupBox =
game.collisionDetection.broadphase.optimizedCollisionsByGroupBox;
final collisionsListByGroup = optimizedCollisionsByGroupBox[cell]!;

final componentsForOptimization =
parentLayer.children.query<HasGridSupport>();
var i = 0;
final toCheck = <BoundingHitboxHydrated>[];
for (final child in componentsForOptimization) {
if (cell.state != CellState.inactive) {
child.boundingBox.collisionType =
child.boundingBox.defaultCollisionType;
child.boundingBox.group = null;
}
toCheck.add(BoundingHitboxHydrated(child.boundingBox, i));
i++;
}

final skip = <int>{};
for (var i = 0; i < toCheck.length; i++) {
if (skip.contains(i)) {
continue;
}
final target = toCheck[i];
if (target.skip) {
continue;
}
final params = OverlappingSearchRequest(
hitboxes: toCheck,
target: target,
);
final hitboxesUnsortedIndices = await _isolateManager!.compute(params);
final hitboxesUnsorted = <BoundingHitbox>[];
for (final responseItem in hitboxesUnsortedIndices) {
try {
final hitbox =
componentsForOptimization[responseItem.index].boundingBox;
CollisionOptimizer.rectCache[hitbox] = responseItem.rect;
skip.add(responseItem.index);
hitboxesUnsorted.add(hitbox);
} on RangeError catch (_) {
skip.add(responseItem.index);
}
}
if (hitboxesUnsorted.length > 1) {
if (hitboxesUnsorted.length > maximumItemsInGroup) {
final hitboxesSorted = hitboxesUnsorted.toList(growable: false);
hitboxesSorted.sort((a, b) {
if (a.aabbCenter == b.aabbCenter) {
return 0;
}
if (a.aabbCenter.y < b.aabbCenter.y) {
return -1;
} else if (a.aabbCenter.y == b.aabbCenter.y) {
return a.aabbCenter.x < b.aabbCenter.x ? -1 : 1;
} else {
return 1;
}
});
var totalInChunk = 0;
var chunk = <ShapeHitbox>[];
for (final hbInChunk in hitboxesSorted) {
if (totalInChunk == maximumItemsInGroup) {
final optimized = OptimizedCollisionList(
<ShapeHitbox>{}..addAll(chunk),
parentLayer,
);
collisionsListByGroup[optimized.boundingBox] = optimized;
_createdCollisionLists.add(optimized);
totalInChunk = 0;
chunk = <ShapeHitbox>[];
} else {
chunk.add(hbInChunk);
totalInChunk++;
}
}
if (chunk.isNotEmpty) {
final optimized = OptimizedCollisionList(
HashSet<ShapeHitbox>()..addAll(chunk),
parentLayer,
);
collisionsListByGroup[optimized.boundingBox] = optimized;
_createdCollisionLists.add(optimized);
}
} else {
final optimized = OptimizedCollisionList(
hitboxesUnsorted,
parentLayer,
);
collisionsListByGroup[optimized.boundingBox] = optimized;
_createdCollisionLists.add(optimized);
}
for (final hb in hitboxesUnsorted) {
hb.collisionType = CollisionType.inactive;
}
if (hitboxesUnsorted.length >= toCheck.length) {
break;
}
}
}
}

Future optimize() async {
await optimizeIsolated();
return;

final cell = clear();
if (cell == null) {
return;
@@ -126,7 +366,7 @@ class CollisionOptimizer {
Iterable<BoundingHitbox> _findOverlappingRects(
BoundingHitbox hitbox, [
int index = 0,
Uint16List? removalsIndicesOutput,
List<int>? removalsIndicesOutput,
]) {
final hitboxes = <BoundingHitbox>[];
hitboxes.add(hitbox);
@@ -135,6 +375,8 @@ class CollisionOptimizer {
if (removalsIndicesOutput != null) {
initialRemovalsCount = removalsIndicesOutput.length;
removalsIndicesOutput.add(index);
} else {
removalsIndicesOutput = <int>[];
}
for (var i = 0; i < _componentsForOptimisation.length;) {
HasGridSupport otherChild;
@@ -147,16 +389,21 @@ class CollisionOptimizer {
.toRectSpecial()
.overlapsSpecial(otherChild.boundingBox.toRectSpecial())) {
hitboxes.add(otherChild.boundingBox);
hitboxes.addAll(_findOverlappingRects(
otherChild.boundingBox,
i,
removalsIndicesOutput,
));
if (removalsIndicesOutput != null &&
hitboxes.addAll(
_findOverlappingRects(
otherChild.boundingBox,
i,
removalsIndicesOutput,
),
);
if (removalsIndicesOutput.isNotEmpty &&
initialRemovalsCount < removalsIndicesOutput.length) {
for (final removedIndex in removalsIndicesOutput) {
if (removedIndex <= i) {
i--;
if (i < 0) {
i = 0;
}
}
}
} else {
2 changes: 1 addition & 1 deletion lib/src/components/layers/cell_layer.dart
Original file line number Diff line number Diff line change
@@ -330,7 +330,7 @@ abstract class CellLayer extends PositionComponent
game.processLifecycleEvents();
if (currentCell?.state == CellState.active) {
if (optimizeCollisions) {
collisionOptimizer.optimize();
await collisionOptimizer.optimize();
}
if (renderMode != LayerRenderMode.component) {
compileToSingleLayer(components);
11 changes: 9 additions & 2 deletions lib/src/components/layers/scheduled_layer_operation.dart
Original file line number Diff line number Diff line change
@@ -19,16 +19,23 @@ class ScheduledLayerOperation {
if (cellLayer.isRemovedLayer) {
return;
}
Future? future;
if (optimizeCollisions) {
cellLayer.collisionOptimizer.optimize();
future = cellLayer.collisionOptimizer.optimize();
}
if (compileToSingleLayer) {
cellLayer.compileToSingleLayer(cellLayer.components);
cellLayer.postCompileActions();
}
if (stateAfterOperation != null &&
cellLayer.currentCell?.state != stateAfterOperation) {
cellLayer.currentCell?.setStateInternal(stateAfterOperation!);
if (future == null) {
cellLayer.currentCell?.setStateInternal(stateAfterOperation!);
} else {
future.then((value) {
cellLayer.currentCell?.setStateInternal(stateAfterOperation!);
});
}
}
}
}
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -7,9 +7,10 @@ environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
flame: ^1.9.1
flame: ^1.10.0
flame_tiled: ^1.12.0
meta: ^1.8.0
isolate_manager: ^4.1.3
collection: any
tiled: any
xml: any

0 comments on commit 503cc7d

Please sign in to comment.