mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-31 00:48:47 +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