mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 08:56:01 +08:00 
			
		
		
		
	feat: Add optional maxDistance to raycast (#2012)
This PR adds an optional parameter to raycast API called maxDistance. Using this parameter users can control the limit within which raycast scans for hits.
This commit is contained in:
		| @ -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 | works similarly but sends out multiple rays uniformly around the origin, or within an angle | ||||||
| centered at the origin. | 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 | 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. | game. After you have added that you can call `collisionDetection.raycast(...)` on your game class. | ||||||
|  |  | ||||||
|  | |||||||
| @ -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/quadtree_example.dart'; | ||||||
| import 'package:examples/stories/collision_detection/raycast_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_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:examples/stories/collision_detection/raytrace_example.dart'; | ||||||
| import 'package:flame/game.dart'; | import 'package:flame/game.dart'; | ||||||
|  |  | ||||||
| @ -60,5 +61,12 @@ void addCollisionDetectionStories(Dashbook dashbook) { | |||||||
|       (_) => GameWidget(game: RaytraceExample()), |       (_) => GameWidget(game: RaytraceExample()), | ||||||
|       codeLink: baseLink('collision_detection/raytrace_example.dart'), |       codeLink: baseLink('collision_detection/raytrace_example.dart'), | ||||||
|       info: RaytraceExample.description, |       info: RaytraceExample.description, | ||||||
|  |     ) | ||||||
|  |     ..add( | ||||||
|  |       'Raycasting Max Distance', | ||||||
|  |       (_) => GameWidget(game: RaycastMaxDistanceExample()), | ||||||
|  |       codeLink: | ||||||
|  |           baseLink('collision_detection/raycast_max_distance_example.dart'), | ||||||
|  |       info: RaycastMaxDistanceExample.description, | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -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<ShapeHitbox>(); | ||||||
|  |  | ||||||
|  |   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<void>? 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<void>? 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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -71,6 +71,9 @@ abstract class CollisionDetection<T extends Hitbox<T>, | |||||||
|   /// Returns the first hitbox that the given [ray] hits and the associated |   /// Returns the first hitbox that the given [ray] hits and the associated | ||||||
|   /// intersection information; or null if the ray doesn't hit any hitbox. |   /// 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. |   /// [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 |   /// the rays will go straight through them. For example the hitbox of the | ||||||
|   /// component that you might be casting the rays from. |   /// component that you might be casting the rays from. | ||||||
| @ -79,6 +82,7 @@ abstract class CollisionDetection<T extends Hitbox<T>, | |||||||
|   /// result. |   /// result. | ||||||
|   RaycastResult<T>? raycast( |   RaycastResult<T>? raycast( | ||||||
|     Ray2 ray, { |     Ray2 ray, { | ||||||
|  |     double? maxDistance, | ||||||
|     List<T>? ignoreHitboxes, |     List<T>? ignoreHitboxes, | ||||||
|     RaycastResult<T>? out, |     RaycastResult<T>? out, | ||||||
|   }); |   }); | ||||||
| @ -88,6 +92,9 @@ abstract class CollisionDetection<T extends Hitbox<T>, | |||||||
|   /// the rays hit. |   /// the rays hit. | ||||||
|   /// [numberOfRays] is the number of rays that should be casted. |   /// [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 |   /// If the [rays] argument is provided its [Ray2]s are populated with the rays | ||||||
|   /// needed to perform the operation. |   /// needed to perform the operation. | ||||||
|   /// If there are less objects in [rays] than the operation requires, the |   /// If there are less objects in [rays] than the operation requires, the | ||||||
| @ -105,6 +112,7 @@ abstract class CollisionDetection<T extends Hitbox<T>, | |||||||
|     required int numberOfRays, |     required int numberOfRays, | ||||||
|     double startAngle = 0, |     double startAngle = 0, | ||||||
|     double sweepAngle = tau, |     double sweepAngle = tau, | ||||||
|  |     double? maxDistance, | ||||||
|     List<Ray2>? rays, |     List<Ray2>? rays, | ||||||
|     List<T>? ignoreHitboxes, |     List<T>? ignoreHitboxes, | ||||||
|     List<RaycastResult<T>>? out, |     List<RaycastResult<T>>? out, | ||||||
|  | |||||||
| @ -67,6 +67,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | |||||||
|   @override |   @override | ||||||
|   RaycastResult<ShapeHitbox>? raycast( |   RaycastResult<ShapeHitbox>? raycast( | ||||||
|     Ray2 ray, { |     Ray2 ray, { | ||||||
|  |     double? maxDistance, | ||||||
|     List<ShapeHitbox>? ignoreHitboxes, |     List<ShapeHitbox>? ignoreHitboxes, | ||||||
|     RaycastResult<ShapeHitbox>? out, |     RaycastResult<ShapeHitbox>? out, | ||||||
|   }) { |   }) { | ||||||
| @ -80,7 +81,8 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | |||||||
|       final possiblyFirstResult = !(finalResult?.isActive ?? false); |       final possiblyFirstResult = !(finalResult?.isActive ?? false); | ||||||
|       if (currentResult != null && |       if (currentResult != null && | ||||||
|           (possiblyFirstResult || |           (possiblyFirstResult || | ||||||
|               currentResult.distance! < finalResult!.distance!)) { |               currentResult.distance! < finalResult!.distance!) && | ||||||
|  |           (currentResult.distance! <= (maxDistance ?? double.infinity))) { | ||||||
|         if (finalResult == null) { |         if (finalResult == null) { | ||||||
|           finalResult = currentResult.clone(); |           finalResult = currentResult.clone(); | ||||||
|         } else { |         } else { | ||||||
| @ -97,6 +99,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | |||||||
|     required int numberOfRays, |     required int numberOfRays, | ||||||
|     double startAngle = 0, |     double startAngle = 0, | ||||||
|     double sweepAngle = tau, |     double sweepAngle = tau, | ||||||
|  |     double? maxDistance, | ||||||
|     List<Ray2>? rays, |     List<Ray2>? rays, | ||||||
|     List<ShapeHitbox>? ignoreHitboxes, |     List<ShapeHitbox>? ignoreHitboxes, | ||||||
|     List<RaycastResult<ShapeHitbox>>? out, |     List<RaycastResult<ShapeHitbox>>? out, | ||||||
| @ -126,7 +129,12 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | |||||||
|         result = RaycastResult(); |         result = RaycastResult(); | ||||||
|         out?.add(result); |         out?.add(result); | ||||||
|       } |       } | ||||||
|       result = raycast(ray, ignoreHitboxes: ignoreHitboxes, out: result); |       result = raycast( | ||||||
|  |         ray, | ||||||
|  |         maxDistance: maxDistance, | ||||||
|  |         ignoreHitboxes: ignoreHitboxes, | ||||||
|  |         out: result, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       if (result != null) { |       if (result != null) { | ||||||
|         results.add(result); |         results.add(result); | ||||||
|  | |||||||
| @ -926,6 +926,38 @@ void main() { | |||||||
|           closeToVector(Vector2(1, -1)..normalize()), |           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<ShapeHitbox>(); | ||||||
|  |  | ||||||
|  |         // 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', () { |     group('Rectangle hitboxes', () { | ||||||
| @ -1209,6 +1241,44 @@ void main() { | |||||||
|           expect(results.every((r) => r.isActive), isTrue); |           expect(results.every((r) => r.isActive), isTrue); | ||||||
|           expect(results.length, 4); |           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); | ||||||
|  |         }, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 DevKage
					DevKage