Files
flame/test/components/collision_detection_test.dart
Lukas Klingsbo bde4585fa0 Collision detection (#633)
* Move out collision detection methods

* Add possibility to define a hull for PositionComponents

* Add example of how to use hull with tapable

* Update contains point comment

* Fix contains point

* Hull should be based on center position

* Remove collision detection parts

* Use percentage of size instead of absolute size

* Separate hull from PositionComponent

* Clarify hull example

* Fix formatting

* Change to relative import

* Use mixin for hitbox

* Update changelog

* Rename HasHitbox to Hitbox

* Clarified names

* Add spaces within braces

* Removed extra spaces in the braces

* Moved point rotation to Vector2 extension

* Render hitbox within extension

* Added collision detection

* Add tests

* Separate classes into files

* Fix formatting

* Move geometry files into geometry directory

* Use relative import for mixin

* Begin intersections between different shapes

* Add shape class

* Align with rebase

* Fix CHANGELOG

* Fix children positioning

* New polygon intersection algorithm

* No anchor for shape in PoC

* Remove unused imports

* Smarter bounding rectangle comparisons

* Formatting

* Add Circle to Circle collision

* Circle-polygon intersections

* Explanation of circle-circle intersections

* Properly render circle circle collisions

* Fix formatting

* Better example

* Update docs for collision detection

* Fix formatting

* Add polygon definition example

* Update documentation about the shapes

* Moved premature rc6 changelog line

* Added a cache system for shape calculations

* Fix formatting

* Fix formatting

* Fix imports

* Add collidable polygon to example

* Use anchorPosition for PositionComponent containsPoint

* Fix angle problem for Rectangle

* collisionCallback -> onCollision

* Fixed Erick's comments

* Improve collision detection example

* Fix #662, zero size doesn't contain any points

* Fix formatting

* Can't contain point if x or y is 0

* Fix formatting

* Fix test

* Remove unnecessary collidable example part

* Align with Draggable overhaul

* Updated collision detection docs

* Fix PR comments

* Have more sensible Circle constructor

* Clarify shape fields

* Need ensureInitialized

* Update docs to conform with switched constructors

* Fix new definitions

* Fix formatting

* Update documentation

* Fix formatting

* Fix formatting

* Exclude metrics check for test files

* Add another simpler example of collision detection

* Updated according to comments

* Fix comments

* Fix more comments

* Fix more comments

* Fix relative import

* Fix comments

* Moved export of geometry

* Fix comments

* Remove unused import

* Fix assert for shape.component

* Fix comments

* Expect instead of assert in test
2021-02-22 00:44:11 +01:00

685 lines
20 KiB
Dart

import 'package:flame/geometry.dart';
import 'package:flame/extensions.dart';
import 'package:flame/geometry.dart' as geometry;
import 'package:flame/src/geometry/circle.dart';
import 'package:flame/src/geometry/line_segment.dart';
import 'package:flame/src/geometry/line.dart';
import 'package:test/test.dart';
void main() {
group('LineSegment.isPointOnSegment tests', () {
test('Can catch simple point', () {
final segment = LineSegment(
Vector2.all(0),
Vector2.all(1),
);
final point = Vector2.all(0.5);
expect(
segment.containsPoint(point),
true,
reason: 'Point should be on segment',
);
});
test('Should not catch point outside of segment, but on line', () {
final segment = LineSegment(
Vector2.all(0),
Vector2.all(1),
);
final point = Vector2.all(3);
expect(
segment.containsPoint(point),
false,
reason: 'Point should not be on segment',
);
});
test('Should not catch point outside of segment', () {
final segment = LineSegment(
Vector2.all(0),
Vector2.all(1),
);
final point = Vector2(3, 2);
expect(
segment.containsPoint(point),
false,
reason: 'Point should not be on segment',
);
});
test('Point on end of segment', () {
final segment = LineSegment(
Vector2.all(0),
Vector2.all(1),
);
final point = Vector2.all(1);
expect(
segment.containsPoint(point),
true,
reason: 'Point should be on segment',
);
});
test('Point on beginning of segment', () {
final segment = LineSegment(
Vector2.all(0),
Vector2.all(1),
);
final point = Vector2.all(0);
expect(
segment.containsPoint(point),
true,
reason: 'Point should be on segment',
);
});
});
group('LineSegment.intersections tests', () {
test('Simple intersection', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2(0, 1), Vector2(1, 0));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isNotEmpty,
true,
reason: 'Should have intersection at (0.5, 0.5)',
);
expect(intersection.first == Vector2.all(0.5), true);
});
test('No intersection', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2(0, 1), Vector2(1, 2));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isEmpty,
true,
reason: 'Should not have any intersection',
);
});
test('Same line segments', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2.all(0), Vector2.all(1));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isNotEmpty,
true,
reason: 'Should have intersection at (0.5, 0.5)',
);
expect(intersection.first == Vector2.all(0.5), true);
});
test('Overlapping line segments', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2.all(0.5), Vector2.all(1.5));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isNotEmpty,
true,
reason: 'Should intersect at (0.75, 0.75)',
);
expect(intersection.first == Vector2.all(0.75), true);
});
test('One pixel overlap in different angles', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2.all(0), Vector2(1, -1));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isNotEmpty,
true,
reason: 'Should have intersection at (0, 0)',
);
expect(intersection.first == Vector2.all(0), true);
});
test('One pixel parallel overlap in same angle', () {
final segmentA = LineSegment(Vector2.all(0), Vector2.all(1));
final segmentB = LineSegment(Vector2.all(1), Vector2.all(2));
final intersection = segmentA.intersections(segmentB);
expect(
intersection.isNotEmpty,
true,
reason: 'Should have intersection at (1, 1)',
);
expect(intersection.first == Vector2.all(1), true);
});
});
group('Line.intersections tests', () {
test('Simple line intersection', () {
const line1 = const Line(1, -1, 0);
const line2 = const Line(1, 1, 0);
final intersection = line1.intersections(line2);
expect(intersection.isNotEmpty, true, reason: 'Should have intersection');
expect(intersection.first == Vector2.all(0), true);
});
test('Lines with c value', () {
const line1 = const Line(1, 1, 1);
const line2 = const Line(1, -1, 1);
final intersection = line1.intersections(line2);
expect(intersection.isNotEmpty, true, reason: 'Should have intersection');
expect(intersection.first == Vector2(1, 0), true);
});
test('Does not catch parallel lines', () {
const line1 = const Line(1, 1, -3);
const line2 = const Line(1, 1, 6);
final intersection = line1.intersections(line2);
expect(
intersection.isEmpty,
true,
reason: 'Should not have intersection',
);
});
test('Does not catch same line', () {
const line1 = const Line(1, 1, 1);
const line2 = const Line(1, 1, 1);
final intersection = line1.intersections(line2);
expect(
intersection.isEmpty,
true,
reason: 'Should not have intersection',
);
});
});
group('LinearEquation.fromPoints tests', () {
test('Simple line from points', () {
final line = Line.fromPoints(Vector2.zero(), Vector2.all(1));
expect(line.a == 1.0, true, reason: 'a value is not correct');
expect(line.b == -1.0, true, reason: 'b value is not correct');
expect(line.c == 0.0, true, reason: 'c value is not correct');
});
test('Line not going through origo', () {
final line = Line.fromPoints(Vector2(-2, 0), Vector2(0, 2));
expect(line.a == 2.0, true, reason: 'a value is not correct');
expect(line.b == -2.0, true, reason: 'b value is not correct');
expect(line.c == -4.0, true, reason: 'c value is not correct');
});
test('Straight vertical line', () {
final line = Line.fromPoints(Vector2.all(1), Vector2(1, -1));
expect(line.a == -2.0, true, reason: 'a value is not correct');
expect(line.b == 0.0, true, reason: 'b value is not correct');
expect(line.c == -2.0, true, reason: 'c value is not correct');
});
test('Straight horizontal line', () {
final line = Line.fromPoints(Vector2.all(1), Vector2(2, 1));
expect(line.a == 0.0, true, reason: 'a value is not correct');
expect(line.b == -1.0, true, reason: 'b value is not correct');
expect(line.c == -1.0, true, reason: 'c value is not correct');
});
});
group('LineSegment.pointsAt tests', () {
test('Simple pointing', () {
final segment = LineSegment(Vector2.zero(), Vector2.all(1));
const line = const Line(1, 1, 3);
final isPointingAt = segment.pointsAt(line);
expect(isPointingAt, true, reason: 'Line should be pointed at');
});
test('Is not pointed at when crossed', () {
final segment = LineSegment(Vector2.zero(), Vector2.all(3));
const line = const Line(1, 1, 3);
final isPointingAt = segment.pointsAt(line);
expect(isPointingAt, false, reason: 'Line should not be pointed at');
});
test('Is not pointed at when parallel', () {
final segment = LineSegment(Vector2.zero(), Vector2(1, -1));
const line = const Line(1, 1, 3);
final isPointingAt = segment.pointsAt(line);
expect(isPointingAt, false, reason: 'Line should not be pointed at');
});
test('Horizonal line can be pointed at', () {
final segment = LineSegment(Vector2.zero(), Vector2.all(1));
const line = const Line(0, 1, 2);
final isPointingAt = segment.pointsAt(line);
expect(isPointingAt, true, reason: 'Line should be pointed at');
});
test('Vertical line can be pointed at', () {
final segment = LineSegment(Vector2.zero(), Vector2.all(1));
const line = const Line(1, 0, 2);
final isPointingAt = segment.pointsAt(line);
expect(isPointingAt, true, reason: 'Line should be pointed at');
});
});
group('Polygon intersections tests', () {
test('Simple polygon collision', () {
final polygonA = Polygon([
Vector2(2, 2),
Vector2(3, 1),
Vector2(2, 0),
Vector2(1, 1),
]);
final polygonB = Polygon([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]);
final intersections = geometry.intersections(polygonA, polygonB);
expect(
intersections.contains(Vector2(1.5, 0.5)),
true,
reason: 'Missed one intersection',
);
expect(
intersections.contains(Vector2(1.5, 1.5)),
true,
reason: 'Missed one intersection',
);
expect(
intersections.length == 2,
true,
reason: 'Wrong number of intersections',
);
});
test('Collision on shared line segment', () {
final polygonA = Polygon([
Vector2(1, 1),
Vector2(1, 2),
Vector2(2, 2),
Vector2(2, 1),
]);
final polygonB = Polygon([
Vector2(2, 1),
Vector2(2, 2),
Vector2(3, 2),
Vector2(3, 1),
]);
final intersections = geometry.intersections(polygonA, polygonB);
expect(
intersections.containsAll([
Vector2(2.0, 2.0),
Vector2(2.0, 1.5),
Vector2(2.0, 1.0),
]),
true,
reason: 'Does not have all the correct intersection points',
);
expect(
intersections.length == 3,
true,
reason: 'Wrong number of intersections',
);
});
test('One point collision', () {
final polygonA = Polygon([
Vector2(1, 1),
Vector2(1, 2),
Vector2(2, 2),
Vector2(2, 1),
]);
final polygonB = Polygon([
Vector2(2, 2),
Vector2(2, 3),
Vector2(3, 3),
Vector2(3, 2),
]);
final intersections = geometry.intersections(polygonA, polygonB);
expect(
intersections.contains(Vector2(2.0, 2.0)),
true,
reason: 'Does not have all the correct intersection points',
);
expect(
intersections.length == 1,
true,
reason: 'Wrong number of intersections',
);
});
test('Collision while no corners are inside the other body', () {
final polygonA = Polygon.fromDefinition(
[
Vector2(1, 1),
Vector2(1, -1),
Vector2(-1, -1),
Vector2(-1, 1),
],
position: Vector2.zero(),
size: Vector2(2, 4),
);
final polygonB = Polygon.fromDefinition(
[
Vector2(1, 1),
Vector2(1, -1),
Vector2(-1, -1),
Vector2(-1, 1),
],
position: Vector2.zero(),
size: Vector2(4, 2),
);
final intersections = geometry.intersections(polygonA, polygonB);
expect(
intersections.containsAll([
Vector2(1, 1),
Vector2(1, -1),
Vector2(-1, 1),
Vector2(-1, -1),
]),
true,
reason: 'Does not have all the correct intersection points',
);
expect(
intersections.length == 4,
true,
reason: 'Wrong number of intersections',
);
});
test('Collision with advanced hitboxes in different quadrants', () {
final polygonA = Polygon([
Vector2(0, 0),
Vector2(-1, 1),
Vector2(0, 3),
Vector2(2, 2),
Vector2(1.5, 0.5),
]);
final polygonB = Polygon([
Vector2(-2, -2),
Vector2(-3, 0),
Vector2(-2, 3),
Vector2(1, 2),
Vector2(2, 1),
]);
final intersections = geometry.intersections(polygonA, polygonB);
intersections.containsAll([
Vector2(-0.2857142857142857, 2.4285714285714284),
Vector2(1.7500000000000002, 1.2500000000000002),
Vector2(1.5555555555555556, 0.6666666666666667),
Vector2(1.1999999999999997, 0.39999999999999997),
]);
expect(
intersections.length == 4,
true,
reason: 'Wrong number of intersections',
);
});
});
group('Rectangle intersections tests', () {
test('Simple intersection', () {
final rectangleA = Rectangle(
position: Vector2(4, 0),
size: Vector2.all(4),
);
final rectangleB = Rectangle(
position: Vector2.zero(),
size: Vector2.all(4),
);
final intersections = geometry.intersections(rectangleA, rectangleB);
expect(
intersections.containsAll([
Vector2(2, -2),
Vector2(2, 0),
Vector2(2, 2),
]),
true,
reason: 'Missed intersections',
);
expect(
intersections.length == 3,
true,
reason: 'Wrong number of intersections',
);
});
});
group('Circle intersections tests', () {
test('Simple collision', () {
final circleA = Circle.fromDefinition(
position: Vector2(4, 0),
size: Vector2.all(4),
);
final circleB = Circle.fromDefinition(
position: Vector2.zero(),
size: Vector2.all(4),
);
final intersections = geometry.intersections(circleA, circleB);
expect(
intersections.contains(Vector2(2, 0)),
true,
reason: 'Missed one intersection',
);
expect(
intersections.length == 1,
true,
reason: 'Wrong number of intersections',
);
});
test('Two point collision', () {
final circleA = Circle.fromDefinition(
position: Vector2(3, 0),
size: Vector2.all(4),
);
final circleB = Circle.fromDefinition(
position: Vector2.zero(),
size: Vector2.all(4),
);
final intersections = geometry.intersections(circleA, circleB);
expect(
intersections.contains(Vector2(1.5, -1.3228756555322954)),
true,
reason: 'Missed one intersection',
);
expect(
intersections.contains(Vector2(1.5, 1.3228756555322954)),
true,
reason: 'Missed one intersection',
);
expect(
intersections.length == 2,
true,
reason: 'Wrong number of intersections',
);
});
test('Same size and position', () {
final circleA = Circle.fromDefinition(
position: Vector2.all(3),
size: Vector2.all(4),
);
final circleB = Circle.fromDefinition(
position: Vector2.all(3),
size: Vector2.all(4),
);
final intersections = geometry.intersections(circleA, circleB);
expect(
intersections.containsAll([
Vector2(5, 3),
Vector2(3, 5),
Vector2(3, 1),
Vector2(1, 3),
]),
true,
reason: 'Missed intersections',
);
expect(
intersections.length == 4,
true,
reason: 'Wrong number of intersections',
);
});
test('Not overlapping', () {
final circleA = Circle.fromDefinition(
position: Vector2.all(-1),
size: Vector2.all(4),
);
final circleB = Circle.fromDefinition(
position: Vector2.all(3),
size: Vector2.all(4),
);
final intersections = geometry.intersections(circleA, circleB);
expect(
intersections.isEmpty,
true,
reason: 'Should not have any intersections',
);
});
test('In third quadrant', () {
final circleA = Circle.fromDefinition(
position: Vector2.all(-1),
size: Vector2.all(2),
);
final circleB = Circle.fromDefinition(
position: Vector2.all(-2),
size: Vector2.all(2),
);
final intersections = geometry.intersections(circleA, circleB).toList();
expect(
intersections.any((v) => v.distanceTo(Vector2(-1, -2)) < 0.000001),
true,
);
expect(
intersections.any((v) => v.distanceTo(Vector2(-2, -1)) < 0.000001),
true,
);
expect(
intersections.length == 2,
true,
reason: 'Wrong number of intersections',
);
});
test('In different quadrants', () {
final circleA = Circle.fromDefinition(
position: Vector2.all(-1),
size: Vector2.all(4),
);
final circleB = Circle.fromDefinition(
position: Vector2.all(1),
size: Vector2.all(4),
);
final intersections = geometry.intersections(circleA, circleB).toList();
expect(
intersections.any((v) => v.distanceTo(Vector2(1, -1)) < 0.000001),
true,
);
expect(
intersections.any((v) => v.distanceTo(Vector2(-1, 1)) < 0.000001),
true,
);
expect(
intersections.length == 2,
true,
reason: 'Wrong number of intersections',
);
});
});
group('Circle-Polygon intersections tests', () {
test('Simple circle-polygon intersection', () {
final circle = Circle.fromDefinition(
position: Vector2.zero(),
size: Vector2.all(2),
);
final polygon = Polygon([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]);
final intersections = geometry.intersections(circle, polygon);
expect(
intersections.containsAll([Vector2(0, 1), Vector2(1, 0)]),
true,
reason: 'Missed intersections',
);
expect(
intersections.length == 2,
true,
reason: 'Wrong number of intersections',
);
});
test('Single point circle-polygon intersection', () {
final circle = Circle.fromDefinition(
position: Vector2(-1, 1),
size: Vector2.all(2),
);
final polygon = Polygon([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]);
final intersections = geometry.intersections(circle, polygon);
expect(
intersections.contains(Vector2(0, 1)),
true,
reason: 'Missed intersections',
);
expect(
intersections.length == 1,
true,
reason: 'Wrong number of intersections',
);
});
test('Four point circle-polygon intersection', () {
final circle = Circle.fromDefinition(
position: Vector2.all(1),
size: Vector2.all(2),
);
final polygon = Polygon([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]);
final intersections = geometry.intersections(circle, polygon);
expect(
intersections.containsAll([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]),
true,
reason: 'Missed intersections',
);
expect(
intersections.length == 4,
true,
reason: 'Wrong number of intersections',
);
});
test('Polygon within circle, no intersections', () {
final circle = Circle.fromDefinition(
position: Vector2.all(1),
size: Vector2.all(2.1),
);
final polygon = Polygon([
Vector2(1, 2),
Vector2(2, 1),
Vector2(1, 0),
Vector2(0, 1),
]);
final intersections = geometry.intersections(circle, polygon);
expect(
intersections.isEmpty,
true,
reason: 'Should not be any intersections',
);
});
});
}