Bring back collision detection example (#736)

This commit is contained in:
Lukas Klingsbo
2021-04-05 13:33:12 +02:00
committed by GitHub
parent 1037aba8ea
commit 1091f441ac
10 changed files with 373 additions and 19 deletions

View File

@ -25,6 +25,11 @@ the latter is very useful for accurate gesture detection. The collision detectio
what should happen when two hitboxes collide, so it is up to the user to implement what will happen
when for example two position components have intersecting hitboxes.
Do note that the built-in collision detection system does not take collisions between two hitboxes
that overshoot each other into account, this could happen when they either move too fast or `update`
being called with a large delta time (for example if your app is not in the foreground). This
behaviour is called tunneling, if you want to read more about it.
## Mixins
### Hitbox
The `Hitbox` mixin is mainly used for two things; to make detection of collisions with other

View File

@ -59,8 +59,12 @@ Example of usage, where visibility of two components are handled by a wrapper:
```dart
class GameOverPanel extends PositionComponent with HasGameRef<MyGame> {
bool visible = false;
final Image spriteImage;
GameOverPanel(Image spriteImage) : super() {
GameOverPanel(this.spriteImage);
@override
Future<void> onLoad() async {
final gameOverText = GameOverText(spriteImage); // GameOverText is a Component
final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent
@ -197,7 +201,7 @@ import 'package:flame_flare/flame_flare.dart';
class YourFlareController extends FlareControls {
ActorNode rightHandNode;
late ActorNode rightHandNode;
void initialize(FlutterActorArtboard artboard) {
super.initialize(artboard);

View File

@ -3,6 +3,7 @@ import 'package:flame/flame.dart';
import 'package:flutter/material.dart';
import 'stories/animations/animations.dart';
import 'stories/collision_detection/collision_detection.dart';
import 'stories/components/components.dart';
import 'stories/controls/controls.dart';
import 'stories/effects/effects.dart';
@ -15,12 +16,13 @@ import 'stories/widgets/widgets.dart';
void main() async {
final dashbook = Dashbook(
title: 'Flame Example',
title: 'Flame Examples',
theme: ThemeData.dark(),
);
addAnimationStories(dashbook);
addComponentsStories(dashbook);
addCollisionDetectionStories(dashbook);
addEffectsStories(dashbook);
addTileMapStories(dashbook);
addControlsStories(dashbook);

View File

@ -0,0 +1,71 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/gestures.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
class MyCollidable extends PositionComponent
with HasGameRef<Circles>, Hitbox, Collidable {
late Vector2 velocity;
final _collisionColor = Colors.amber;
final _defaultColor = Colors.cyan;
bool _isWallHit = false;
bool _isCollision = false;
MyCollidable(Vector2 position)
: super(
position: position,
size: Vector2.all(100),
anchor: Anchor.center,
) {
addShape(HitboxCircle());
}
@override
Future<void> onLoad() async {
final center = gameRef.size / 2;
velocity = (center - position)..scaleTo(150);
}
@override
void update(double dt) {
super.update(dt);
if (_isWallHit) {
remove();
return;
}
debugColor = _isCollision ? _collisionColor : _defaultColor;
position.add(velocity * dt);
_isCollision = false;
}
@override
void render(Canvas canvas) {
super.render(canvas);
renderShapes(canvas);
}
@override
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
if (other is ScreenCollidable) {
_isWallHit = true;
return;
}
_isCollision = true;
}
}
class Circles extends BaseGame with HasCollidables, TapDetector {
@override
Future<void> onLoad() async {
add(ScreenCollidable());
}
@override
void onTapDown(TapDownDetails details) {
add(MyCollidable(details.localPosition.toVector2()));
}
}

View File

@ -0,0 +1,20 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import '../../commons/commons.dart';
import 'circles.dart';
import 'multiple_shapes.dart';
void addCollisionDetectionStories(Dashbook dashbook) {
dashbook.storiesOf('Collision Detection')
..add(
'Circles',
(_) => GameWidget(game: Circles()),
codeLink: baseLink('collision_detection/circles.dart'),
)
..add(
'Multiple shapes',
(_) => GameWidget(game: MultipleShapes()),
codeLink: baseLink('collision_detection/multiple_shapes.dart'),
);
}

View File

@ -0,0 +1,237 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
enum Shapes { circle, rectangle, polygon }
abstract class MyCollidable extends PositionComponent
with Draggable, Hitbox, Collidable {
double rotationSpeed = 0.0;
final Vector2 velocity;
final delta = Vector2.zero();
double angleDelta = 0;
bool _isDragged = false;
final _activePaint = Paint()..color = Colors.amber;
double _wallHitTime = double.infinity;
MyCollidable(Vector2 position, Vector2 size, this.velocity) {
this.position = position;
this.size = size;
anchor = Anchor.center;
}
@override
void update(double dt) {
super.update(dt);
if (_isDragged) {
return;
}
_wallHitTime += dt;
delta.setFrom(velocity * dt);
position.add(delta);
angleDelta = dt * rotationSpeed;
angle = (angle + angleDelta) % (2 * pi);
}
@override
void render(Canvas canvas) {
super.render(canvas);
renderShapes(canvas);
final localCenter = (size / 2).toOffset();
if (_isDragged) {
canvas.drawCircle(localCenter, 5, _activePaint);
}
if (_wallHitTime < 1.0) {
// Show a rectangle in the center for a second if we hit the wall
canvas.drawRect(
Rect.fromCenter(center: localCenter, width: 10, height: 10),
debugPaint,
);
}
}
@override
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
final averageIntersection = intersectionPoints.reduce((sum, v) => sum + v) /
intersectionPoints.length.toDouble();
final collisionDirection = (averageIntersection - absoluteCenter)
..normalize()
..round();
if (velocity.angleToSigned(collisionDirection).abs() > 3) {
// This entity got hit by something else
return;
}
final angleToCollision = velocity.angleToSigned(collisionDirection);
if (angleToCollision.abs() < pi / 8) {
velocity.rotate(pi);
} else {
velocity.rotate(-pi / 2 * angleToCollision.sign);
}
position.sub(delta * 2);
angle = (angle - angleDelta) % (2 * pi);
if (other is ScreenCollidable) {
_wallHitTime = 0;
}
}
@override
bool onDragUpdate(int pointerId, DragUpdateDetails details) {
_isDragged = true;
return true;
}
@override
bool onDragEnd(int pointerId, DragEndDetails details) {
velocity.setFrom(details.velocity.pixelsPerSecond.toVector2() / 10);
_isDragged = false;
return true;
}
}
class CollidablePolygon extends MyCollidable {
CollidablePolygon(Vector2 position, Vector2 size, Vector2 velocity)
: super(position, size, velocity) {
final shape = HitboxPolygon([
Vector2(-1.0, 0.0),
Vector2(-0.8, 0.6),
Vector2(0.0, 1.0),
Vector2(0.6, 0.9),
Vector2(1.0, 0.0),
Vector2(0.6, -0.8),
Vector2(0, -1.0),
Vector2(-0.8, -0.8),
]);
addShape(shape);
}
}
class CollidableRectangle extends MyCollidable {
CollidableRectangle(Vector2 position, Vector2 size, Vector2 velocity)
: super(position, size, velocity) {
addShape(HitboxRectangle());
}
}
class CollidableCircle extends MyCollidable {
CollidableCircle(Vector2 position, Vector2 size, Vector2 velocity)
: super(position, size, velocity) {
final shape = HitboxCircle();
addShape(shape);
}
}
class SnowmanPart extends HitboxCircle {
static const startColor = Colors.white;
final hitPaint = Paint()
..color = startColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
SnowmanPart(double definition, Vector2 relativePosition, Color hitColor)
: super(definition: definition) {
this.relativePosition.setFrom(relativePosition);
onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) {
if (other.component is ScreenCollidable) {
hitPaint..color = startColor;
} else {
hitPaint..color = hitColor;
}
};
}
@override
void render(Canvas canvas, Paint paint) {
super.render(canvas, hitPaint);
}
}
class CollidableSnowman extends MyCollidable {
CollidableSnowman(Vector2 position, Vector2 size, Vector2 velocity)
: super(position, size, velocity) {
rotationSpeed = 0.2;
final top = SnowmanPart(0.4, Vector2(0, -0.8), Colors.red);
final middle = SnowmanPart(0.6, Vector2(0, -0.3), Colors.yellow);
final bottom = SnowmanPart(1.0, Vector2(0, 0.5), Colors.green);
addShape(top);
addShape(middle);
addShape(bottom);
}
}
class MultipleShapes extends BaseGame
with HasCollidables, HasDraggableComponents {
final TextConfig fpsTextConfig = TextConfig(
color: BasicPalette.white.color,
);
@override
Future<void> onLoad() async {
final screen = ScreenCollidable();
final snowman = CollidableSnowman(
Vector2.all(150),
Vector2(100, 200),
Vector2(-100, 100),
);
MyCollidable lastToAdd = snowman;
add(screen);
add(snowman);
var totalAdded = 1;
while (totalAdded < 20) {
lastToAdd = createRandomCollidable(lastToAdd);
final lastBottomRight =
lastToAdd.toAbsoluteRect().bottomRight.toVector2();
if (screen.containsPoint(lastBottomRight)) {
add(lastToAdd);
totalAdded++;
} else {
break;
}
}
}
final _rng = Random();
final _distance = Vector2(100, 0);
MyCollidable createRandomCollidable(MyCollidable lastCollidable) {
final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100;
final isXOverflow = lastCollidable.position.x +
lastCollidable.size.x / 2 +
_distance.x +
collidableSize.x >
size.x;
var position = _distance + Vector2(0, lastCollidable.position.y + 200);
if (!isXOverflow) {
position = (lastCollidable.position + _distance)
..x += collidableSize.x / 2;
}
final velocity = Vector2.random(_rng) * 200;
final rotationSpeed = 0.5 - _rng.nextDouble();
final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];
switch (shapeType) {
case Shapes.circle:
return CollidableCircle(position, collidableSize, velocity);
case Shapes.rectangle:
return CollidableRectangle(position, collidableSize, velocity)
..rotationSpeed = rotationSpeed;
case Shapes.polygon:
return CollidablePolygon(position, collidableSize, velocity)
..rotationSpeed = rotationSpeed;
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
fpsTextConfig.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}

View File

@ -21,6 +21,8 @@
- Add a `renderPoint` method to `Canvas`
- Add zoom to the camera
- Add `moveToTarget` as an extension method to `Vector2`
- Bring back collision detection examples
- Fix collision detection in Collidable with multiple offset shapes
## 1.0.0-rc8
- Migrate to null safety

View File

@ -38,8 +38,8 @@ mixin Hitbox on PositionComponent {
if (!_cachedBoundingRect.isCacheValid([position, size])) {
final maxRadius = size.length;
_cachedBoundingRect.updateCache(
Rect.fromCenter(
center: absoluteCenter.toOffset(),
RectExtension.fromVector2Center(
center: absoluteCenter,
width: maxRadius,
height: maxRadius,
),

View File

@ -37,11 +37,7 @@ extension RectExtension on Rect {
bottomLeft.toVector2(),
];
}
}
// Until [extension] will allow static methods we need to keep these functions
// in a utility class
class RectFactory {
/// Creates bounds in from of a [Rect] from a list of [Vector2]
static Rect fromBounds(List<Vector2> pts) {
final minX = pts.map((e) => e.x).reduce(min);
@ -50,4 +46,19 @@ class RectFactory {
final maxY = pts.map((e) => e.y).reduce(max);
return Rect.fromPoints(Offset(minX, minY), Offset(maxX, maxY));
}
/// Constructs a rectangle from its center point (specified as a Vector2),
/// width and height.
static Rect fromVector2Center({
required Vector2 center,
required double width,
required double height,
}) {
return Rect.fromLTRB(
center.x - width / 2,
center.y - height / 2,
center.x + width / 2,
center.y + height / 2,
);
}
}

View File

@ -56,10 +56,12 @@ mixin HitboxShape on Shape {
@override
double get angle => component.angle;
/// The shapes center, before rotation
/// The shape's absolute center
@override
Vector2 get shapeCenter {
return (component.absoluteCenter + position)
return component.absoluteCenter +
position +
((size / 2)..multiply(relativePosition))
..rotate(angle, center: anchorPosition);
}