mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-28 11:27:24 +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:
@ -223,6 +223,141 @@ class MyGame extends FlameGame with HasCollisionDetection {
|
||||
```
|
||||
|
||||
|
||||
## Ray casting and Ray tracing
|
||||
|
||||
Ray casting and ray tracing are methods for sending out rays from a point in your game and
|
||||
being able to see what these rays collide with and how they reflect after hitting
|
||||
something.
|
||||
|
||||
|
||||
### Ray casting
|
||||
|
||||
Ray casting is the operation of casting out one or more rays from a point and see if they hit
|
||||
anything, in Flame's case, hitboxes.
|
||||
|
||||
We provide two methods for doing so, `raycast` and `raycastAll`. The first one just casts out
|
||||
a single ray and gets back a result with information about what and where the ray hit, and some
|
||||
extra information like the distance, the normal and the reflection ray. The second one, `raycastAll`,
|
||||
works similarly but sends out multiple rays uniformly around the origin, or within an angle
|
||||
centered at the origin.
|
||||
|
||||
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.
|
||||
|
||||
Example:
|
||||
|
||||
```dart
|
||||
class MyGame extends FlameGame with HasCollisionDetection {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 100),
|
||||
direction: Vector2(1, 0),
|
||||
);
|
||||
final result = collisionDetection.raycast(ray);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example one can see that the `Ray2` class is being used, this class defines a ray from an
|
||||
origin position and a direction (which are both defined by `Vector2`s). This particular ray starts
|
||||
from `0, 100` and shoots a ray straight to the right.
|
||||
|
||||
The result from this operation will either be `null` if the ray didn't hit anything, or a
|
||||
`RaycastResult` which contains:
|
||||
- Which hitbox the ray hit
|
||||
- The intersection point of the collision
|
||||
- The reflection ray, i.e. how the ray would reflect on the hitbox that it hix
|
||||
- The normal of the collision, i.e. a vector perpendicular to the face of the hitbox that it hits
|
||||
|
||||
If you are concerned about performance you can pre create a `RaycastResult` object that you send in
|
||||
to the method with the `out` argument, this will make it possible for the method to reuse this
|
||||
object instead of creating a new one for each iteration. This can be good if you do a lot of
|
||||
ray casting in your `update` methods.
|
||||
|
||||
|
||||
#### raycastAll
|
||||
|
||||
Sometimes you want to send out rays in all, or a limited range, of directions from an origin. This
|
||||
can have a lot of applications, for example you could calculate the field of view of a player or
|
||||
enemy, or it can also be used to create light sources.
|
||||
|
||||
Example:
|
||||
|
||||
```dart
|
||||
class MyGame extends FlameGame with HasCollisionDetection {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final origin = Vector2(200, 200);
|
||||
final result = collisionDetection.raycastAll(
|
||||
origin,
|
||||
numberOfRays: 100,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example we would send out 100 rays from (200, 200) uniformingly spread in all directions.
|
||||
|
||||
If you want to limit the directions you can use the `startAngle` and the `sweepAngle` arguments.
|
||||
Where the `startAngle` (counting from straight up) is where the rays will start and then the rays
|
||||
will end at `startAngle + sweepAngle`.
|
||||
|
||||
If you are concerned about performance you can re-use the `RaycastResult` objects that are created
|
||||
by the function by sending them in as a list with the `out` argument.
|
||||
|
||||
|
||||
### Ray tracing
|
||||
|
||||
Ray tracing is similar to ray casting, but instead of just checking what the ray hits you can
|
||||
continue to trace the ray and see what its reflection ray (the ray bouncing off the hitbox) will
|
||||
hit and then what that casted reflection ray's reflection ray will hit and so on, until you decide
|
||||
that you have traced the ray for long enough. If you imagine how a pool ball would bounce on a pool
|
||||
table for example, that information could be retrieved with the help of ray tracing.
|
||||
|
||||
Example:
|
||||
|
||||
```dart
|
||||
class MyGame extends FlameGame with HasCollisionDetection {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 100),
|
||||
direction: Vector2(1, 1)..normalize()
|
||||
);
|
||||
final results = collisionDetection.raytrace(
|
||||
ray,
|
||||
maxDepth: 100,
|
||||
);
|
||||
for (final result in results) {
|
||||
if (result.intersectionPoint.distanceTo(ray.origin) > 300) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the example above we send out a ray from (0, 100) diagonally down to the right and we say that we
|
||||
want it the bounce on at most 100 hitboxes, it doesn't necessarily have to get 100 results since at
|
||||
some point one of the reflection rays might not hit a hitbox and then the method is done.
|
||||
|
||||
The method is lazy, which means that it will only do the calculations that you ask for, so you have
|
||||
to loop through the iterable that it returns to get the results, or do `toList()` to directly
|
||||
calculate all the results.
|
||||
|
||||
In the for-loop it can be seen how this can be used, in that loop we check whether the current
|
||||
reflection rays intersection point (where the previous ray hit the hitbox) is further away than 300
|
||||
pixels from the origin of the starting ray, and if it is we don't care about the rest of the results
|
||||
(and then they don't have to be calculated either).
|
||||
|
||||
If you are concerned about performance you can re-use the `RaycastResult` objects that are created
|
||||
by the function by sending them in as a list with the `out` argument.
|
||||
|
||||
|
||||
## Comparison to Forge2D
|
||||
|
||||
If you want to have a full-blown physics engine in your game we recommend that you use
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,4 +9,6 @@ export 'src/collisions/hitboxes/polygon_hitbox.dart';
|
||||
export 'src/collisions/hitboxes/rectangle_hitbox.dart';
|
||||
export 'src/collisions/hitboxes/screen_hitbox.dart';
|
||||
export 'src/collisions/hitboxes/shape_hitbox.dart';
|
||||
export 'src/collisions/standard_collision_detection.dart';
|
||||
export 'src/collisions/sweep.dart';
|
||||
export 'src/experimental/raycast_result.dart';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export 'src/extensions/canvas.dart';
|
||||
export 'src/extensions/color.dart';
|
||||
export 'src/extensions/double.dart';
|
||||
export 'src/extensions/image.dart';
|
||||
export 'src/extensions/matrix4.dart';
|
||||
export 'src/extensions/offset.dart';
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
export 'src/geometry/circle_component.dart';
|
||||
export 'src/geometry/constants.dart';
|
||||
export 'src/geometry/line.dart';
|
||||
export 'src/geometry/line_segment.dart';
|
||||
export 'src/geometry/polygon_component.dart';
|
||||
export 'src/geometry/polygon_ray_intersection.dart';
|
||||
export 'src/geometry/ray2.dart';
|
||||
export 'src/geometry/rectangle_component.dart';
|
||||
export 'src/geometry/shape_component.dart';
|
||||
|
||||
@ -12,6 +12,11 @@ abstract class Broadphase<T extends Hitbox<T>> {
|
||||
|
||||
Broadphase({List<T>? items}) : items = items ?? [];
|
||||
|
||||
/// This method can be used if there are things that needs to be prepared in
|
||||
/// each tick.
|
||||
void update() {}
|
||||
|
||||
/// Returns the potential hitbox collisions
|
||||
Set<CollisionProspect<T>> query();
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
|
||||
/// [CollisionDetection] is the foundation of the collision detection system in
|
||||
/// Flame.
|
||||
@ -26,6 +27,7 @@ abstract class CollisionDetection<T extends Hitbox<T>> {
|
||||
|
||||
/// Run collision detection for the current state of [items].
|
||||
void run() {
|
||||
broadphase.update();
|
||||
final potentials = broadphase.query();
|
||||
potentials.forEach((tuple) {
|
||||
final itemA = tuple.a;
|
||||
@ -64,64 +66,49 @@ abstract class CollisionDetection<T extends Hitbox<T>> {
|
||||
void handleCollisionStart(Set<Vector2> intersectionPoints, T itemA, T itemB);
|
||||
void handleCollision(Set<Vector2> intersectionPoints, T itemA, T itemB);
|
||||
void handleCollisionEnd(T itemA, T itemB);
|
||||
}
|
||||
|
||||
/// The default implementation of [CollisionDetection].
|
||||
/// Checks whether any [ShapeHitbox]s in [items] collide with each other and
|
||||
/// calls their callback methods accordingly.
|
||||
///
|
||||
/// By default the [Sweep] broadphase is used, this can be configured by
|
||||
/// passing in another [Broadphase] to the constructor.
|
||||
class StandardCollisionDetection extends CollisionDetection<ShapeHitbox> {
|
||||
StandardCollisionDetection({Broadphase<ShapeHitbox>? broadphase})
|
||||
: super(broadphase: broadphase ?? Sweep<ShapeHitbox>());
|
||||
|
||||
/// Check what the intersection points of two collidables are,
|
||||
/// returns an empty list if there are no intersections.
|
||||
@override
|
||||
Set<Vector2> intersections(
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
return hitboxA.intersections(hitboxB);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes when they first starts to collide.
|
||||
/// They are called with the [intersectionPoints] and instances of each other,
|
||||
/// so that they can determine what hitbox (and what
|
||||
/// [ShapeHitbox.hitboxParent] that they have collided with.
|
||||
@override
|
||||
void handleCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
hitboxA.onCollisionStart(intersectionPoints, hitboxB);
|
||||
hitboxB.onCollisionStart(intersectionPoints, hitboxA);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes every tick when they are colliding.
|
||||
/// They are called with the [intersectionPoints] and instances of each other,
|
||||
/// so that they can determine what hitbox (and what
|
||||
/// [ShapeHitbox.hitboxParent] that they have collided with.
|
||||
@override
|
||||
void handleCollision(
|
||||
Set<Vector2> intersectionPoints,
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
hitboxA.onCollision(intersectionPoints, hitboxB);
|
||||
hitboxB.onCollision(intersectionPoints, hitboxA);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes once when two hitboxes have stopped
|
||||
/// colliding.
|
||||
/// They are called with instances of each other, so that they can determine
|
||||
/// what hitbox (and what [ShapeHitbox.hitboxParent] that they have stopped
|
||||
/// colliding with.
|
||||
@override
|
||||
void handleCollisionEnd(ShapeHitbox hitboxA, ShapeHitbox hitboxB) {
|
||||
hitboxA.onCollisionEnd(hitboxB);
|
||||
hitboxB.onCollisionEnd(hitboxA);
|
||||
}
|
||||
|
||||
/// Returns the first hitbox that the given [ray] hits and the associated
|
||||
/// intersection information; or null if the ray doesn't hit any hitbox.
|
||||
///
|
||||
/// If [out] is provided that object will be modified and returned with the
|
||||
/// result.
|
||||
RaycastResult<T>? raycast(Ray2 ray, {RaycastResult<T>? out});
|
||||
|
||||
/// Casts rays uniformly between [startAngle] to [startAngle]+[sweepAngle]
|
||||
/// from the given [origin] and returns all hitboxes and intersection points
|
||||
/// the rays hit.
|
||||
/// [numberOfRays] is the number of rays that should be casted.
|
||||
///
|
||||
/// 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
|
||||
/// missing [Ray2] objects will be created and added to [rays].
|
||||
///
|
||||
/// If [out] is provided the [RaycastResult]s in that list be modified and
|
||||
/// returned with the result. If there are less objects in [out] than the
|
||||
/// result requires, the missing [RaycastResult] objects will be created.
|
||||
List<RaycastResult<T>> raycastAll(
|
||||
Vector2 origin, {
|
||||
required int numberOfRays,
|
||||
double startAngle = 0,
|
||||
double sweepAngle = tau,
|
||||
List<Ray2>? rays,
|
||||
List<RaycastResult<T>>? out,
|
||||
});
|
||||
|
||||
/// Follows the ray and its reflections until [maxDepth] is reached and then
|
||||
/// returns all hitboxes, intersection points, normals and reflection rays
|
||||
/// (bundled in a list of [RaycastResult]s) from where the ray hits.
|
||||
///
|
||||
/// [maxDepth] is how many times the ray should collide before returning a
|
||||
/// result, defaults to 10.
|
||||
///
|
||||
/// If [out] is provided the [RaycastResult]s in that list be modified and
|
||||
/// returned with the result. If there are less objects in [out] than the
|
||||
/// result requires, the missing [RaycastResult] objects will be created.
|
||||
Iterable<RaycastResult<T>> raytrace(
|
||||
Ray2 ray, {
|
||||
int maxDepth = 10,
|
||||
List<RaycastResult<T>>? out,
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,10 +4,11 @@ import 'package:flame/game.dart';
|
||||
/// Keeps track of all the [ShapeHitbox]s in the component tree and initiates
|
||||
/// collision detection every tick.
|
||||
mixin HasCollisionDetection on FlameGame {
|
||||
CollisionDetection<Hitbox> _collisionDetection = StandardCollisionDetection();
|
||||
CollisionDetection<Hitbox> get collisionDetection => _collisionDetection;
|
||||
CollisionDetection<ShapeHitbox> _collisionDetection =
|
||||
StandardCollisionDetection();
|
||||
CollisionDetection<ShapeHitbox> get collisionDetection => _collisionDetection;
|
||||
|
||||
set collisionDetection(CollisionDetection<Hitbox> cd) {
|
||||
set collisionDetection(CollisionDetection<ShapeHitbox> cd) {
|
||||
cd.addAll(_collisionDetection.items);
|
||||
_collisionDetection = cd;
|
||||
}
|
||||
@ -18,3 +19,26 @@ mixin HasCollisionDetection on FlameGame {
|
||||
collisionDetection.run();
|
||||
}
|
||||
}
|
||||
|
||||
/// This mixin is useful if you have written your own collision detection which
|
||||
/// isn't operating on [ShapeHitbox] since you can have any hitbox here.
|
||||
///
|
||||
/// Do note that [collisionDetection] has to be initialized before the game
|
||||
/// starts the update loop for the collision detection to work.
|
||||
mixin HasGenericCollisionDetection<T extends Hitbox<T>> on FlameGame {
|
||||
CollisionDetection<T>? _collisionDetection;
|
||||
CollisionDetection<T> get collisionDetection => _collisionDetection!;
|
||||
|
||||
set collisionDetection(CollisionDetection<T> cd) {
|
||||
if (_collisionDetection != null) {
|
||||
cd.addAll(_collisionDetection!.items);
|
||||
}
|
||||
_collisionDetection = cd;
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
_collisionDetection?.run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
|
||||
/// A [Hitbox] in the shape of a circle.
|
||||
class CircleHitbox extends CircleComponent with ShapeHitbox {
|
||||
@ -32,4 +33,68 @@ class CircleHitbox extends CircleComponent with ShapeHitbox {
|
||||
// There is no need to do anything here since the size already is bound to
|
||||
// the parent size and the radius is defined from the shortest side.
|
||||
}
|
||||
|
||||
late final _temporaryLineSegment = LineSegment.zero();
|
||||
late final _temporaryNormal = Vector2.zero();
|
||||
late final _temporaryCenter = Vector2.zero();
|
||||
late final _temporaryAbsoluteCenter = Vector2.zero();
|
||||
|
||||
@override
|
||||
RaycastResult<ShapeHitbox>? rayIntersection(
|
||||
Ray2 ray, {
|
||||
RaycastResult<ShapeHitbox>? out,
|
||||
}) {
|
||||
var isInsideHitbox = false;
|
||||
_temporaryLineSegment.from.setFrom(ray.origin);
|
||||
_temporaryAbsoluteCenter.setFrom(absoluteCenter);
|
||||
_temporaryCenter
|
||||
..setFrom(_temporaryAbsoluteCenter)
|
||||
..sub(ray.origin);
|
||||
_temporaryCenter.projection(ray.direction, out: _temporaryLineSegment.to);
|
||||
_temporaryLineSegment.to
|
||||
..x *= (ray.direction.x.sign * _temporaryLineSegment.to.x.sign)
|
||||
..y *= (ray.direction.y.sign * _temporaryLineSegment.to.y.sign);
|
||||
if (_temporaryLineSegment.to.length2 < radius * radius) {
|
||||
_temporaryLineSegment.to.scaleTo(2 * radius);
|
||||
isInsideHitbox = true;
|
||||
}
|
||||
_temporaryLineSegment.to.add(ray.origin);
|
||||
final intersections = lineSegmentIntersections(_temporaryLineSegment).where(
|
||||
(i) => i.distanceToSquared(ray.origin) > 0.0000001,
|
||||
);
|
||||
if (intersections.isEmpty) {
|
||||
out?.reset();
|
||||
return null;
|
||||
} else {
|
||||
final result = out ?? RaycastResult();
|
||||
final intersectionPoint = intersections.first;
|
||||
_temporaryNormal
|
||||
..setFrom(intersectionPoint)
|
||||
..sub(_temporaryAbsoluteCenter)
|
||||
..normalize();
|
||||
if (isInsideHitbox) {
|
||||
_temporaryNormal.invert();
|
||||
}
|
||||
final reflectionDirection =
|
||||
(out?.reflectionRay?.direction ?? Vector2.zero())
|
||||
..setFrom(ray.direction)
|
||||
..reflect(_temporaryNormal);
|
||||
|
||||
final reflectionRay = (out?.reflectionRay
|
||||
?..setWith(
|
||||
origin: intersectionPoint,
|
||||
direction: reflectionDirection,
|
||||
)) ??
|
||||
Ray2(origin: intersectionPoint, direction: reflectionDirection);
|
||||
|
||||
result.setWith(
|
||||
hitbox: this,
|
||||
reflectionRay: reflectionRay,
|
||||
normal: _temporaryNormal,
|
||||
distance: ray.origin.distanceTo(intersectionPoint),
|
||||
isInsideHitbox: isInsideHitbox,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
// ignore_for_file: comment_references
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
|
||||
/// A [Hitbox] in the shape of a polygon.
|
||||
class PolygonHitbox extends PolygonComponent with ShapeHitbox {
|
||||
class PolygonHitbox extends PolygonComponent
|
||||
with ShapeHitbox, PolygonRayIntersection {
|
||||
PolygonHitbox(
|
||||
super.vertices, {
|
||||
super.angle,
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/geometry/polygon_ray_intersection.dart';
|
||||
|
||||
/// A [Hitbox] in the shape of a rectangle (a simplified polygon).
|
||||
class RectangleHitbox extends RectangleComponent with ShapeHitbox {
|
||||
class RectangleHitbox extends RectangleComponent
|
||||
with ShapeHitbox, PolygonRayIntersection<RectangleHitbox> {
|
||||
@override
|
||||
final bool shouldFillParent;
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/src/geometry/shape_intersections.dart'
|
||||
as intersection_system;
|
||||
import 'package:meta/meta.dart';
|
||||
@ -129,6 +130,15 @@ mixin ShapeHitbox on ShapeComponent implements Hitbox<ShapeHitbox> {
|
||||
return collisionAllowed && aabb.intersectsWithAabb2(other.aabb);
|
||||
}
|
||||
|
||||
/// Returns information about how the ray intersects the shape.
|
||||
///
|
||||
/// If you are only interested in the intersection point use
|
||||
/// [RaycastResult.intersectionPoint] of the result.
|
||||
RaycastResult<ShapeHitbox>? rayIntersection(
|
||||
Ray2 ray, {
|
||||
RaycastResult<ShapeHitbox>? out,
|
||||
});
|
||||
|
||||
/// This determines how the shape should scale if it should try to fill its
|
||||
/// parents boundaries.
|
||||
void fillParent();
|
||||
|
||||
@ -0,0 +1,155 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
|
||||
/// The default implementation of [CollisionDetection].
|
||||
/// Checks whether any [ShapeHitbox]s in [items] collide with each other and
|
||||
/// calls their callback methods accordingly.
|
||||
///
|
||||
/// By default the [Sweep] broadphase is used, this can be configured by
|
||||
/// passing in another [Broadphase] to the constructor.
|
||||
class StandardCollisionDetection extends CollisionDetection<ShapeHitbox> {
|
||||
StandardCollisionDetection({Broadphase<ShapeHitbox>? broadphase})
|
||||
: super(broadphase: broadphase ?? Sweep<ShapeHitbox>());
|
||||
|
||||
/// Check what the intersection points of two collidables are,
|
||||
/// returns an empty list if there are no intersections.
|
||||
@override
|
||||
Set<Vector2> intersections(
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
return hitboxA.intersections(hitboxB);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes when they first starts to collide.
|
||||
/// They are called with the [intersectionPoints] and instances of each other,
|
||||
/// so that they can determine what hitbox (and what
|
||||
/// [ShapeHitbox.hitboxParent] that they have collided with.
|
||||
@override
|
||||
void handleCollisionStart(
|
||||
Set<Vector2> intersectionPoints,
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
hitboxA.onCollisionStart(intersectionPoints, hitboxB);
|
||||
hitboxB.onCollisionStart(intersectionPoints, hitboxA);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes every tick when they are colliding.
|
||||
/// They are called with the [intersectionPoints] and instances of each other,
|
||||
/// so that they can determine what hitbox (and what
|
||||
/// [ShapeHitbox.hitboxParent] that they have collided with.
|
||||
@override
|
||||
void handleCollision(
|
||||
Set<Vector2> intersectionPoints,
|
||||
ShapeHitbox hitboxA,
|
||||
ShapeHitbox hitboxB,
|
||||
) {
|
||||
hitboxA.onCollision(intersectionPoints, hitboxB);
|
||||
hitboxB.onCollision(intersectionPoints, hitboxA);
|
||||
}
|
||||
|
||||
/// Calls the two colliding hitboxes once when two hitboxes have stopped
|
||||
/// colliding.
|
||||
/// They are called with instances of each other, so that they can determine
|
||||
/// what hitbox (and what [ShapeHitbox.hitboxParent] that they have stopped
|
||||
/// colliding with.
|
||||
@override
|
||||
void handleCollisionEnd(ShapeHitbox hitboxA, ShapeHitbox hitboxB) {
|
||||
hitboxA.onCollisionEnd(hitboxB);
|
||||
hitboxB.onCollisionEnd(hitboxA);
|
||||
}
|
||||
|
||||
static final _temporaryRaycastResult = RaycastResult<ShapeHitbox>();
|
||||
|
||||
@override
|
||||
RaycastResult<ShapeHitbox>? raycast(
|
||||
Ray2 ray, {
|
||||
RaycastResult<ShapeHitbox>? out,
|
||||
}) {
|
||||
var finalResult = out?..reset();
|
||||
for (final item in items) {
|
||||
final currentResult =
|
||||
item.rayIntersection(ray, out: _temporaryRaycastResult);
|
||||
final possiblyFirstResult = !(finalResult?.isActive ?? false);
|
||||
if (currentResult != null &&
|
||||
(possiblyFirstResult ||
|
||||
currentResult.distance! < finalResult!.distance!)) {
|
||||
if (finalResult == null) {
|
||||
finalResult = currentResult.clone();
|
||||
} else {
|
||||
finalResult.setFrom(currentResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (finalResult?.isActive ?? false) ? finalResult : null;
|
||||
}
|
||||
|
||||
@override
|
||||
List<RaycastResult<ShapeHitbox>> raycastAll(
|
||||
Vector2 origin, {
|
||||
required int numberOfRays,
|
||||
double startAngle = 0,
|
||||
double sweepAngle = tau,
|
||||
List<Ray2>? rays,
|
||||
List<RaycastResult<ShapeHitbox>>? out,
|
||||
}) {
|
||||
final isFullCircle = (sweepAngle % tau).abs() < 0.0001;
|
||||
final angle = sweepAngle / (numberOfRays + (isFullCircle ? 0 : -1));
|
||||
final results = <RaycastResult<ShapeHitbox>>[];
|
||||
final direction = Vector2(1, 0);
|
||||
for (var i = 0; i < numberOfRays; i++) {
|
||||
Ray2 ray;
|
||||
if (i < (rays?.length ?? 0)) {
|
||||
ray = rays![i];
|
||||
} else {
|
||||
ray = Ray2.zero();
|
||||
rays?.add(ray);
|
||||
}
|
||||
ray.origin.setFrom(origin);
|
||||
direction
|
||||
..setValues(0, -1)
|
||||
..rotate(startAngle - angle * i);
|
||||
ray.direction = direction;
|
||||
|
||||
RaycastResult<ShapeHitbox>? result;
|
||||
if (i < (out?.length ?? 0)) {
|
||||
result = out![i];
|
||||
} else {
|
||||
result = RaycastResult();
|
||||
out?.add(result);
|
||||
}
|
||||
result = raycast(ray, out: result);
|
||||
|
||||
if (result != null) {
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<RaycastResult<ShapeHitbox>> raytrace(
|
||||
Ray2 ray, {
|
||||
int maxDepth = 10,
|
||||
List<RaycastResult<ShapeHitbox>>? out,
|
||||
}) sync* {
|
||||
out?.forEach((e) => e.reset());
|
||||
var currentRay = ray;
|
||||
for (var i = 0; i < maxDepth; i++) {
|
||||
final hasResultObject = (out?.length ?? 0) > i;
|
||||
final currentResult =
|
||||
hasResultObject ? out![i] : RaycastResult<ShapeHitbox>();
|
||||
if (raycast(currentRay, out: currentResult) != null) {
|
||||
currentRay = currentResult.reflectionRay!;
|
||||
if (!hasResultObject && out != null) {
|
||||
out.add(currentResult);
|
||||
}
|
||||
yield currentResult;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,20 @@
|
||||
import 'package:flame/src/collisions/broadphase.dart';
|
||||
import 'package:flame/src/collisions/collision_callbacks.dart';
|
||||
import 'package:flame/src/collisions/hitboxes/hitbox.dart';
|
||||
import 'package:flame/collisions.dart';
|
||||
|
||||
class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
|
||||
Sweep({super.items});
|
||||
|
||||
final List<T> _active = [];
|
||||
final Set<CollisionProspect<T>> _potentials = {};
|
||||
late final List<T> _active = [];
|
||||
late final Set<CollisionProspect<T>> _potentials = {};
|
||||
|
||||
@override
|
||||
void update() {
|
||||
items.sort((a, b) => a.aabb.min.x.compareTo(b.aabb.min.x));
|
||||
}
|
||||
|
||||
@override
|
||||
Set<CollisionProspect<T>> query() {
|
||||
_active.clear();
|
||||
_potentials.clear();
|
||||
items.sort((a, b) => (a.aabb.min.x - b.aabb.min.x).ceil());
|
||||
for (final item in items) {
|
||||
if (item.collisionType == CollisionType.inactive) {
|
||||
continue;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
|
||||
import 'package:flame/src/extensions/vector2.dart';
|
||||
import 'package:flame/src/game/transform2d.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// The circle with a given [center] and a [radius].
|
||||
///
|
||||
@ -85,6 +85,3 @@ class Circle extends Shape {
|
||||
@override
|
||||
String toString() => 'Circle([${_center.x}, ${_center.y}], $_radius)';
|
||||
}
|
||||
|
||||
@internal
|
||||
const tau = Transform2D.tau; // 2π
|
||||
|
||||
91
packages/flame/lib/src/experimental/raycast_result.dart
Normal file
91
packages/flame/lib/src/experimental/raycast_result.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/src/geometry/ray2.dart';
|
||||
|
||||
/// The result of a raycasting operation.
|
||||
///
|
||||
/// Note that the members of this class is heavily re-used. If you want to
|
||||
/// keep the result in an object, clone the parts you want, or the whole
|
||||
/// [RaycastResult] with [clone].
|
||||
///
|
||||
/// NOTE: This class might be subject to breaking changes in an upcoming
|
||||
/// version, to make it possible to calculate the values lazily.
|
||||
class RaycastResult<T extends Hitbox<T>> {
|
||||
RaycastResult({
|
||||
T? hitbox,
|
||||
Ray2? reflectionRay,
|
||||
Vector2? normal,
|
||||
double? distance,
|
||||
bool isInsideHitbox = false,
|
||||
}) : _isInsideHitbox = isInsideHitbox,
|
||||
_hitbox = hitbox,
|
||||
_reflectionRay = reflectionRay ?? Ray2.zero(),
|
||||
_normal = normal ?? Vector2.zero(),
|
||||
_distance = distance ?? double.maxFinite;
|
||||
|
||||
/// Whether this result has active results in it.
|
||||
///
|
||||
/// This is used so that the objects in there can continue to live even when
|
||||
/// there is no result from a ray cast.
|
||||
bool get isActive => _hitbox != null;
|
||||
|
||||
/// Whether the origin of the ray was inside the hitbox.
|
||||
bool get isInsideHitbox => _isInsideHitbox;
|
||||
bool _isInsideHitbox;
|
||||
|
||||
T? _hitbox;
|
||||
T? get hitbox => isActive ? _hitbox : null;
|
||||
|
||||
final Ray2 _reflectionRay;
|
||||
Ray2? get reflectionRay => isActive ? _reflectionRay : null;
|
||||
|
||||
Vector2? get intersectionPoint => reflectionRay?.origin;
|
||||
|
||||
double _distance;
|
||||
double? get distance => isActive ? _distance : null;
|
||||
|
||||
final Vector2 _normal;
|
||||
Vector2? get normal => isActive ? _normal : null;
|
||||
|
||||
void reset() => _hitbox = null;
|
||||
|
||||
/// Sets this [RaycastResult]'s objects to the values stored in [other].
|
||||
void setFrom(RaycastResult<T> other) {
|
||||
setWith(
|
||||
hitbox: other.hitbox,
|
||||
reflectionRay: other.reflectionRay,
|
||||
normal: other.normal,
|
||||
distance: other.distance,
|
||||
isInsideHitbox: other.isInsideHitbox,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sets the values of the result from the specified arguments.
|
||||
void setWith({
|
||||
T? hitbox,
|
||||
Ray2? reflectionRay,
|
||||
Vector2? normal,
|
||||
double? distance,
|
||||
bool isInsideHitbox = false,
|
||||
}) {
|
||||
_hitbox = hitbox;
|
||||
if (reflectionRay != null) {
|
||||
_reflectionRay.setFrom(reflectionRay);
|
||||
}
|
||||
if (normal != null) {
|
||||
_normal.setFrom(normal);
|
||||
}
|
||||
_distance = distance ?? double.maxFinite;
|
||||
_isInsideHitbox = isInsideHitbox;
|
||||
}
|
||||
|
||||
RaycastResult<T> clone() {
|
||||
return RaycastResult(
|
||||
hitbox: hitbox,
|
||||
reflectionRay: _reflectionRay.clone(),
|
||||
normal: _normal.clone(),
|
||||
distance: distance,
|
||||
isInsideHitbox: isInsideHitbox,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -87,8 +87,10 @@ class CircleComponent extends ShapeComponent implements SizeProvider {
|
||||
///
|
||||
/// This can be an empty list (if they don't intersect), one point (if the
|
||||
/// line is tangent) or two points (if the line is secant).
|
||||
/// An edge point of the [lineSegment] that originates on the edge of the
|
||||
/// circle doesn't count as an intersection.
|
||||
List<Vector2> lineSegmentIntersections(
|
||||
LineSegment line, {
|
||||
LineSegment lineSegment, {
|
||||
double epsilon = double.minPositive,
|
||||
}) {
|
||||
// A point on a line is `from + t*(to - from)`. We're trying to solve the
|
||||
@ -97,18 +99,18 @@ class CircleComponent extends ShapeComponent implements SizeProvider {
|
||||
// Expanding the norm, this becomes a square equation in `t`:
|
||||
// `t²Δ₂₁² + 2tΔ₂₁Δ₁₀ + Δ₁₀² - radius² == 0`.
|
||||
_delta21
|
||||
..setFrom(line.to)
|
||||
..sub(line.from); // to - from
|
||||
..setFrom(lineSegment.to)
|
||||
..sub(lineSegment.from); // to - from
|
||||
_delta10
|
||||
..setFrom(line.from)
|
||||
..setFrom(lineSegment.from)
|
||||
..sub(absoluteCenter); // from - absoluteCenter
|
||||
final a = _delta21.length2;
|
||||
final b = 2 * _delta21.dot(_delta10);
|
||||
final c = _delta10.length2 - radius * radius;
|
||||
|
||||
return solveQuadratic(a, b, c)
|
||||
.where((t) => t >= 0 && t <= 1)
|
||||
.map((t) => line.from.clone()..addScaled(_delta21, t))
|
||||
.where((t) => t > 0 && t <= 1)
|
||||
.map((t) => lineSegment.from.clone()..addScaled(_delta21, t))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
6
packages/flame/lib/src/geometry/constants.dart
Normal file
6
packages/flame/lib/src/geometry/constants.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// A simpler constant to use for angles than 2pi (well it is 2pi).
|
||||
///
|
||||
/// For example: tau/2 is 180 degrees, or pi radians.
|
||||
const tau = pi * 2;
|
||||
@ -2,12 +2,11 @@ import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/src/anchor.dart';
|
||||
import 'package:flame/src/cache/value_cache.dart';
|
||||
import 'package:flame/src/extensions/rect.dart';
|
||||
import 'package:flame/src/extensions/vector2.dart';
|
||||
import 'package:flame/src/geometry/line_segment.dart';
|
||||
import 'package:flame/src/geometry/shape_component.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class PolygonComponent extends ShapeComponent {
|
||||
@ -154,15 +153,13 @@ class PolygonComponent extends ShapeComponent {
|
||||
scale,
|
||||
angle,
|
||||
])) {
|
||||
var i = 0;
|
||||
for (final vertex in vertices) {
|
||||
vertices.forEachIndexed((i, vertex) {
|
||||
_globalVertices[i]
|
||||
..setFrom(vertex)
|
||||
..multiply(scale)
|
||||
..add(position)
|
||||
..rotate(angle, center: position);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
if (scale.y.isNegative || scale.x.isNegative) {
|
||||
// Since the list will be clockwise we have to reverse it for it to
|
||||
// become counterclockwise.
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
|
||||
/// Used to add the [rayIntersection] method to [RectangleHitbox] and
|
||||
/// [PolygonHitbox], used by the raytracing and raycasting methods.
|
||||
mixin PolygonRayIntersection<T extends ShapeHitbox> on PolygonComponent {
|
||||
late final _temporaryNormal = Vector2.zero();
|
||||
|
||||
/// Returns whether the [RaycastResult] if the [ray] intersects the polygon.
|
||||
///
|
||||
/// If [out] is defined that is used to populate with the result and then
|
||||
/// returned, to minimize the creation of new objects.
|
||||
RaycastResult<ShapeHitbox>? rayIntersection(
|
||||
Ray2 ray, {
|
||||
RaycastResult<ShapeHitbox>? out,
|
||||
}) {
|
||||
final vertices = globalVertices();
|
||||
var closestDistance = double.infinity;
|
||||
LineSegment? closestSegment;
|
||||
var crossings = 0;
|
||||
var isOverlappingPoint = false;
|
||||
for (var i = 0; i < vertices.length; i++) {
|
||||
final lineSegment = getEdge(i, vertices: vertices);
|
||||
final distance = ray.lineSegmentIntersection(lineSegment);
|
||||
// Using a small value above 0 just because of rounding errors later that
|
||||
// might cause a ray to go in the wrong direction.
|
||||
if (distance != null && distance > 0.0000000001) {
|
||||
crossings++;
|
||||
if (distance < closestDistance) {
|
||||
isOverlappingPoint = false;
|
||||
closestDistance = distance;
|
||||
closestSegment = lineSegment;
|
||||
} else if (distance == closestDistance) {
|
||||
isOverlappingPoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (crossings > 0) {
|
||||
final intersectionPoint =
|
||||
ray.point(closestDistance, out: out?.intersectionPoint);
|
||||
// This is "from" to "to" since it is defined ccw in the canvas
|
||||
// coordinate system
|
||||
_temporaryNormal
|
||||
..setFrom(closestSegment!.from)
|
||||
..sub(closestSegment.to);
|
||||
_temporaryNormal
|
||||
..setValues(_temporaryNormal.y, -_temporaryNormal.x)
|
||||
..normalize();
|
||||
var isInsideHitbox = false;
|
||||
if (crossings == 1 || isOverlappingPoint) {
|
||||
_temporaryNormal.invert();
|
||||
isInsideHitbox = true;
|
||||
}
|
||||
final reflectionDirection =
|
||||
(out?.reflectionRay?.direction ?? Vector2.zero())
|
||||
..setFrom(ray.direction)
|
||||
..reflect(_temporaryNormal);
|
||||
|
||||
final reflectionRay = (out?.reflectionRay
|
||||
?..setWith(
|
||||
origin: intersectionPoint,
|
||||
direction: reflectionDirection,
|
||||
)) ??
|
||||
Ray2(origin: intersectionPoint, direction: reflectionDirection);
|
||||
return (out ?? RaycastResult<ShapeHitbox>())
|
||||
..setWith(
|
||||
hitbox: this as T,
|
||||
reflectionRay: reflectionRay,
|
||||
normal: _temporaryNormal,
|
||||
distance: closestDistance,
|
||||
isInsideHitbox: isInsideHitbox,
|
||||
);
|
||||
}
|
||||
out?.reset();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,41 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/src/extensions/double.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
/// A ray in the 2d plane.
|
||||
///
|
||||
/// The [direction] should be normalized.
|
||||
class Ray2 {
|
||||
Ray2(this.origin, Vector2 direction) {
|
||||
Ray2({required this.origin, required Vector2 direction}) {
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
Ray2.zero() : this(Vector2.zero(), Vector2(1, 0));
|
||||
Ray2.zero() : this(origin: Vector2.zero(), direction: Vector2(1, 0));
|
||||
|
||||
/// The point where the ray originates from.
|
||||
Vector2 origin;
|
||||
final Vector2 _direction = Vector2.zero();
|
||||
|
||||
/// The normalized direction of the ray.
|
||||
///
|
||||
/// The values within the direction object should not be updated manually, use
|
||||
/// the setter instead.
|
||||
Vector2 get direction => _direction;
|
||||
set direction(Vector2 direction) {
|
||||
_direction.setFrom(direction);
|
||||
_updateInverses();
|
||||
}
|
||||
|
||||
final Vector2 _direction = Vector2.zero();
|
||||
|
||||
/// Should be called if the [direction] values are updated within the object
|
||||
/// instead of by the setter.
|
||||
void _updateInverses() {
|
||||
assert(
|
||||
(direction.length2 - 1).abs() < 0.000001,
|
||||
'direction must be normalized',
|
||||
);
|
||||
_direction.setFrom(direction);
|
||||
directionInvX = (1 / direction.x).toFinite();
|
||||
directionInvY = (1 / direction.y).toFinite();
|
||||
}
|
||||
@ -74,7 +86,9 @@ class Ray2 {
|
||||
/// [LineSegment] or null if there is no intersection.
|
||||
///
|
||||
/// A ray that is parallel and overlapping with the [segment] is considered to
|
||||
/// not intersect.
|
||||
/// not intersect. This is due to that a single intersection point can't be
|
||||
/// determined and that a [LineSegment] is almost always connected to another
|
||||
/// line segment which will get the intersection on one of its ends instead.
|
||||
double? lineSegmentIntersection(LineSegment segment) {
|
||||
_v1
|
||||
..setFrom(origin)
|
||||
@ -95,7 +109,7 @@ class Ray2 {
|
||||
|
||||
/// Deep clones the object, i.e. both [origin] and [direction] are cloned into
|
||||
/// a new [Ray2] object.
|
||||
Ray2 clone() => Ray2(origin.clone(), direction.clone());
|
||||
Ray2 clone() => Ray2(origin: origin.clone(), direction: direction.clone());
|
||||
|
||||
/// Sets the values by copying them from [other].
|
||||
void setFrom(Ray2 other) {
|
||||
@ -106,4 +120,7 @@ class Ray2 {
|
||||
this.origin.setFrom(origin);
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Ray2(origin: $origin, direction: $direction)';
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class PaletteEntry {
|
||||
}
|
||||
|
||||
class BasicPalette {
|
||||
static const PaletteEntry transparent = PaletteEntry(Color(0x00FFFFFF));
|
||||
static const PaletteEntry white = PaletteEntry(Color(0xFFFFFFFF));
|
||||
static const PaletteEntry black = PaletteEntry(Color(0xFF000000));
|
||||
static const PaletteEntry red = PaletteEntry(Color(0xFFFF0000));
|
||||
|
||||
@ -40,7 +40,7 @@ class _TestBlock extends PositionComponent with CollisionCallbacks {
|
||||
|
||||
void main() {
|
||||
group('Benchmark collision detection', () {
|
||||
testCollidableGame('collidable callbacks are called', (game) async {
|
||||
testCollisionDetectionGame('collidable callbacks are called', (game) async {
|
||||
final rng = Random(0);
|
||||
final blocks = List.generate(
|
||||
100,
|
||||
|
||||
@ -7,7 +7,7 @@ import 'collision_test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('Collision callbacks', () {
|
||||
testCollidableGame('collidable callbacks are called', (game) async {
|
||||
testCollisionDetectionGame('collidable callbacks are called', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -48,7 +48,7 @@ void main() {
|
||||
expect(blockB.endCounter, 1);
|
||||
});
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'collidable callbacks are called when removing a Collidable',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -76,7 +76,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame('hitbox callbacks are called', (game) async {
|
||||
testCollisionDetectionGame('hitbox callbacks are called', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -115,7 +115,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'hitbox callbacks are called when Collidable is removed',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -145,7 +145,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'hitbox end callbacks are called when hitbox is moved away fast',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -175,7 +175,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'onCollisionEnd is only called when there previously was a collision',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -199,7 +199,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'callbacks are only called once for hitboxes on each other',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -257,7 +257,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'end and start callbacks are only called once for hitboxes sharing a side',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
@ -316,7 +316,7 @@ void main() {
|
||||
);
|
||||
|
||||
// Reproduced #1478
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'collision callbacks with many hitboxes added',
|
||||
(game) async {
|
||||
const side = 10.0;
|
||||
@ -379,7 +379,7 @@ void main() {
|
||||
);
|
||||
|
||||
// Reproduced #1478
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'collision callbacks with changed game size',
|
||||
(game) async {
|
||||
final block = TestBlock(Vector2.all(20), Vector2.all(10))
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import 'package:flame/collisions.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/geometry.dart' as geometry;
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'collision_test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('LineSegment.isPointOnSegment', () {
|
||||
test('can catch simple point', () {
|
||||
@ -675,4 +679,561 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Raycasting', () {
|
||||
testCollisionDetectionGame('one hitbox', (game) async {
|
||||
game.ensureAdd(
|
||||
PositionComponent(
|
||||
children: [RectangleHitbox()],
|
||||
position: Vector2(100, 0),
|
||||
size: Vector2.all(100),
|
||||
anchor: Anchor.center,
|
||||
),
|
||||
);
|
||||
await game.ready();
|
||||
|
||||
final ray = Ray2(
|
||||
origin: Vector2.zero(),
|
||||
direction: Vector2(1, 0),
|
||||
);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(50, 0)));
|
||||
expect(result?.reflectionRay?.direction, closeToVector(Vector2(-1, 0)));
|
||||
});
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'multiple hitboxes after each other',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
for (var i = 0.0; i < 10; i++)
|
||||
PositionComponent(
|
||||
position: Vector2.all(100 + i * 10),
|
||||
size: Vector2.all(20 - i),
|
||||
anchor: Anchor.center,
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(
|
||||
origin: Vector2.zero(),
|
||||
direction: Vector2.all(1)..normalize(),
|
||||
);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2.all(90)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(-1, 1)..normalize()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray with origin on hitbox corner',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.all(10),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(
|
||||
origin: Vector2.all(10),
|
||||
direction: Vector2.all(1)..normalize(),
|
||||
);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(20, 20)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(1, -1)..normalize()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Rectangle hitboxes', () {
|
||||
testCollisionDetectionGame(
|
||||
'ray from within RectangleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.all(0),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(
|
||||
origin: Vector2.all(5),
|
||||
direction: Vector2.all(1)..normalize(),
|
||||
);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.normal, closeToVector(Vector2(0, -1)));
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 10)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(1, -1)..normalize()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the left of RectangleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(-1, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the top of RectangleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(0, -1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the right of RectangleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(1, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the bottom of RectangleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(RectangleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(0, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Circle hitboxes', () {
|
||||
testCollisionDetectionGame(
|
||||
'ray from top to bottom within CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(5, 4), direction: Vector2(0, 1));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.normal, closeToVector(Vector2(0, -1)));
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(0, -1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from bottom-right to top-left within CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(
|
||||
origin: Vector2.all(6),
|
||||
direction: Vector2.all(-1)..normalize(),
|
||||
);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.normal, closeToVector(Vector2.all(0.707106781186547)));
|
||||
expect(
|
||||
result?.intersectionPoint,
|
||||
closeToVector(Vector2.all(1.4644660940672631)),
|
||||
);
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2.all(1)..normalize()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from bottom within CircleHitbox going down',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final direction = Vector2(0, 1);
|
||||
final ray = Ray2(origin: Vector2(5, 6), direction: direction);
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.normal, closeToVector(Vector2(0, -1)));
|
||||
expect(
|
||||
result?.intersectionPoint,
|
||||
closeToVector(Vector2(5, 10)),
|
||||
);
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(direction.inverted()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the left of CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(-5, 5), direction: Vector2(1, 0));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(0, 5)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(-1, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the top of CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(5, -5), direction: Vector2(0, 1));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 0)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(0, -1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the right of CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(15, 5), direction: Vector2(-1, 0));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(10, 5)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(1, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testCollisionDetectionGame(
|
||||
'ray from the bottom of CircleHitbox',
|
||||
(game) async {
|
||||
game.ensureAddAll([
|
||||
PositionComponent(
|
||||
position: Vector2.zero(),
|
||||
size: Vector2.all(10),
|
||||
)..add(CircleHitbox()),
|
||||
]);
|
||||
await game.ready();
|
||||
final ray = Ray2(origin: Vector2(5, 15), direction: Vector2(0, -1));
|
||||
final result = game.collisionDetection.raycast(ray);
|
||||
expect(result?.hitbox?.parent, game.children.first);
|
||||
expect(result?.reflectionRay?.origin, closeToVector(Vector2(5, 10)));
|
||||
expect(
|
||||
result?.reflectionRay?.direction,
|
||||
closeToVector(Vector2(0, 1)),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('raycastAll', () {
|
||||
testCollisionDetectionGame(
|
||||
'All directions and all hits',
|
||||
(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);
|
||||
final results = game.collisionDetection.raycastAll(
|
||||
origin,
|
||||
numberOfRays: 4,
|
||||
);
|
||||
expect(results.every((r) => r.isActive), isTrue);
|
||||
expect(results.length, 4);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Raytracing', () {
|
||||
testCollisionDetectionGame('on single circle', (game) async {
|
||||
final circle = CircleComponent(
|
||||
radius: 10.0,
|
||||
position: Vector2.all(20),
|
||||
anchor: Anchor.center,
|
||||
)..add(CircleHitbox());
|
||||
await game.ensureAdd(circle);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 10),
|
||||
direction: Vector2.all(1.0)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray);
|
||||
expect(results.length, 1);
|
||||
expect(results.first.isActive, isTrue);
|
||||
expect(results.first.isInsideHitbox, isFalse);
|
||||
expect(results.first.intersectionPoint, Vector2(10, 20));
|
||||
final reflectionRay = results.first.reflectionRay;
|
||||
expect(reflectionRay?.origin, Vector2(10, 20));
|
||||
expect(reflectionRay?.direction, Vector2(-1, 1)..normalize());
|
||||
expect(results.first.normal, Vector2(-1, 0));
|
||||
});
|
||||
|
||||
testCollisionDetectionGame('on single rectangle', (game) async {
|
||||
final rectangle = RectangleComponent(
|
||||
position: Vector2.all(20),
|
||||
size: Vector2.all(20),
|
||||
anchor: Anchor.center,
|
||||
)..add(RectangleHitbox());
|
||||
await game.ensureAdd(rectangle);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 10),
|
||||
direction: Vector2.all(1.0)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray);
|
||||
expect(results.length, 1);
|
||||
expect(results.first.isActive, isTrue);
|
||||
expect(results.first.isInsideHitbox, isFalse);
|
||||
expect(results.first.intersectionPoint, Vector2(10, 20));
|
||||
final reflectionRay = results.first.reflectionRay;
|
||||
expect(reflectionRay?.origin, Vector2(10, 20));
|
||||
expect(reflectionRay?.direction, Vector2(-1, 1)..normalize());
|
||||
expect(results.first.normal, Vector2(-1, 0));
|
||||
});
|
||||
|
||||
testCollisionDetectionGame('on single rectangle with ray with negative X',
|
||||
(game) async {
|
||||
final rectangle = RectangleComponent(
|
||||
position: Vector2(-20, 40),
|
||||
size: Vector2.all(20),
|
||||
anchor: Anchor.center,
|
||||
)..add(RectangleHitbox());
|
||||
await game.ensureAdd(rectangle);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(10, 20),
|
||||
direction: Vector2(-1, 1)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray);
|
||||
expect(results.length, 1);
|
||||
expect(results.first.isActive, isTrue);
|
||||
expect(results.first.isInsideHitbox, isFalse);
|
||||
expect(results.first.intersectionPoint, Vector2(-10, 40));
|
||||
final reflectionRay = results.first.reflectionRay;
|
||||
expect(reflectionRay?.origin, Vector2(-10, 40));
|
||||
expect(reflectionRay?.direction, Vector2(1, 1)..normalize());
|
||||
expect(results.first.normal, Vector2(1, 0));
|
||||
});
|
||||
|
||||
testCollisionDetectionGame('on two circles', (game) async {
|
||||
final circle1 = CircleComponent(
|
||||
position: Vector2.all(20),
|
||||
radius: 10,
|
||||
anchor: Anchor.center,
|
||||
)..add(CircleHitbox());
|
||||
final circle2 = CircleComponent(
|
||||
position: Vector2(-20, 40),
|
||||
radius: 10,
|
||||
anchor: Anchor.center,
|
||||
)..add(CircleHitbox());
|
||||
await game.ensureAddAll([circle1, circle2]);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 10),
|
||||
direction: Vector2.all(1.0)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray).toList();
|
||||
expect(results.length, 2);
|
||||
expect(results.every((e) => e.isActive), isTrue);
|
||||
expect(results.every((e) => e.isInsideHitbox), isFalse);
|
||||
// First box
|
||||
expect(results[0].intersectionPoint, Vector2(10, 20));
|
||||
expect(results[0].normal, Vector2(-1, 0));
|
||||
final reflectionRay1 = results[0].reflectionRay;
|
||||
expect(reflectionRay1?.origin, Vector2(10, 20));
|
||||
expect(reflectionRay1?.direction, Vector2(-1, 1)..normalize());
|
||||
final results2 = game.collisionDetection.raytrace(reflectionRay1!);
|
||||
expect(results2.length, 1);
|
||||
// Second box
|
||||
expect(results[1].intersectionPoint, Vector2(-10, 40));
|
||||
expect(results[1].normal, Vector2(1, 0));
|
||||
final reflectionRay2 = results[1].reflectionRay;
|
||||
expect(reflectionRay2?.origin, Vector2(-10, 40));
|
||||
expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());
|
||||
});
|
||||
|
||||
testCollisionDetectionGame('on two rectangles', (game) async {
|
||||
final rectangle1 = RectangleComponent(
|
||||
position: Vector2.all(20),
|
||||
size: Vector2.all(20),
|
||||
anchor: Anchor.center,
|
||||
)..add(RectangleHitbox());
|
||||
final rectangle2 = RectangleComponent(
|
||||
position: Vector2(-20, 40),
|
||||
size: Vector2.all(20),
|
||||
anchor: Anchor.center,
|
||||
)..add(RectangleHitbox());
|
||||
await game.ensureAddAll([rectangle1, rectangle2]);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(0, 10),
|
||||
direction: Vector2.all(1.0)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray).toList();
|
||||
expect(results.length, 2);
|
||||
expect(results.every((e) => e.isActive), isTrue);
|
||||
expect(results.every((e) => e.isInsideHitbox), isFalse);
|
||||
// First box
|
||||
expect(results[0].intersectionPoint, Vector2(10, 20));
|
||||
expect(results[0].normal, Vector2(-1, 0));
|
||||
final reflectionRay1 = results[0].reflectionRay;
|
||||
expect(reflectionRay1?.origin, Vector2(10, 20));
|
||||
expect(reflectionRay1?.direction, Vector2(-1, 1)..normalize());
|
||||
final results2 =
|
||||
game.collisionDetection.raytrace(reflectionRay1!).toList();
|
||||
expect(results2.length, 1);
|
||||
// Second box
|
||||
expect(results[1].intersectionPoint, Vector2(-10, 40));
|
||||
expect(results[1].normal, Vector2(1, 0));
|
||||
final reflectionRay2 = results[1].reflectionRay;
|
||||
expect(reflectionRay2?.origin, Vector2(-10, 40));
|
||||
expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());
|
||||
});
|
||||
|
||||
testCollisionDetectionGame('on a rectangle within another', (game) async {
|
||||
final rectangle1 = RectangleComponent(
|
||||
position: Vector2.all(20),
|
||||
size: Vector2.all(20),
|
||||
)..add(RectangleHitbox());
|
||||
final rectangle2 = RectangleComponent(
|
||||
size: Vector2.all(200),
|
||||
)..add(RectangleHitbox());
|
||||
await game.ensureAddAll([rectangle1, rectangle2]);
|
||||
final ray = Ray2(
|
||||
origin: Vector2(20, 10),
|
||||
direction: Vector2.all(1.0)..normalize(),
|
||||
);
|
||||
final results = game.collisionDetection.raytrace(ray).toList();
|
||||
expect(results.length, 10);
|
||||
expect(results.every((e) => e.isActive), isTrue);
|
||||
expect(results[0].isInsideHitbox, isFalse);
|
||||
expect(results[1].isInsideHitbox, isTrue);
|
||||
// First box
|
||||
expect(results[0].intersectionPoint, Vector2(30, 20));
|
||||
expect(results[0].normal, Vector2(0, -1));
|
||||
final reflectionRay1 = results[0].reflectionRay;
|
||||
expect(reflectionRay1?.origin, Vector2(30, 20));
|
||||
expect(reflectionRay1?.direction, Vector2(1, -1)..normalize());
|
||||
final results2 =
|
||||
game.collisionDetection.raytrace(reflectionRay1!).toList();
|
||||
expect(results2.length, 10);
|
||||
// Second box
|
||||
expect(results[1].intersectionPoint, Vector2(50, 0));
|
||||
expect(results[1].normal, Vector2(0, 1));
|
||||
final reflectionRay2 = results[1].reflectionRay;
|
||||
expect(reflectionRay2?.origin, Vector2(50, 0));
|
||||
expect(reflectionRay2?.direction, Vector2(1, 1)..normalize());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ class Passthrough extends TestBlock with CollisionPassthrough {
|
||||
|
||||
void main() {
|
||||
group('CollisionPassthrough', () {
|
||||
testCollidableGame('Passing collisions to parent', (game) async {
|
||||
testCollisionDetectionGame('Passing collisions to parent', (game) async {
|
||||
final passthrough = Passthrough();
|
||||
final hitboxParent =
|
||||
TestBlock(Vector2.zero(), Vector2.all(10), addTestHitbox: false)
|
||||
|
||||
@ -7,7 +7,7 @@ import 'package:meta/meta.dart';
|
||||
class HasCollidablesGame extends FlameGame with HasCollisionDetection {}
|
||||
|
||||
@isTest
|
||||
Future<void> testCollidableGame(
|
||||
Future<void> testCollisionDetectionGame(
|
||||
String testName,
|
||||
Future Function(HasCollidablesGame) testBody,
|
||||
) {
|
||||
|
||||
@ -10,7 +10,7 @@ import 'collision_test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('CollisionType', () {
|
||||
testCollidableGame('actives do collide', (game) async {
|
||||
testCollisionDetectionGame('actives do collide', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -27,7 +27,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('passives do not collide', (game) async {
|
||||
testCollisionDetectionGame('passives do not collide', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -44,7 +44,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.isEmpty, true);
|
||||
});
|
||||
|
||||
testCollidableGame('inactives do not collide', (game) async {
|
||||
testCollisionDetectionGame('inactives do not collide', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -61,7 +61,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.isEmpty, true);
|
||||
});
|
||||
|
||||
testCollidableGame('active collides with static', (game) async {
|
||||
testCollisionDetectionGame('active collides with static', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -79,7 +79,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('passive collides with active', (game) async {
|
||||
testCollisionDetectionGame('passive collides with active', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -97,7 +97,8 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('passive does not collide with inactive', (game) async {
|
||||
testCollisionDetectionGame('passive does not collide with inactive',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -114,7 +115,8 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 0);
|
||||
});
|
||||
|
||||
testCollidableGame('inactive does not collide with static', (game) async {
|
||||
testCollisionDetectionGame('inactive does not collide with static',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -131,7 +133,8 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 0);
|
||||
});
|
||||
|
||||
testCollidableGame('active does not collide with inactive', (game) async {
|
||||
testCollisionDetectionGame('active does not collide with inactive',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -147,7 +150,8 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 0);
|
||||
});
|
||||
|
||||
testCollidableGame('inactive does not collide with active', (game) async {
|
||||
testCollisionDetectionGame('inactive does not collide with active',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -163,7 +167,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 0);
|
||||
});
|
||||
|
||||
testCollidableGame(
|
||||
testCollisionDetectionGame(
|
||||
'correct collisions with many involved collidables',
|
||||
(game) async {
|
||||
final rng = Random(0);
|
||||
@ -192,7 +196,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testCollidableGame('detects collision after scale', (game) async {
|
||||
testCollisionDetectionGame('detects collision after scale', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -217,7 +221,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('detects collision after flip', (game) async {
|
||||
testCollisionDetectionGame('detects collision after flip', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -242,7 +246,7 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('detects collision after scale', (game) async {
|
||||
testCollisionDetectionGame('detects collision after scale', (game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -267,7 +271,8 @@ void main() {
|
||||
expect(blockB.activeCollisions.length, 1);
|
||||
});
|
||||
|
||||
testCollidableGame('testPoint detects point after flip', (game) async {
|
||||
testCollisionDetectionGame('testPoint detects point after flip',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -280,7 +285,8 @@ void main() {
|
||||
expect(blockA.containsPoint(Vector2(-1, 1)), true);
|
||||
});
|
||||
|
||||
testCollidableGame('testPoint detects point after scale', (game) async {
|
||||
testCollisionDetectionGame('testPoint detects point after scale',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
@ -293,7 +299,8 @@ void main() {
|
||||
expect(blockA.containsPoint(Vector2.all(11)), true);
|
||||
});
|
||||
|
||||
testCollidableGame('detects collision on child components', (game) async {
|
||||
testCollisionDetectionGame('detects collision on child components',
|
||||
(game) async {
|
||||
final blockA = TestBlock(
|
||||
Vector2.zero(),
|
||||
Vector2.all(10),
|
||||
|
||||
@ -263,4 +263,19 @@ void main() {
|
||||
expect(w, Vector2.all(-1));
|
||||
});
|
||||
});
|
||||
|
||||
group('inversion', () {
|
||||
test('invert', () {
|
||||
final v = Vector2.all(1);
|
||||
v.invert();
|
||||
expect(v, Vector2.all(-1));
|
||||
});
|
||||
|
||||
test('inverted', () {
|
||||
final v = Vector2.all(1);
|
||||
final w = v.inverted();
|
||||
expect(v, Vector2.all(1));
|
||||
expect(w, Vector2.all(-1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/src/experimental/geometry/shapes/circle.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@ -9,8 +8,8 @@ void main() {
|
||||
test('Properly updates direction inverses', () {
|
||||
final direction = Vector2(-10.0, 3).normalized();
|
||||
final ray = Ray2(
|
||||
Vector2.all(2.0),
|
||||
direction,
|
||||
origin: Vector2.all(2.0),
|
||||
direction: direction,
|
||||
);
|
||||
expect(
|
||||
ray.directionInvX,
|
||||
@ -41,13 +40,16 @@ void main() {
|
||||
expect(
|
||||
() {
|
||||
Ray2(
|
||||
Vector2.all(2.0),
|
||||
direction,
|
||||
origin: Vector2.all(2.0),
|
||||
direction: direction,
|
||||
);
|
||||
},
|
||||
failsAssert('direction must be normalized'),
|
||||
);
|
||||
final ray = Ray2(Vector2.all(2.0), direction.normalized());
|
||||
final ray = Ray2(
|
||||
origin: Vector2.all(2.0),
|
||||
direction: direction.normalized(),
|
||||
);
|
||||
expect(
|
||||
() => ray.direction = direction,
|
||||
failsAssert('direction must be normalized'),
|
||||
@ -57,7 +59,8 @@ void main() {
|
||||
group('intersectsWithAabb2', () {
|
||||
test('Ray from the east', () {
|
||||
final direction = Vector2(1.0, 0.0);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, -1), Vector2(2, 1))),
|
||||
isTrue,
|
||||
@ -66,7 +69,8 @@ void main() {
|
||||
|
||||
test('Ray from the north', () {
|
||||
final direction = Vector2(0.0, 1.0);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(Aabb2.minMax(Vector2(-1, 1), Vector2(1, 2))),
|
||||
isTrue,
|
||||
@ -75,7 +79,8 @@ void main() {
|
||||
|
||||
test('Ray from the west', () {
|
||||
final direction = Vector2(-1.0, 0.0);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(
|
||||
Aabb2.minMax(Vector2(-2, -1), Vector2(-1, 1)),
|
||||
@ -86,7 +91,8 @@ void main() {
|
||||
|
||||
test('Ray from the south', () {
|
||||
final direction = Vector2(0.0, -1.0);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(
|
||||
Aabb2.minMax(Vector2(-1, -2), Vector2(1, -1)),
|
||||
@ -97,7 +103,8 @@ void main() {
|
||||
|
||||
test('Ray from the northEast', () {
|
||||
final direction = Vector2(0.5, 0.5);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, 1), Vector2(2, 2))),
|
||||
isTrue,
|
||||
@ -106,7 +113,8 @@ void main() {
|
||||
|
||||
test('Ray from the northWest', () {
|
||||
final direction = Vector2(-0.5, 0.5);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(Aabb2.minMax(Vector2(-2, 1), Vector2(-1, 2))),
|
||||
isTrue,
|
||||
@ -115,7 +123,8 @@ void main() {
|
||||
|
||||
test('Ray from the southWest', () {
|
||||
final direction = Vector2(-0.5, -0.5);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(
|
||||
Aabb2.minMax(Vector2(-2, -2), Vector2(-1, -1)),
|
||||
@ -126,7 +135,8 @@ void main() {
|
||||
|
||||
test('Ray from the southEast', () {
|
||||
final direction = Vector2(0.5, -0.5);
|
||||
final ray = Ray2(Vector2.zero(), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.zero(), direction: direction.normalized());
|
||||
expect(
|
||||
ray.intersectsWithAabb2(Aabb2.minMax(Vector2(1, -2), Vector2(2, -1))),
|
||||
isTrue,
|
||||
@ -140,7 +150,8 @@ void main() {
|
||||
const numberOfDirections = 16;
|
||||
for (var i = 0; i < numberOfDirections; i++) {
|
||||
direction.rotate(tau * (i / numberOfDirections));
|
||||
final ray = Ray2(Vector2.all(5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2.all(5), direction: direction.normalized());
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
ray.intersectsWithAabb2(aabb2),
|
||||
@ -160,7 +171,8 @@ void main() {
|
||||
final angle =
|
||||
(tau / 2 - 2 * epsilon) * (i / numberOfDirections) + epsilon;
|
||||
direction.rotate(angle);
|
||||
final ray = Ray2(Vector2(10, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(10, 5), direction: direction.normalized());
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
ray.intersectsWithAabb2(aabb2),
|
||||
@ -180,7 +192,8 @@ void main() {
|
||||
final angle =
|
||||
(tau / 2 - 2 * epsilon) * (i / numberOfDirections) + epsilon;
|
||||
direction.rotate(-angle);
|
||||
final ray = Ray2(Vector2(10, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(10, 5), direction: direction.normalized());
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
ray.intersectsWithAabb2(aabb2),
|
||||
@ -191,15 +204,17 @@ void main() {
|
||||
);
|
||||
|
||||
test(
|
||||
'Rays that originates and follows a box edge intersects',
|
||||
'Rays that originates and follows a box edge does intersects',
|
||||
() {
|
||||
final rayVertical = Ray2(Vector2(10, 5), Vector2(0, 1));
|
||||
final rayVertical =
|
||||
Ray2(origin: Vector2(10, 5), direction: Vector2(0, 1));
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
rayVertical.intersectsWithAabb2(aabb2),
|
||||
isTrue,
|
||||
);
|
||||
final rayHorizontal = Ray2(Vector2(5, 0), Vector2(1, 0));
|
||||
final rayHorizontal =
|
||||
Ray2(origin: Vector2(5, 0), direction: Vector2(1, 0));
|
||||
expect(
|
||||
rayHorizontal.intersectsWithAabb2(aabb2),
|
||||
isTrue,
|
||||
@ -210,13 +225,15 @@ void main() {
|
||||
test(
|
||||
'Rays that originates in a corner intersects',
|
||||
() {
|
||||
final rayZero = Ray2(Vector2.zero(), Vector2(0, 1));
|
||||
final rayZero =
|
||||
Ray2(origin: Vector2.zero(), direction: Vector2(0, 1));
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
rayZero.intersectsWithAabb2(aabb2),
|
||||
isTrue,
|
||||
);
|
||||
final rayTen = Ray2(Vector2.all(10), Vector2(0, -1));
|
||||
final rayTen =
|
||||
Ray2(origin: Vector2.all(10), direction: Vector2(0, -1));
|
||||
expect(
|
||||
rayTen.intersectsWithAabb2(aabb2),
|
||||
isTrue,
|
||||
@ -228,7 +245,8 @@ void main() {
|
||||
'Ray in the opposite direction does not intersect',
|
||||
() {
|
||||
final direction = Vector2(1, 0);
|
||||
final ray = Ray2(Vector2(15, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(15, 5), direction: direction.normalized());
|
||||
final aabb2 = Aabb2.minMax(Vector2.zero(), Vector2.all(10));
|
||||
expect(
|
||||
ray.intersectsWithAabb2(aabb2),
|
||||
@ -243,7 +261,8 @@ void main() {
|
||||
'Correct intersection point length on ray going east',
|
||||
() {
|
||||
final direction = Vector2(1, 0);
|
||||
final ray = Ray2(Vector2(5, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 5), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(10, 0), Vector2.all(10));
|
||||
expect(ray.lineSegmentIntersection(segment), 5);
|
||||
},
|
||||
@ -253,7 +272,8 @@ void main() {
|
||||
'Correct intersection point length on ray going west',
|
||||
() {
|
||||
final direction = Vector2(-1, 0);
|
||||
final ray = Ray2(Vector2(5, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 5), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(0, 10));
|
||||
expect(ray.lineSegmentIntersection(segment), 5);
|
||||
},
|
||||
@ -263,7 +283,8 @@ void main() {
|
||||
'Correct intersection point length on ray going south',
|
||||
() {
|
||||
final direction = Vector2(0, 1);
|
||||
final ray = Ray2(Vector2(5, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 5), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 10), Vector2(10, 10));
|
||||
expect(ray.lineSegmentIntersection(segment), 5);
|
||||
},
|
||||
@ -273,17 +294,19 @@ void main() {
|
||||
'Correct intersection point length on ray going north',
|
||||
() {
|
||||
final direction = Vector2(0, -1);
|
||||
final ray = Ray2(Vector2(5, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 5), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));
|
||||
expect(ray.lineSegmentIntersection(segment), 5);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Correct intersection point when ray originates on segment',
|
||||
'Origin as intersection point when ray originates on segment',
|
||||
() {
|
||||
final direction = Vector2(0, -1);
|
||||
final ray = Ray2(Vector2(5, 0), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 0), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));
|
||||
expect(ray.lineSegmentIntersection(segment), 0);
|
||||
},
|
||||
@ -293,7 +316,8 @@ void main() {
|
||||
'No intersection when ray is parallel and originates on segment',
|
||||
() {
|
||||
final direction = Vector2(1, 0);
|
||||
final ray = Ray2(Vector2(5, 0), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 0), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));
|
||||
expect(ray.lineSegmentIntersection(segment), null);
|
||||
},
|
||||
@ -303,7 +327,8 @@ void main() {
|
||||
'No intersection point when ray is parallel to the segment',
|
||||
() {
|
||||
final direction = Vector2(1, 0);
|
||||
final ray = Ray2(Vector2(-5, 0), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(-5, 0), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));
|
||||
expect(ray.lineSegmentIntersection(segment), null);
|
||||
},
|
||||
@ -313,7 +338,8 @@ void main() {
|
||||
'No intersection point when ray is parallel without intersection',
|
||||
() {
|
||||
final direction = Vector2(1, 0);
|
||||
final ray = Ray2(Vector2(5, 5), direction.normalized());
|
||||
final ray =
|
||||
Ray2(origin: Vector2(5, 5), direction: direction.normalized());
|
||||
final segment = LineSegment(Vector2(0, 0), Vector2(10, 0));
|
||||
expect(ray.lineSegmentIntersection(segment), null);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user