mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 17:06:50 +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 | ||||
| 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. | ||||
|  | ||||
|  | ||||
| @ -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, | ||||
|     ); | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|   /// 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<T extends Hitbox<T>, | ||||
|   /// result. | ||||
|   RaycastResult<T>? raycast( | ||||
|     Ray2 ray, { | ||||
|     double? maxDistance, | ||||
|     List<T>? ignoreHitboxes, | ||||
|     RaycastResult<T>? out, | ||||
|   }); | ||||
| @ -88,6 +92,9 @@ abstract class CollisionDetection<T extends Hitbox<T>, | ||||
|   /// 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<T extends Hitbox<T>, | ||||
|     required int numberOfRays, | ||||
|     double startAngle = 0, | ||||
|     double sweepAngle = tau, | ||||
|     double? maxDistance, | ||||
|     List<Ray2>? rays, | ||||
|     List<T>? ignoreHitboxes, | ||||
|     List<RaycastResult<T>>? out, | ||||
|  | ||||
| @ -67,6 +67,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | ||||
|   @override | ||||
|   RaycastResult<ShapeHitbox>? raycast( | ||||
|     Ray2 ray, { | ||||
|     double? maxDistance, | ||||
|     List<ShapeHitbox>? ignoreHitboxes, | ||||
|     RaycastResult<ShapeHitbox>? out, | ||||
|   }) { | ||||
| @ -80,7 +81,8 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | ||||
|       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<B extends Broadphase<ShapeHitbox>> | ||||
|     required int numberOfRays, | ||||
|     double startAngle = 0, | ||||
|     double sweepAngle = tau, | ||||
|     double? maxDistance, | ||||
|     List<Ray2>? rays, | ||||
|     List<ShapeHitbox>? ignoreHitboxes, | ||||
|     List<RaycastResult<ShapeHitbox>>? out, | ||||
| @ -126,7 +129,12 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>> | ||||
|         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); | ||||
|  | ||||
| @ -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<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', () { | ||||
| @ -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); | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 DevKage
					DevKage