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:
Lukas Klingsbo
2022-08-19 22:44:18 +02:00
committed by GitHub
parent e2de70c98a
commit ed452dd172
34 changed files with 1896 additions and 160 deletions

View File

@ -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,
);
}

View 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);
}
}

View File

@ -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,
);
}
}
}

View 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;
}
}
}