From c884a116846bc584445f7d0b5dbd0dc32965b7af Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 13 Mar 2023 17:30:44 +0100 Subject: [PATCH 1/6] feat: HasCollisionDetection on Component --- .../collisions/has_collision_detection.dart | 5 +- .../src/collisions/hitboxes/shape_hitbox.dart | 6 +- .../lib/src/components/core/component.dart | 7 +- .../collision_callback_benchmark_test.dart | 4 +- .../collisions/collision_callback_test.dart | 45 ++++++- .../collisions/collision_detection_test.dart | 118 +++++++++++------- .../collisions/collision_test_helpers.dart | 3 + pubspec.lock | 8 +- 8 files changed, 137 insertions(+), 59 deletions(-) diff --git a/packages/flame/lib/src/collisions/has_collision_detection.dart b/packages/flame/lib/src/collisions/has_collision_detection.dart index 5482c85727f..925e86c6479 100644 --- a/packages/flame/lib/src/collisions/has_collision_detection.dart +++ b/packages/flame/lib/src/collisions/has_collision_detection.dart @@ -1,9 +1,10 @@ import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; import 'package:flame/game.dart'; /// Keeps track of all the [ShapeHitbox]s in the component tree and initiates /// collision detection every tick. -mixin HasCollisionDetection> on FlameGame { +mixin HasCollisionDetection> on Component { CollisionDetection _collisionDetection = StandardCollisionDetection(); CollisionDetection get collisionDetection => @@ -27,7 +28,7 @@ mixin HasCollisionDetection> on FlameGame { /// Do note that [collisionDetection] has to be initialized before the game /// starts the update loop for the collision detection to work. mixin HasGenericCollisionDetection, B extends Broadphase> - on FlameGame { + on Component { CollisionDetection? _collisionDetection; CollisionDetection get collisionDetection => _collisionDetection!; diff --git a/packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart b/packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart index 0d0019f5915..d0fcdcee737 100644 --- a/packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart +++ b/packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart @@ -96,9 +96,9 @@ mixin ShapeHitbox on ShapeComponent implements Hitbox { // This should be placed after the hitbox parent listener // since the correct hitbox size is required by the QuadTree. - final parentGame = findParent(); - if (parentGame is HasCollisionDetection) { - _collisionDetection = parentGame.collisionDetection; + final parent = findParent(); + if (parent is HasCollisionDetection) { + _collisionDetection = parent.collisionDetection; _collisionDetection?.add(this); } } diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index 618b60c022f..c7f574bcf84 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -300,8 +300,11 @@ class Component { /// Returns the closest parent further up the hierarchy that satisfies type=T, /// or null if no such parent can be found. - T? findParent() { - return ancestors().whereType().firstOrNull; + /// + /// If [includeSelf] is set to true (default is false) then the component + /// which the call is made for is also included in the search. + T? findParent({bool includeSelf = false}) { + return ancestors(includeSelf: includeSelf).whereType().firstOrNull; } /// Returns the first child that matches the given type [T], or null if there diff --git a/packages/flame/test/collisions/collision_callback_benchmark_test.dart b/packages/flame/test/collisions/collision_callback_benchmark_test.dart index 1e7a2175f12..f5765be80d4 100644 --- a/packages/flame/test/collisions/collision_callback_benchmark_test.dart +++ b/packages/flame/test/collisions/collision_callback_benchmark_test.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:test/test.dart'; @@ -41,7 +42,8 @@ class _TestBlock extends PositionComponent with CollisionCallbacks { void main() { group('Benchmark collision detection', () { runCollisionTestRegistry({ - 'collidable callbacks are called': (game) async { + 'collidable callbacks are called': (collisionSystem) async { + final game = collisionSystem as FlameGame; final rng = Random(0); final blocks = List.generate( 100, diff --git a/packages/flame/test/collisions/collision_callback_test.dart b/packages/flame/test/collisions/collision_callback_test.dart index df68f064446..0f983312c16 100644 --- a/packages/flame/test/collisions/collision_callback_test.dart +++ b/packages/flame/test/collisions/collision_callback_test.dart @@ -1,5 +1,7 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:test/test.dart'; @@ -463,7 +465,8 @@ void main() { expect(player.endCounter, 0); }, // Reproduced #1478 - 'collision callbacks with changed game size': (game) async { + 'collision callbacks with changed game size': (collisionSystem) async { + final game = collisionSystem as FlameGame; final block = TestBlock(Vector2.all(20), Vector2.all(10)) ..anchor = Anchor.center; await game.ensureAddAll([ScreenHitbox(), block]); @@ -512,6 +515,46 @@ void main() { expect(outerBlock.onCollisionCounter, 1); expect(outerBlock.endCounter, 1); }, + 'collision callbacks for nested World': (outerCollisionSystem) async { + final game = outerCollisionSystem as FlameGame; + final world1 = CollisionDetectionWorld(); + final world2 = CollisionDetectionWorld(); + final camera1 = CameraComponent(world: world1); + final camera2 = CameraComponent(world: world2); + await game.ensureAddAll([world1, world2, camera1, camera2]); + final block1 = TestBlock(Vector2(1, 1), Vector2.all(2)) + ..anchor = Anchor.center; + final block2 = TestBlock(Vector2(1, -1), Vector2.all(2)) + ..anchor = Anchor.center; + final block3 = TestBlock(Vector2(-1, 1), Vector2.all(2)) + ..anchor = Anchor.center; + final block4 = TestBlock(Vector2(-1, -1), Vector2.all(2)) + ..anchor = Anchor.center; + await world1.ensureAddAll([block1, block2]); + await world2.ensureAddAll([block3, block4]); + + game.update(0); + for (final block in [block1, block2, block3, block4]) { + expect(block.startCounter, 1); + expect(block.onCollisionCounter, 1); + expect(block.endCounter, 0); + } + expect(block1.collidedWithExactly([block2]), isTrue); + expect(block2.collidedWithExactly([block1]), isTrue); + expect(block3.collidedWithExactly([block4]), isTrue); + expect(block4.collidedWithExactly([block3]), isTrue); + + for (final block in [block1, block2, block3, block4]) { + block.position.scale(3); + } + + game.update(0); + for (final block in [block1, block2, block3, block4]) { + expect(block.startCounter, 1); + expect(block.onCollisionCounter, 1); + expect(block.endCounter, 1); + } + }, }); group('ComponentTypeCheck(only supported in the QuadTree)', () { diff --git a/packages/flame/test/collisions/collision_detection_test.dart b/packages/flame/test/collisions/collision_detection_test.dart index d7cab1cba8f..f0fafe62070 100644 --- a/packages/flame/test/collisions/collision_detection_test.dart +++ b/packages/flame/test/collisions/collision_detection_test.dart @@ -1,5 +1,6 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame/geometry.dart'; import 'package:flame/geometry.dart' as geometry; import 'package:flame_test/flame_test.dart'; @@ -969,7 +970,8 @@ void main() { group('Raycasting', () { runCollisionTestRegistry({ - 'one hitbox': (game) async { + 'one hitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAdd( PositionComponent( children: [RectangleHitbox()], @@ -984,12 +986,13 @@ void main() { origin: Vector2.zero(), direction: Vector2(1, 0), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(50, 0))); expect(result?.reflectionRay?.direction, closeToVector(Vector2(-1, 0))); }, - 'multiple hitboxes after each other': (game) async { + 'multiple hitboxes after each other': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ for (var i = 0.0; i < 10; i++) PositionComponent( @@ -1003,7 +1006,7 @@ void main() { origin: Vector2.zero(), direction: Vector2.all(1)..normalize(), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2.all(90))); expect( @@ -1011,7 +1014,9 @@ void main() { closeToVector(Vector2(-1, 1)..normalize()), ); }, - 'multiple hitboxes after each other with one ignored': (game) async { + 'multiple hitboxes after each other with one ignored': + (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ for (var i = 0.0; i < 10; i++) PositionComponent( @@ -1025,7 +1030,7 @@ void main() { origin: Vector2.zero(), direction: Vector2.all(1)..normalize(), ); - final result = game.collisionDetection.raycast( + final result = collisionSystem.collisionDetection.raycast( ray, ignoreHitboxes: [ game.children.first.children.first as ShapeHitbox, @@ -1041,7 +1046,8 @@ void main() { closeToVector(Vector2(-1, 1)..normalize()), ); }, - 'ray with origin on hitbox corner': (game) async { + 'ray with origin on hitbox corner': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.all(10), @@ -1053,7 +1059,7 @@ void main() { origin: Vector2.all(10), direction: Vector2.all(1)..normalize(), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(20, 20))); expect( @@ -1061,7 +1067,8 @@ void main() { closeToVector(Vector2(1, -1)..normalize()), ); }, - 'raycast with maxDistance': (game) async { + 'raycast with maxDistance': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.all(20), @@ -1078,7 +1085,7 @@ void main() { final result = RaycastResult(); // No hit cast - game.collisionDetection.raycast( + collisionSystem.collisionDetection.raycast( ray, maxDistance: Vector2.all(9).length, out: result, @@ -1086,7 +1093,7 @@ void main() { expect(result.hitbox?.parent, isNull); // Extended cast - game.collisionDetection.raycast( + collisionSystem.collisionDetection.raycast( ray, maxDistance: Vector2.all(10).length, out: result, @@ -1097,7 +1104,8 @@ void main() { group('Rectangle hitboxes', () { runCollisionTestRegistry({ - 'ray from within RectangleHitbox': (game) async { + 'ray from within RectangleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.all(0), @@ -1109,7 +1117,7 @@ void main() { origin: Vector2.all(5), direction: Vector2.all(1)..normalize(), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.normal, closeToVector(Vector2(0, -1))); expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 10))); @@ -1118,7 +1126,8 @@ void main() { closeToVector(Vector2(1, -1)..normalize()), ); }, - 'ray from the left of RectangleHitbox': (game) async { + 'ray from the left of RectangleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1127,7 +1136,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5))); expect( @@ -1135,7 +1144,8 @@ void main() { closeToVector(Vector2(-1, 0)), ); }, - 'ray from the top of RectangleHitbox': (game) async { + 'ray from the top of RectangleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1144,7 +1154,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0))); expect( @@ -1152,7 +1162,8 @@ void main() { closeToVector(Vector2(0, -1)), ); }, - 'ray from the right of RectangleHitbox': (game) async { + 'ray from the right of RectangleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1161,7 +1172,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5))); expect( @@ -1169,7 +1180,8 @@ void main() { closeToVector(Vector2(1, 0)), ); }, - 'ray from the bottom of RectangleHitbox': (game) async { + 'ray from the bottom of RectangleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1178,7 +1190,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10))); expect( @@ -1191,7 +1203,8 @@ void main() { group('Circle hitboxes', () { runCollisionTestRegistry({ - 'ray from top to bottom within CircleHitbox': (game) async { + 'ray from top to bottom within CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1200,7 +1213,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(5, 4), direction: Vector2(0, 1)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.normal, closeToVector(Vector2(0, -1))); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10))); @@ -1209,7 +1222,9 @@ void main() { closeToVector(Vector2(0, -1)), ); }, - 'ray from bottom-right to top-left within CircleHitbox': (game) async { + 'ray from bottom-right to top-left within CircleHitbox': + (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1221,7 +1236,7 @@ void main() { origin: Vector2.all(6), direction: Vector2.all(-1)..normalize(), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.normal, closeToVector(Vector2.all(0.707106781186547))); expect( @@ -1233,7 +1248,9 @@ void main() { closeToVector(Vector2.all(1)..normalize()), ); }, - 'ray from bottom within CircleHitbox going down': (game) async { + 'ray from bottom within CircleHitbox going down': + (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1243,7 +1260,7 @@ void main() { await game.ready(); final direction = Vector2(0, 1); final ray = Ray2(origin: Vector2(5, 6), direction: direction); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.normal, closeToVector(Vector2(0, -1))); expect( @@ -1255,7 +1272,8 @@ void main() { closeToVector(direction.inverted()), ); }, - 'ray from the left of CircleHitbox': (game) async { + 'ray from the left of CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1264,7 +1282,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5))); expect( @@ -1272,7 +1290,8 @@ void main() { closeToVector(Vector2(-1, 0)), ); }, - 'ray from the top of CircleHitbox': (game) async { + 'ray from the top of CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1281,7 +1300,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0))); expect( @@ -1289,7 +1308,8 @@ void main() { closeToVector(Vector2(0, -1)), ); }, - 'ray from the right of CircleHitbox': (game) async { + 'ray from the right of CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1298,7 +1318,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5))); expect( @@ -1306,7 +1326,8 @@ void main() { closeToVector(Vector2(1, 0)), ); }, - 'ray from the bottom of CircleHitbox': (game) async { + 'ray from the bottom of CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2.zero(), @@ -1315,7 +1336,7 @@ void main() { ]); await game.ready(); final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1)); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, game.children.first); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10))); expect( @@ -1323,7 +1344,8 @@ void main() { closeToVector(Vector2(0, 1)), ); }, - 'ray from the center of CircleHitbox': (game) async { + 'ray from the center of CircleHitbox': (collisionSystem) async { + final game = collisionSystem as FlameGame; final positionComponent = PositionComponent( position: Vector2.zero(), size: Vector2.all(10), @@ -1335,7 +1357,7 @@ void main() { origin: positionComponent.absoluteCenter, direction: Vector2(0, -1), ); - final result = game.collisionDetection.raycast(ray); + final result = collisionSystem.collisionDetection.raycast(ray); expect(result?.hitbox?.parent, positionComponent); expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0))); expect( @@ -1348,7 +1370,8 @@ void main() { group('raycastAll', () { runCollisionTestRegistry({ - 'All directions and all hits': (game) async { + 'All directions and all hits': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2(10, 0), @@ -1369,14 +1392,15 @@ void main() { ]); await game.ready(); final origin = Vector2.all(15); - final results = game.collisionDetection.raycastAll( + final results = collisionSystem.collisionDetection.raycastAll( origin, numberOfRays: 4, ); expect(results.every((r) => r.isActive), isTrue); expect(results.length, 4); }, - 'raycastAll with maxDistance': (game) async { + 'raycastAll with maxDistance': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2(10, 0), @@ -1399,7 +1423,7 @@ void main() { final origin = Vector2.all(15); // No hit - final results1 = game.collisionDetection.raycastAll( + final results1 = collisionSystem.collisionDetection.raycastAll( origin, maxDistance: 4, numberOfRays: 4, @@ -1407,7 +1431,7 @@ void main() { expect(results1.length, isZero); // Hit all four - final results2 = game.collisionDetection.raycastAll( + final results2 = collisionSystem.collisionDetection.raycastAll( origin, maxDistance: 5, numberOfRays: 4, @@ -1418,7 +1442,8 @@ void main() { }); runCollisionTestRegistry({ - 'All directions and all hits': (game) async { + 'All directions and all hits': (collisionSystem) async { + final game = collisionSystem as FlameGame; await game.ensureAddAll([ PositionComponent( position: Vector2(10, 0), @@ -1440,7 +1465,7 @@ void main() { await game.ready(); final origin = Vector2.all(15); final ignoreHitbox = game.children.first.children.first as ShapeHitbox; - final results = game.collisionDetection.raycastAll( + final results = collisionSystem.collisionDetection.raycastAll( origin, numberOfRays: 4, ignoreHitboxes: [ignoreHitbox], @@ -1454,7 +1479,8 @@ void main() { group('Raytracing', () { runCollisionTestRegistry({ - 'on single circle': (game) async { + 'on single circle': (collisionSystem) async { + final game = collisionSystem as FlameGame; final circle = CircleComponent( radius: 10.0, position: Vector2.all(20), @@ -1465,7 +1491,7 @@ void main() { origin: Vector2(0, 10), direction: Vector2.all(1.0)..normalize(), ); - final results = game.collisionDetection.raytrace(ray); + final results = collisionSystem.collisionDetection.raytrace(ray); expect(results.length, 1); expect(results.first.isActive, isTrue); expect(results.first.isInsideHitbox, isFalse); diff --git a/packages/flame/test/collisions/collision_test_helpers.dart b/packages/flame/test/collisions/collision_test_helpers.dart index cd22d2b12ec..9862a5f1338 100644 --- a/packages/flame/test/collisions/collision_test_helpers.dart +++ b/packages/flame/test/collisions/collision_test_helpers.dart @@ -1,5 +1,6 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/game.dart'; import 'package:flame/image_composition.dart'; import 'package:flame_test/flame_test.dart'; @@ -10,6 +11,8 @@ class HasCollidablesGame extends FlameGame with HasCollisionDetection {} class HasQuadTreeCollidablesGame extends FlameGame with HasQuadTreeCollisionDetection {} +class CollisionDetectionWorld extends World with HasCollisionDetection {} + @isTest Future testCollisionDetectionGame( String testName, diff --git a/pubspec.lock b/pubspec.lock index 8b19f6c4190..408cae47aff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: conventional_commit - sha256: "40321e55c2416c0940a846bf938c0a70efaf9eb72da042d5ea76b79b6f5045ee" + sha256: "8eee25c315cf1946215d02d598402ca75cfee8a8ab482f3fac34cb0717323afa" url: "https://pub.dev" source: hosted - version: "0.6.0-dev.0" + version: "0.6.0" file: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: "direct dev" description: name: melos - sha256: "739431cbc88d802dc215c0a7bac6e48a9a7fe2c42ef15cbe2f8c54433e6588d6" + sha256: cd8e7db0250ee822c5354a8214afc751b6c1c41aadfbbef927456d509d953244 url: "https://pub.dev" source: hosted - version: "3.0.0-dev.0" + version: "3.0.0" meta: dependency: transitive description: From 126c56b9541b2bceb376b2870adbf4147ecd43a2 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 13 Mar 2023 17:37:56 +0100 Subject: [PATCH 2/6] Fix analyze --- packages/flame/lib/src/collisions/has_collision_detection.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flame/lib/src/collisions/has_collision_detection.dart b/packages/flame/lib/src/collisions/has_collision_detection.dart index 925e86c6479..b2cf633e4c9 100644 --- a/packages/flame/lib/src/collisions/has_collision_detection.dart +++ b/packages/flame/lib/src/collisions/has_collision_detection.dart @@ -1,6 +1,5 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; -import 'package:flame/game.dart'; /// Keeps track of all the [ShapeHitbox]s in the component tree and initiates /// collision detection every tick. From e8fa842eb24583a93efe15ccbd24043f60a5faf0 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 13 Mar 2023 22:56:40 +0100 Subject: [PATCH 3/6] Add dartdocs and docs --- doc/flame/collision_detection.md | 17 +++++++++++++++++ .../src/collisions/has_collision_detection.dart | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index 9a67030f54a..ec37da174e4 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -42,6 +42,23 @@ class MyGame extends FlameGame with HasCollisionDetection { Now when you add `ShapeHitbox`s to components that are then added to the game, they will automatically be checked for collisions. +You can also add `HasCollisionDetection` directly to another `Component` instead of the `FlameGame`, +for example to the `World` that is used for the `CameraComponent`. +If that is done, hitboxes that are added in that component's tree will only be compared to other +hitboxes in that subtree, which makes it possible to have several worlds with collision detection +within one `FlameGame`. + +Example: + +```dart +class CollidableWorld extends World with HasCollisionDetection {} +``` + +```{note} +Hitboxes will only be connected to one collision detection system and that is the closest parent +that has the `HasCollisionDetection` mixin. +``` + ### CollisionCallbacks diff --git a/packages/flame/lib/src/collisions/has_collision_detection.dart b/packages/flame/lib/src/collisions/has_collision_detection.dart index b2cf633e4c9..c1ee5969707 100644 --- a/packages/flame/lib/src/collisions/has_collision_detection.dart +++ b/packages/flame/lib/src/collisions/has_collision_detection.dart @@ -1,8 +1,12 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; -/// Keeps track of all the [ShapeHitbox]s in the component tree and initiates +/// Keeps track of all the [ShapeHitbox]s in the component's tree and initiates /// collision detection every tick. +/// +/// Hitboxes are only part of the collision detection performed by its closest +/// parent with the [HasCollisionDetection] mixin, if there are multiple nested +/// classes that has [HasCollisionDetection]. mixin HasCollisionDetection> on Component { CollisionDetection _collisionDetection = StandardCollisionDetection(); From 347d52e8a613b6435a8ac18f08efd2d62a8e4aa1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 14 Mar 2023 00:00:38 +0100 Subject: [PATCH 4/6] Add docs and example --- doc/flame/collision_detection.md | 2 +- .../collision_detection.dart | 7 ++ .../multiple_worlds_example.dart | 84 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 examples/lib/stories/collision_detection/multiple_worlds_example.dart diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index ec37da174e4..0200e1c42dc 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -51,7 +51,7 @@ within one `FlameGame`. Example: ```dart -class CollidableWorld extends World with HasCollisionDetection {} +class CollisionDetectionWorld extends World with HasCollisionDetection {} ``` ```{note} diff --git a/examples/lib/stories/collision_detection/collision_detection.dart b/examples/lib/stories/collision_detection/collision_detection.dart index b3ead488221..f8ba8cea64e 100644 --- a/examples/lib/stories/collision_detection/collision_detection.dart +++ b/examples/lib/stories/collision_detection/collision_detection.dart @@ -4,6 +4,7 @@ import 'package:examples/stories/collision_detection/bouncing_ball_example.dart' import 'package:examples/stories/collision_detection/circles_example.dart'; import 'package:examples/stories/collision_detection/collidable_animation_example.dart'; import 'package:examples/stories/collision_detection/multiple_shapes_example.dart'; +import 'package:examples/stories/collision_detection/multiple_worlds_example.dart'; import 'package:examples/stories/collision_detection/quadtree_example.dart'; import 'package:examples/stories/collision_detection/raycast_example.dart'; import 'package:examples/stories/collision_detection/raycast_light_example.dart'; @@ -38,6 +39,12 @@ void addCollisionDetectionStories(Dashbook dashbook) { codeLink: baseLink('collision_detection/multiple_shapes_example.dart'), info: MultipleShapesExample.description, ) + ..add( + 'Multiple worlds', + (_) => GameWidget(game: MultipleWorldsExample()), + codeLink: baseLink('collision_detection/multiple_worlds_example.dart'), + info: MultipleWorldsExample.description, + ) ..add( 'QuadTree collision', (_) => GameWidget(game: QuadTreeExample()), diff --git a/examples/lib/stories/collision_detection/multiple_worlds_example.dart b/examples/lib/stories/collision_detection/multiple_worlds_example.dart new file mode 100644 index 00000000000..ff9a52d1479 --- /dev/null +++ b/examples/lib/stories/collision_detection/multiple_worlds_example.dart @@ -0,0 +1,84 @@ +import 'dart:math'; + +import 'package:examples/commons/ember.dart'; +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/extensions.dart' show OffsetExtension; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; + +class MultipleWorldsExample extends FlameGame { + static const description = ''' + This example shows how multiple worlds can have discrete collision + detection. + + The top two Embers live in one world and turn green when they collide and + the bottom two embers live in another world and turn red when they collide, + you can see that when one of the top ones collide with one of the bottom + ones, neither change their colors since they are in different worlds. + '''; + + @override + Future onLoad() async { + final world1 = CollisionDetectionWorld(); + final world2 = CollisionDetectionWorld(); + final camera1 = CameraComponent(world: world1); + final camera2 = CameraComponent(world: world2); + await addAll([world1, world2, camera1, camera2]); + final ember1 = CollidableEmber(position: Vector2(75, 75)); + final ember2 = CollidableEmber(position: Vector2(-75, 75)); + final ember3 = CollidableEmber(position: Vector2(75, -75)); + final ember4 = CollidableEmber(position: Vector2(-75, -75)); + world1.addAll([ember1, ember2]); + world2.addAll([ember3, ember4]); + } +} + +class CollisionDetectionWorld extends World with HasCollisionDetection {} + +class CollidableEmber extends Ember with CollisionCallbacks { + CollidableEmber({super.position}); + + static final Random _rng = Random(); + int get index => + (position.x.isNegative ? 1 : 0) + (position.y.isNegative ? 2 : 0); + + @override + Future onLoad() async { + super.onLoad(); + add(CircleHitbox()); + add( + MoveToEffect( + Vector2.zero(), + EffectController( + duration: 0.5 + _rng.nextDouble(), + infinite: true, + alternate: true, + ), + ), + ); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + + add( + ColorEffect( + index < 2 ? Colors.red : Colors.green, + const Offset(0, 0.9), + EffectController( + duration: 0.2, + alternate: true, + ), + ), + ); + } +} From e3c0224c56ad7e6b1373f0dc94213e24e20daa7b Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 14 Mar 2023 00:24:14 +0100 Subject: [PATCH 5/6] Fix analyze errors --- doc/flame/collision_detection.md | 4 ++-- .../stories/collision_detection/multiple_worlds_example.dart | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index 0200e1c42dc..3699e48af1b 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -55,8 +55,8 @@ class CollisionDetectionWorld extends World with HasCollisionDetection {} ``` ```{note} -Hitboxes will only be connected to one collision detection system and that is the closest parent -that has the `HasCollisionDetection` mixin. +Hitboxes will only be connected to one collision detection system and that is +the closest parent that has the `HasCollisionDetection` mixin. ``` diff --git a/examples/lib/stories/collision_detection/multiple_worlds_example.dart b/examples/lib/stories/collision_detection/multiple_worlds_example.dart index ff9a52d1479..34e77820e9e 100644 --- a/examples/lib/stories/collision_detection/multiple_worlds_example.dart +++ b/examples/lib/stories/collision_detection/multiple_worlds_example.dart @@ -5,11 +5,8 @@ import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/experimental.dart'; -import 'package:flame/extensions.dart' show OffsetExtension; import 'package:flame/game.dart'; -import 'package:flame/input.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/painting.dart'; class MultipleWorldsExample extends FlameGame { static const description = ''' From d7d8f9e99894348bfe6fc6a576d3015f0a3cba7c Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 14 Mar 2023 00:24:39 +0100 Subject: [PATCH 6/6] Add pubspec.lock --- examples/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/.gitignore b/examples/.gitignore index 5d2a02c35fa..94db148efba 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -44,3 +44,4 @@ app.*.symbols # Obfuscation related app.*.map.json .vscode/ +pubspec.lock