mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 10:38:17 +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:
@ -1,10 +1,45 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: f30b7f4db93ee747cd727df747941a28ead25ff5
|
||||
channel: beta
|
||||
revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: android
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: ios
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: linux
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: macos
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: web
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
- platform: windows
|
||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
||||
@ -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