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