Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: CircleHitbox should properly detect when ray is outside #3100

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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/rays_in_shape_example.dart';
import 'package:examples/stories/collision_detection/raytrace_example.dart';
import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';
Expand Down Expand Up @@ -76,5 +77,11 @@ void addCollisionDetectionStories(Dashbook dashbook) {
codeLink:
baseLink('collision_detection/raycast_max_distance_example.dart'),
info: RaycastMaxDistanceExample.description,
)
..add(
'Ray inside/outside shapes',
(_) => GameWidget(game: RaysInShapeExample()),
codeLink: baseLink('collision_detection/rays_in_shape_example.dart'),
info: RaysInShapeExample.description,
);
}
158 changes: 158 additions & 0 deletions examples/lib/stories/collision_detection/rays_in_shape_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import 'dart:async';
import 'dart:math';

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flutter/material.dart';

const playArea = Rect.fromLTRB(-100, -100, 100, 100);

class RaysInShapeExample extends FlameGame {
static const description = '''
In this example we showcase the raytrace functionality where you can see whether
the rays are inside the shapes or not. Click to change the shape that the rays
are casted against. The rays originates from small circles, and if the circle is
inside the shape it will be red, otherwise green. And if the ray doesn't hit any
shape it will be gray.
''';

RaysInShapeExample()
: super(
world: RaysInShapeWorld(),
camera: CameraComponent.withFixedResolution(
width: playArea.width,
height: playArea.height,
),
);
}

final whiteStroke = Paint()
..color = const Color(0xffffffff)
..style = PaintingStyle.stroke;

final lightStroke = Paint()
..color = const Color(0x50ffffff)
..style = PaintingStyle.stroke;

final greenStroke = Paint()
..color = const Color(0xff00ff00)
..style = PaintingStyle.stroke;

final redStroke = Paint()
..color = const Color(0xffff0000)
..style = PaintingStyle.stroke;

class RaysInShapeWorld extends World
with
HasGameReference<RaysInShapeExample>,
HasCollisionDetection,
TapCallbacks {
final _rng = Random();
List<Ray2> _rays = [];

List<Ray2> randomRays(int count) => List<Ray2>.generate(
count,
(index) => Ray2(
origin: (Vector2.random(_rng)) * playArea.size.width -
playArea.size.toVector2() / 2,
direction: (Vector2.random(_rng) - Vector2(0.5, 0.5)).normalized(),
),
);

int _componentIndex = 0;

final _components = [
CircleComponent(
radius: 60,
anchor: Anchor.center,
position: Vector2.zero(),
paint: whiteStroke,
children: [CircleHitbox()],
),
RectangleComponent(
size: Vector2(100, 100),
anchor: Anchor.center,
position: Vector2.zero(),
paint: whiteStroke,
children: [RectangleHitbox()],
),
PositionComponent(
position: Vector2.zero(),
children: [
PolygonHitbox.relative(
[
Vector2(-0.7, -1),
Vector2(1, -0.4),
Vector2(0.3, 1),
Vector2(-1, 0.6),
],
parentSize: Vector2(100, 100),
anchor: Anchor.center,
position: Vector2.zero(),
)
..paint = whiteStroke
..renderShape = true,
],
),
];

@override
FutureOr<void> onLoad() {
super.onLoad();
add(_components[_componentIndex]);
_rays = randomRays(200);
}

@override
void onTapUp(TapUpEvent event) {
super.onTapUp(event);
remove(_components[_componentIndex]);
_componentIndex = (_componentIndex + 1) % _components.length;
add(_components[_componentIndex]);
_recording.clear();
_rays = randomRays(200);
}

final Map<Ray2, RaycastResult<ShapeHitbox>?> _recording = {};

@override
void update(double dt) {
super.update(dt);

for (final ray in _rays) {
final result = collisionDetection.raycast(ray);
_recording.addAll({ray: result});
}
}

@override
void render(Canvas canvas) {
super.render(canvas);
for (final ray in _recording.keys) {
final result = _recording[ray];
if (result == null) {
canvas.drawLine(
ray.origin.toOffset(),
(ray.origin + ray.direction.scaled(10)).toOffset(),
lightStroke,
);
canvas.drawCircle(ray.origin.toOffset(), 1, lightStroke);
} else {
canvas.drawLine(
ray.origin.toOffset(),
result.intersectionPoint!.toOffset(),
lightStroke,
);
canvas.drawCircle(
ray.origin.toOffset(),
1,
result.isInsideHitbox ? redStroke : greenStroke,
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class CircleHitbox extends CircleComponent with ShapeHitbox {
..y *= (ray.direction.y.sign * _temporaryLineSegment.to.y.sign);
}

if (_temporaryLineSegment.to.length2 < radius * radius) {
if (ray.origin.distanceToSquared(_temporaryAbsoluteCenter) <
radius * radius) {
_temporaryLineSegment.to.scaleTo(2 * radius);
isInsideHitbox = true;
}
Expand Down
19 changes: 19 additions & 0 deletions packages/flame/test/collisions/collision_detection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,25 @@ void main() {
closeToVector(Vector2(0, 1)),
);
},
'ray from slightly outside of the CircleHitbox should not be counted '
'as inside': (collisionSystem) async {
final game = collisionSystem as FlameGame;
final world = game.world;
final positionComponent = PositionComponent(
position: Vector2.zero(),
anchor: Anchor.center,
size: Vector2.all(120),
)..add(CircleHitbox());
await world.ensureAdd(positionComponent);
await game.ready();
final ray = Ray2(
origin: Vector2(-38.06044293218409, -48.5986651724067),
direction: Vector2(0.927474693393028, -0.3738859359691247),
);
final result = collisionSystem.collisionDetection.raycast(ray);
expect(result?.hitbox?.parent, positionComponent);
expect(result?.isInsideHitbox, isFalse);
},
});
});

Expand Down
Loading