diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index 652fee9f4..b5d5e5da7 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -346,6 +346,10 @@ extra information like the distance, the normal and the reflection ray. The seco works similarly but sends out multiple rays uniformly around the origin, or within an angle centered at the origin. +By default, `raycast` and `raycastAll` scan for the nearest hit irrespective of how far it lies from +the ray origin. But in some use cases, it might be interesting to find hits only within a certain +range. For such cases, an optional `maxDistance` can be provided. + To use the ray casting functionality you have to have the `HasCollisionDetection` mixin on your game. After you have added that you can call `collisionDetection.raycast(...)` on your game class. diff --git a/examples/lib/stories/collision_detection/collision_detection.dart b/examples/lib/stories/collision_detection/collision_detection.dart index 945c8d91d..b3ead4882 100644 --- a/examples/lib/stories/collision_detection/collision_detection.dart +++ b/examples/lib/stories/collision_detection/collision_detection.dart @@ -7,6 +7,7 @@ import 'package:examples/stories/collision_detection/multiple_shapes_example.dar 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'; +import 'package:examples/stories/collision_detection/raycast_max_distance_example.dart'; import 'package:examples/stories/collision_detection/raytrace_example.dart'; import 'package:flame/game.dart'; @@ -60,5 +61,12 @@ void addCollisionDetectionStories(Dashbook dashbook) { (_) => GameWidget(game: RaytraceExample()), codeLink: baseLink('collision_detection/raytrace_example.dart'), info: RaytraceExample.description, + ) + ..add( + 'Raycasting Max Distance', + (_) => GameWidget(game: RaycastMaxDistanceExample()), + codeLink: + baseLink('collision_detection/raycast_max_distance_example.dart'), + info: RaycastMaxDistanceExample.description, ); } diff --git a/examples/lib/stories/collision_detection/raycast_max_distance_example.dart b/examples/lib/stories/collision_detection/raycast_max_distance_example.dart new file mode 100644 index 000000000..e5ab5e19f --- /dev/null +++ b/examples/lib/stories/collision_detection/raycast_max_distance_example.dart @@ -0,0 +1,132 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +class RaycastMaxDistanceExample extends FlameGame with HasCollisionDetection { + static const description = ''' +This examples showcases how raycast APIs can be used to detect hits within certain range. +'''; + + static const _maxDistance = 50.0; + + late Ray2 _ray; + late _Character _character; + final _result = RaycastResult(); + + final _text = TextComponent( + text: "Hey! Who's there?", + anchor: Anchor.center, + textRenderer: TextPaint( + style: const TextStyle( + fontSize: 8, + color: Colors.amber, + ), + ), + )..positionType = PositionType.viewport; + + @override + Future? onLoad() { + camera.viewport = FixedResolutionViewport(Vector2(320, 180)); + + _addMovingWall(); + + add( + _character = _Character( + maxDistance: _maxDistance, + position: size / 2 - Vector2(50, 0), + anchor: Anchor.center, + ), + ); + + _text.position = _character.position - Vector2(0, 50); + + _ray = Ray2( + origin: _character.absolutePosition, + direction: Vector2(1, 0), + ); + + return super.onLoad(); + } + + void _addMovingWall() { + add( + RectangleComponent( + position: size / 2, + size: Vector2(20, 40), + anchor: Anchor.center, + paint: BasicPalette.red.paint(), + children: [ + RectangleHitbox(), + MoveByEffect( + Vector2(50, 0), + EffectController( + duration: 2, + alternate: true, + infinite: true, + ), + ), + ], + ), + ); + } + + @override + void update(double dt) { + collisionDetection.raycast(_ray, maxDistance: _maxDistance, out: _result); + if (_result.isActive) { + if (!camera.shaking) { + camera.shake(duration: 0.2, intensity: 1); + } + if (!_text.isMounted) { + add(_text); + } + } else { + _text.removeFromParent(); + } + super.update(dt); + } +} + +class _Character extends PositionComponent { + _Character({required this.maxDistance, super.position, super.anchor}); + + final double maxDistance; + + final _rayOriginPoint = Offset.zero; + late final _rayEndPoint = Offset(maxDistance, 0); + final _rayPaint = BasicPalette.gray.paint(); + + @override + Future? onLoad() async { + add( + CircleComponent( + radius: 20, + anchor: Anchor.center, + paint: BasicPalette.green.paint(), + )..scale = Vector2(0.55, 1), + ); + add( + CircleComponent( + radius: 10, + anchor: Anchor.center, + paint: _rayPaint, + ), + ); + add( + RectangleComponent( + size: Vector2(10, 3), + position: Vector2(12, 5), + ), + ); + } + + @override + void render(Canvas canvas) { + canvas.drawLine(_rayOriginPoint, _rayEndPoint, _rayPaint); + } +} diff --git a/packages/flame/lib/src/collisions/collision_detection.dart b/packages/flame/lib/src/collisions/collision_detection.dart index 7efc95cdc..a69c96869 100644 --- a/packages/flame/lib/src/collisions/collision_detection.dart +++ b/packages/flame/lib/src/collisions/collision_detection.dart @@ -71,6 +71,9 @@ abstract class CollisionDetection, /// Returns the first hitbox that the given [ray] hits and the associated /// intersection information; or null if the ray doesn't hit any hitbox. /// + /// [maxDistance] can be provided to limit the raycast to only return hits + /// within this distance from the ray origin. + /// /// [ignoreHitboxes] can be used if you want to ignore certain hitboxes, i.e. /// the rays will go straight through them. For example the hitbox of the /// component that you might be casting the rays from. @@ -79,6 +82,7 @@ abstract class CollisionDetection, /// result. RaycastResult? raycast( Ray2 ray, { + double? maxDistance, List? ignoreHitboxes, RaycastResult? out, }); @@ -88,6 +92,9 @@ abstract class CollisionDetection, /// the rays hit. /// [numberOfRays] is the number of rays that should be casted. /// + /// [maxDistance] can be provided to limit the raycasts to only return hits + /// within this distance from the ray origin. + /// /// If the [rays] argument is provided its [Ray2]s are populated with the rays /// needed to perform the operation. /// If there are less objects in [rays] than the operation requires, the @@ -105,6 +112,7 @@ abstract class CollisionDetection, required int numberOfRays, double startAngle = 0, double sweepAngle = tau, + double? maxDistance, List? rays, List? ignoreHitboxes, List>? out, diff --git a/packages/flame/lib/src/collisions/standard_collision_detection.dart b/packages/flame/lib/src/collisions/standard_collision_detection.dart index 7b1e818f8..635b05690 100644 --- a/packages/flame/lib/src/collisions/standard_collision_detection.dart +++ b/packages/flame/lib/src/collisions/standard_collision_detection.dart @@ -67,6 +67,7 @@ class StandardCollisionDetection> @override RaycastResult? raycast( Ray2 ray, { + double? maxDistance, List? ignoreHitboxes, RaycastResult? out, }) { @@ -80,7 +81,8 @@ class StandardCollisionDetection> final possiblyFirstResult = !(finalResult?.isActive ?? false); if (currentResult != null && (possiblyFirstResult || - currentResult.distance! < finalResult!.distance!)) { + currentResult.distance! < finalResult!.distance!) && + (currentResult.distance! <= (maxDistance ?? double.infinity))) { if (finalResult == null) { finalResult = currentResult.clone(); } else { @@ -97,6 +99,7 @@ class StandardCollisionDetection> required int numberOfRays, double startAngle = 0, double sweepAngle = tau, + double? maxDistance, List? rays, List? ignoreHitboxes, List>? out, @@ -126,7 +129,12 @@ class StandardCollisionDetection> result = RaycastResult(); out?.add(result); } - result = raycast(ray, ignoreHitboxes: ignoreHitboxes, out: result); + result = raycast( + ray, + maxDistance: maxDistance, + ignoreHitboxes: ignoreHitboxes, + out: result, + ); if (result != null) { results.add(result); diff --git a/packages/flame/test/collisions/collision_detection_test.dart b/packages/flame/test/collisions/collision_detection_test.dart index 878c7be1c..23ad66ba8 100644 --- a/packages/flame/test/collisions/collision_detection_test.dart +++ b/packages/flame/test/collisions/collision_detection_test.dart @@ -926,6 +926,38 @@ void main() { closeToVector(Vector2(1, -1)..normalize()), ); }, + 'raycast with maxDistance': (game) async { + game.ensureAddAll([ + PositionComponent( + position: Vector2.all(20), + size: Vector2.all(40), + children: [RectangleHitbox()], + ), + ]); + await game.ready(); + final ray = Ray2( + origin: Vector2.all(10), + direction: Vector2.all(1)..normalize(), + ); + + final result = RaycastResult(); + + // No hit cast + game.collisionDetection.raycast( + ray, + maxDistance: Vector2.all(9).length, + out: result, + ); + expect(result.hitbox?.parent, isNull); + + // Extended cast + game.collisionDetection.raycast( + ray, + maxDistance: Vector2.all(10).length, + out: result, + ); + expect(result.hitbox?.parent, game.children.first); + }, }); group('Rectangle hitboxes', () { @@ -1209,6 +1241,44 @@ void main() { expect(results.every((r) => r.isActive), isTrue); expect(results.length, 4); }, + 'raycastAll with maxDistance': (game) async { + game.ensureAddAll([ + PositionComponent( + position: Vector2(10, 0), + size: Vector2.all(10), + )..add(RectangleHitbox()), + PositionComponent( + position: Vector2(20, 10), + size: Vector2.all(10), + )..add(RectangleHitbox()), + PositionComponent( + position: Vector2(10, 20), + size: Vector2.all(10), + )..add(RectangleHitbox()), + PositionComponent( + position: Vector2(0, 10), + size: Vector2.all(10), + )..add(RectangleHitbox()), + ]); + await game.ready(); + final origin = Vector2.all(15); + + // No hit + final results1 = game.collisionDetection.raycastAll( + origin, + maxDistance: 4, + numberOfRays: 4, + ); + expect(results1.length, isZero); + + // Hit all four + final results2 = game.collisionDetection.raycastAll( + origin, + maxDistance: 5, + numberOfRays: 4, + ); + expect(results2.length, 4); + }, }); });