mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 08:56:01 +08:00 
			
		
		
		
	feat!: Raycasting and raytracing (#1785)
This PR implements raytracing and raycasting for the built-in hitboxes. If you pass in your own collision detection system to the HasCollisionDetection mixin you have to change the signature of that to: CollisionDetection<ShapeHitbox>instead of CollisionDetection<Hitbox>.
This commit is contained in:
		| @ -3,6 +3,9 @@ import 'package:examples/commons/commons.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/raycast_example.dart'; | ||||
| import 'package:examples/stories/collision_detection/raycast_light_example.dart'; | ||||
| import 'package:examples/stories/collision_detection/raytrace_example.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
|  | ||||
| void addCollisionDetectionStories(Dashbook dashbook) { | ||||
| @ -25,5 +28,23 @@ void addCollisionDetectionStories(Dashbook dashbook) { | ||||
|       (_) => GameWidget(game: MultipleShapesExample()), | ||||
|       codeLink: baseLink('collision_detection/multiple_shapes_example.dart'), | ||||
|       info: MultipleShapesExample.description, | ||||
|     ) | ||||
|     ..add( | ||||
|       'Raycasting (light)', | ||||
|       (_) => GameWidget(game: RaycastLightExample()), | ||||
|       codeLink: baseLink('collision_detection/raycast_light_example.dart'), | ||||
|       info: RaycastLightExample.description, | ||||
|     ) | ||||
|     ..add( | ||||
|       'Raycasting', | ||||
|       (_) => GameWidget(game: RaycastExample()), | ||||
|       codeLink: baseLink('collision_detection/raycast_example.dart'), | ||||
|       info: RaycastExample.description, | ||||
|     ) | ||||
|     ..add( | ||||
|       'Raytracing', | ||||
|       (_) => GameWidget(game: RaytraceExample()), | ||||
|       codeLink: baseLink('collision_detection/raytrace.dart'), | ||||
|       info: RaytraceExample.description, | ||||
|     ); | ||||
| } | ||||
|  | ||||
							
								
								
									
										137
									
								
								examples/lib/stories/collision_detection/raycast_example.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								examples/lib/stories/collision_detection/raycast_example.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,137 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flame/collisions.dart'; | ||||
| import 'package:flame/components.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/geometry.dart'; | ||||
| import 'package:flame/palette.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class RaycastExample extends FlameGame with HasCollisionDetection { | ||||
|   static const description = ''' | ||||
| In this example the raycast functionality is showcased. The circle moves around | ||||
| and casts 10 rays and checks how far the nearest hitboxes are and naively moves | ||||
| around trying not to hit them. | ||||
|   '''; | ||||
|  | ||||
|   Ray2? ray; | ||||
|   Ray2? reflection; | ||||
|   Vector2 origin = Vector2(250, 100); | ||||
|   Paint paint = Paint()..color = Colors.amber.withOpacity(0.6); | ||||
|   final speed = 100; | ||||
|   final inertia = 3.0; | ||||
|   final safetyDistance = 50; | ||||
|   final direction = Vector2(0, 1); | ||||
|   final velocity = Vector2.zero(); | ||||
|   final random = Random(); | ||||
|  | ||||
|   static const numberOfRays = 10; | ||||
|   final List<Ray2> rays = []; | ||||
|   final List<RaycastResult<ShapeHitbox>> results = []; | ||||
|  | ||||
|   late Path path; | ||||
|   @override | ||||
|   Future<void> onLoad() async { | ||||
|     final paint = BasicPalette.gray.paint() | ||||
|       ..style = PaintingStyle.stroke | ||||
|       ..strokeWidth = 2.0; | ||||
|     add(ScreenHitbox()); | ||||
|     add( | ||||
|       CircleComponent( | ||||
|         position: Vector2(100, 100), | ||||
|         radius: 50, | ||||
|         paint: paint, | ||||
|         children: [CircleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       CircleComponent( | ||||
|         position: Vector2(150, 500), | ||||
|         radius: 50, | ||||
|         paint: paint, | ||||
|         children: [CircleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2.all(300), | ||||
|         size: Vector2.all(100), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2.all(500), | ||||
|         size: Vector2(100, 200), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2(550, 200), | ||||
|         size: Vector2(200, 150), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   final _velocityModifier = Vector2.zero(); | ||||
|  | ||||
|   @override | ||||
|   void update(double dt) { | ||||
|     super.update(dt); | ||||
|     collisionDetection.raycastAll( | ||||
|       origin, | ||||
|       numberOfRays: numberOfRays, | ||||
|       rays: rays, | ||||
|       out: results, | ||||
|     ); | ||||
|     velocity.scale(inertia); | ||||
|     for (final result in results) { | ||||
|       _velocityModifier | ||||
|         ..setFrom(result.intersectionPoint!) | ||||
|         ..sub(origin) | ||||
|         ..normalize(); | ||||
|       if (result.distance! < safetyDistance) { | ||||
|         _velocityModifier.negate(); | ||||
|       } else if (random.nextDouble() < 0.2) { | ||||
|         velocity.add(_velocityModifier); | ||||
|       } | ||||
|       velocity.add(_velocityModifier); | ||||
|     } | ||||
|     velocity | ||||
|       ..normalize() | ||||
|       ..scale(speed * dt); | ||||
|     origin.add(velocity); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void render(Canvas canvas) { | ||||
|     super.render(canvas); | ||||
|     renderResult(canvas, origin, results, paint); | ||||
|   } | ||||
|  | ||||
|   void renderResult( | ||||
|     Canvas canvas, | ||||
|     Vector2 origin, | ||||
|     List<RaycastResult<ShapeHitbox>> results, | ||||
|     Paint paint, | ||||
|   ) { | ||||
|     final originOffset = origin.toOffset(); | ||||
|     for (final result in results) { | ||||
|       if (!result.isActive) { | ||||
|         continue; | ||||
|       } | ||||
|       final intersectionPoint = result.intersectionPoint!.toOffset(); | ||||
|       canvas.drawLine( | ||||
|         originOffset, | ||||
|         intersectionPoint, | ||||
|         paint, | ||||
|       ); | ||||
|     } | ||||
|     canvas.drawCircle(originOffset, 5, paint); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,163 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flame/collisions.dart'; | ||||
| import 'package:flame/components.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/geometry.dart'; | ||||
| import 'package:flame/input.dart'; | ||||
| import 'package:flame/palette.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class RaycastLightExample extends FlameGame | ||||
|     with HasCollisionDetection, TapDetector, MouseMovementDetector { | ||||
|   static const description = ''' | ||||
| In this example the raycast functionality is showcased by using it as a light | ||||
| source, if you move the mouse around the canvas the rays will be cast from its | ||||
| location. You can also tap to create a permanent source of rays that wont move | ||||
| with with mouse. | ||||
|   '''; | ||||
|  | ||||
|   Ray2? ray; | ||||
|   Ray2? reflection; | ||||
|   Vector2? origin; | ||||
|   Vector2? tapOrigin; | ||||
|   bool isOriginCasted = false; | ||||
|   bool isTapOriginCasted = false; | ||||
|   Paint paint = Paint(); | ||||
|   Paint tapPaint = Paint(); | ||||
|  | ||||
|   final _colorTween = ColorTween( | ||||
|     begin: Colors.blue.withOpacity(0.2), | ||||
|     end: Colors.red.withOpacity(0.2), | ||||
|   ); | ||||
|  | ||||
|   static const numberOfRays = 2000; | ||||
|   final List<Ray2> rays = []; | ||||
|   final List<Ray2> tapRays = []; | ||||
|   final List<RaycastResult<ShapeHitbox>> results = []; | ||||
|   final List<RaycastResult<ShapeHitbox>> tapResults = []; | ||||
|  | ||||
|   late Path path; | ||||
|   @override | ||||
|   Future<void> onLoad() async { | ||||
|     final paint = BasicPalette.gray.paint() | ||||
|       ..style = PaintingStyle.stroke | ||||
|       ..strokeWidth = 2.0; | ||||
|     add(ScreenHitbox()); | ||||
|     add( | ||||
|       CircleComponent( | ||||
|         position: Vector2(100, 100), | ||||
|         radius: 50, | ||||
|         paint: paint, | ||||
|         children: [CircleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       CircleComponent( | ||||
|         position: Vector2(150, 500), | ||||
|         radius: 50, | ||||
|         paint: paint, | ||||
|         children: [CircleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2.all(300), | ||||
|         size: Vector2.all(100), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2.all(500), | ||||
|         size: Vector2(100, 200), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|     add( | ||||
|       RectangleComponent( | ||||
|         position: Vector2(550, 200), | ||||
|         size: Vector2(200, 150), | ||||
|         paint: paint, | ||||
|         children: [RectangleHitbox()], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool onTapDown(TapDownInfo info) { | ||||
|     super.onTapDown(info); | ||||
|     final origin = info.eventPosition.game; | ||||
|     isTapOriginCasted = origin == tapOrigin; | ||||
|     tapOrigin = origin; | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onMouseMove(PointerHoverInfo info) { | ||||
|     final origin = info.eventPosition.game; | ||||
|     isOriginCasted = origin == this.origin; | ||||
|     this.origin = origin; | ||||
|   } | ||||
|  | ||||
|   var _timePassed = 0.0; | ||||
|  | ||||
|   @override | ||||
|   void update(double dt) { | ||||
|     super.update(dt); | ||||
|     _timePassed += dt; | ||||
|     paint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!; | ||||
|     tapPaint.color = _colorTween.transform(0.5 + (cos(_timePassed) / 2))!; | ||||
|     if (origin != null && !isOriginCasted) { | ||||
|       collisionDetection.raycastAll( | ||||
|         origin!, | ||||
|         numberOfRays: numberOfRays, | ||||
|         rays: rays, | ||||
|         out: results, | ||||
|       ); | ||||
|       isOriginCasted = true; | ||||
|     } | ||||
|     if (tapOrigin != null && !isTapOriginCasted) { | ||||
|       collisionDetection.raycastAll( | ||||
|         tapOrigin!, | ||||
|         numberOfRays: numberOfRays, | ||||
|         rays: tapRays, | ||||
|         out: tapResults, | ||||
|       ); | ||||
|       isTapOriginCasted = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void render(Canvas canvas) { | ||||
|     super.render(canvas); | ||||
|     if (origin != null) { | ||||
|       renderResult(canvas, origin!, results, paint); | ||||
|     } | ||||
|     if (tapOrigin != null) { | ||||
|       renderResult(canvas, tapOrigin!, tapResults, tapPaint); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void renderResult( | ||||
|     Canvas canvas, | ||||
|     Vector2 origin, | ||||
|     List<RaycastResult<ShapeHitbox>> results, | ||||
|     Paint paint, | ||||
|   ) { | ||||
|     final originOffset = origin.toOffset(); | ||||
|     for (final result in results) { | ||||
|       if (!result.isActive) { | ||||
|         continue; | ||||
|       } | ||||
|       final intersectionPoint = result.intersectionPoint!.toOffset(); | ||||
|       canvas.drawLine( | ||||
|         originOffset, | ||||
|         intersectionPoint, | ||||
|         paint, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										191
									
								
								examples/lib/stories/collision_detection/raytrace_example.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								examples/lib/stories/collision_detection/raytrace_example.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,191 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flame/collisions.dart'; | ||||
| import 'package:flame/components.dart'; | ||||
| import 'package:flame/events.dart'; | ||||
| import 'package:flame/game.dart'; | ||||
| import 'package:flame/geometry.dart'; | ||||
| import 'package:flame/palette.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class RaytraceExample extends FlameGame | ||||
|     with | ||||
|         HasCollisionDetection, | ||||
|         TapDetector, | ||||
|         MouseMovementDetector, | ||||
|         TapDetector { | ||||
|   static const description = ''' | ||||
| In this example the raytrace functionality is showcased. | ||||
| Click to start sending out a ray which will bounce around to visualize how it | ||||
| works. If you move the mouse around the canvas, rays and their reflections will | ||||
| be moved rendered and if you click again some more objects that the rays can | ||||
| bounce on will appear. | ||||
|   '''; | ||||
|  | ||||
|   final _colorTween = ColorTween( | ||||
|     begin: Colors.amber.withOpacity(1.0), | ||||
|     end: Colors.lightBlueAccent.withOpacity(1.0), | ||||
|   ); | ||||
|   final random = Random(); | ||||
|   Ray2? ray; | ||||
|   Ray2? reflection; | ||||
|   Vector2? origin; | ||||
|   bool isOriginCasted = false; | ||||
|   Paint rayPaint = Paint(); | ||||
|   final boxPaint = BasicPalette.gray.paint() | ||||
|     ..style = PaintingStyle.stroke | ||||
|     ..strokeWidth = 2.0; | ||||
|  | ||||
|   final List<Ray2> rays = []; | ||||
|   final List<RaycastResult<ShapeHitbox>> results = []; | ||||
|  | ||||
|   late Path path; | ||||
|   @override | ||||
|   Future<void> onLoad() async { | ||||
|     addAll([ | ||||
|       ScreenHitbox(), | ||||
|       CircleComponent( | ||||
|         radius: min(camera.canvasSize.x, camera.canvasSize.y) / 2, | ||||
|         paint: boxPaint, | ||||
|         children: [CircleHitbox()], | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   bool isClicked = false; | ||||
|   final extraChildren = <Component>[]; | ||||
|   @override | ||||
|   void onTap() { | ||||
|     if (!isClicked) { | ||||
|       isClicked = true; | ||||
|       return; | ||||
|     } | ||||
|     _timePassed = 0; | ||||
|     if (extraChildren.isEmpty) { | ||||
|       addAll( | ||||
|         extraChildren | ||||
|           ..addAll( | ||||
|             [ | ||||
|               CircleComponent( | ||||
|                 position: Vector2(100, 100), | ||||
|                 radius: 50, | ||||
|                 paint: boxPaint, | ||||
|                 children: [CircleHitbox()], | ||||
|               ), | ||||
|               CircleComponent( | ||||
|                 position: Vector2(150, 500), | ||||
|                 radius: 50, | ||||
|                 paint: boxPaint, | ||||
|                 anchor: Anchor.center, | ||||
|                 children: [CircleHitbox()], | ||||
|               ), | ||||
|               CircleComponent( | ||||
|                 position: Vector2(150, 500), | ||||
|                 radius: 150, | ||||
|                 paint: boxPaint, | ||||
|                 anchor: Anchor.center, | ||||
|                 children: [CircleHitbox()], | ||||
|               ), | ||||
|               RectangleComponent( | ||||
|                 position: Vector2.all(300), | ||||
|                 size: Vector2.all(100), | ||||
|                 paint: boxPaint, | ||||
|                 children: [RectangleHitbox()], | ||||
|               ), | ||||
|               RectangleComponent( | ||||
|                 position: Vector2.all(500), | ||||
|                 size: Vector2(100, 200), | ||||
|                 paint: boxPaint, | ||||
|                 children: [RectangleHitbox()], | ||||
|               ), | ||||
|               CircleComponent( | ||||
|                 position: Vector2(650, 275), | ||||
|                 radius: 50, | ||||
|                 paint: boxPaint, | ||||
|                 anchor: Anchor.center, | ||||
|                 children: [CircleHitbox()], | ||||
|               ), | ||||
|               RectangleComponent( | ||||
|                 position: Vector2(550, 200), | ||||
|                 size: Vector2(200, 150), | ||||
|                 paint: boxPaint, | ||||
|                 children: [RectangleHitbox()], | ||||
|               ), | ||||
|               RectangleComponent( | ||||
|                 position: Vector2(350, 30), | ||||
|                 size: Vector2(200, 150), | ||||
|                 paint: boxPaint, | ||||
|                 angle: tau / 10, | ||||
|                 children: [RectangleHitbox()], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|       ); | ||||
|     } else { | ||||
|       removeAll(extraChildren); | ||||
|       extraChildren.clear(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onMouseMove(PointerHoverInfo info) { | ||||
|     final origin = info.eventPosition.game; | ||||
|     isOriginCasted = origin == this.origin; | ||||
|     this.origin = origin; | ||||
|   } | ||||
|  | ||||
|   final Ray2 _ray = Ray2.zero(); | ||||
|   var _timePassed = 0.0; | ||||
|  | ||||
|   @override | ||||
|   void update(double dt) { | ||||
|     super.update(dt); | ||||
|     if (isClicked) { | ||||
|       _timePassed += dt; | ||||
|     } | ||||
|     rayPaint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!; | ||||
|     if (origin != null) { | ||||
|       _ray.origin.setFrom(origin!); | ||||
|       _ray.direction | ||||
|         ..setValues(1, 1) | ||||
|         ..normalize(); | ||||
|       collisionDetection | ||||
|           .raytrace( | ||||
|             _ray, | ||||
|             maxDepth: min((_timePassed * 8).ceil(), 1000), | ||||
|             out: results, | ||||
|           ) | ||||
|           .toList(); | ||||
|       isOriginCasted = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void render(Canvas canvas) { | ||||
|     super.render(canvas); | ||||
|     if (origin != null) { | ||||
|       renderResult(canvas, origin!, results, rayPaint); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void renderResult( | ||||
|     Canvas canvas, | ||||
|     Vector2 origin, | ||||
|     List<RaycastResult<ShapeHitbox>> results, | ||||
|     Paint paint, | ||||
|   ) { | ||||
|     var originOffset = origin.toOffset(); | ||||
|     for (final result in results) { | ||||
|       if (!result.isActive) { | ||||
|         continue; | ||||
|       } | ||||
|       final intersectionPoint = result.intersectionPoint!.toOffset(); | ||||
|       canvas.drawLine( | ||||
|         originOffset, | ||||
|         intersectionPoint, | ||||
|         paint, | ||||
|       ); | ||||
|       originOffset = intersectionPoint; | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Lukas Klingsbo
					Lukas Klingsbo