ShapeComponent and Hitbox to take transform of parents full ancestor tree into consideration (#1076)

* `ShapeComponent` changes size, position and angle of underlying Shape

* Added description to ShapeComponent

* Fix test

* Update packages/flame/lib/src/components/shape_component.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Add absoluteScale and absoluteAngle to PositionComponent

* Refactor ShapeComponent

* Should be scaled by total scale, not scaled size

* Premature optimization for creation for objects in Polygon

* Use path for default Polygon constructor

* Do not sync component and hitbox shape

* Fix analyze issue

* Add example for flipping with collision detection

* Don't use absoluteScale

* Fix examples

* Fix examples

* Doesn't need super.render

* Fix Circle dartdoc

* Update changelog

* Update names of vertices caches in Polygon

* Update text docs

* Revert "Update text docs"

This reverts commit 73a68a465d76eb0eb50bb3753e57b2f4e3b5a7f4.

* Fix examples

* ShapeComponents docs

* Move example games to the top

* Fix dartdoc comment about polygon vertex relation

* Fix order of polygon vertices in dartdoc

* Fix anchor for PolygonComponent.fromPoints

* Add test with ancestors

* Update doc/components.md

Co-authored-by: Pasha Stetsenko <stpasha@google.com>

* Update doc/components.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Rename example classes

* Fix linting issues in examples

* Don't use px

* Use isTrue and isFalse

* Update doc/components.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Fixed comments on PR

Co-authored-by: Erick <erickzanardoo@gmail.com>
Co-authored-by: Pasha Stetsenko <stpasha@google.com>
This commit is contained in:
Lukas Klingsbo
2021-11-13 16:00:24 +01:00
committed by GitHub
parent d53ac50859
commit cd7a0bbb65
28 changed files with 1224 additions and 390 deletions

View File

@ -7,14 +7,27 @@ import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
const circlesInfo = '''
This example will create a circle every time you tap on the screen. It will have
the initial velocity towards the center of the screen and if it touches another
circle both of them will change color.
''';
class CirclesExample extends FlameGame with HasCollidables, TapDetector {
static const description = '''
This example will create a circle every time you tap on the screen. It will
have the initial velocity towards the center of the screen and if it touches
another circle both of them will change color.
''';
@override
Future<void> onLoad() async {
super.onLoad();
add(ScreenCollidable());
}
@override
void onTapDown(TapDownInfo info) {
add(MyCollidable(info.eventPosition.game));
}
}
class MyCollidable extends PositionComponent
with HasGameRef<Circles>, HasHitboxes, Collidable {
with HasGameRef<CirclesExample>, HasHitboxes, Collidable {
late Vector2 velocity;
final _collisionColor = Colors.amber;
final _defaultColor = Colors.cyan;
@ -63,16 +76,3 @@ class MyCollidable extends PositionComponent
_isCollision = true;
}
}
class Circles extends FlameGame with HasCollidables, TapDetector {
@override
Future<void> onLoad() async {
super.onLoad();
add(ScreenCollidable());
}
@override
void onTapDown(TapDownInfo info) {
add(MyCollidable(info.eventPosition.game));
}
}

View File

@ -0,0 +1,123 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
class CollidableAnimationExample extends FlameGame with HasCollidables {
static const description = '''
In this example you can see four animated birds which are flying straight
along the same route until they hit either another bird or the wall, which
makes them turn. The birds have PolygonHitboxes which are marked with the
green lines and dots.
''';
@override
Future<void> onLoad() async {
await super.onLoad();
add(ScreenCollidable());
// Top left component
add(
AnimatedComponent(Vector2.all(200), Vector2.all(100))..flipVertically(),
);
// Bottom right component
add(
AnimatedComponent(
Vector2(-100, -100),
size.clone()..sub(Vector2.all(200)),
),
);
// Bottom left component
add(
AnimatedComponent(
Vector2(100, -100),
Vector2(100, size.y - 100),
angle: pi / 4,
),
);
// Top right component
add(
AnimatedComponent(
Vector2(-300, 300),
Vector2(size.x - 100, 100),
angle: pi / 4,
)..flipVertically(),
);
}
}
class AnimatedComponent extends SpriteAnimationComponent
with HasHitboxes, Collidable, HasGameRef {
final Vector2 velocity;
final List<Collidable> activeCollisions = [];
AnimatedComponent(this.velocity, Vector2 position, {double angle = -pi / 4})
: super(
position: position,
size: Vector2(150, 100),
angle: angle,
anchor: Anchor.center,
);
late HitboxPolygon hitbox;
@override
Future<void> onLoad() async {
await super.onLoad();
animation = await gameRef.loadSpriteAnimation(
'bomb_ptero.png',
SpriteAnimationData.sequenced(
amount: 4,
stepTime: 0.2,
textureSize: Vector2.all(48),
),
);
hitbox = HitboxPolygon([
Vector2(0.0, -1.0),
Vector2(-1.0, -0.1),
Vector2(-0.2, 0.4),
Vector2(0.2, 0.4),
Vector2(1.0, -0.1),
]);
addHitbox(hitbox);
}
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
final Paint hitboxPaint = BasicPalette.green.paint()
..style = PaintingStyle.stroke;
final Paint dotPaint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
@override
void render(Canvas canvas) {
super.render(canvas);
// This is just to clearly see the vertices in the hitboxes
hitbox.render(canvas, hitboxPaint);
hitbox
.localVertices()
.forEach((p) => canvas.drawCircle(p.toOffset(), 4, dotPaint));
}
@override
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
if (!activeCollisions.contains(other)) {
velocity.negate();
flipVertically();
activeCollisions.add(other);
}
}
@override
void onCollisionEnd(Collidable other) {
activeCollisions.remove(other);
}
}

View File

@ -3,27 +3,34 @@ import 'package:flame/game.dart';
import '../../commons/commons.dart';
import 'circles.dart';
import 'collidable_animation.dart';
import 'multiple_shapes.dart';
import 'only_shapes.dart';
import 'simple_shapes.dart';
void addCollisionDetectionStories(Dashbook dashbook) {
dashbook.storiesOf('Collision Detection')
..add(
'Collidable AnimationComponent',
(_) => GameWidget(game: CollidableAnimationExample()),
codeLink: baseLink('collision_detection/collidable_animation.dart'),
info: CollidableAnimationExample.description,
)
..add(
'Circles',
(_) => GameWidget(game: Circles()),
(_) => GameWidget(game: CirclesExample()),
codeLink: baseLink('collision_detection/circles.dart'),
info: circlesInfo,
info: CirclesExample.description,
)
..add(
'Multiple shapes',
(_) => GameWidget(game: MultipleShapes()),
(_) => GameWidget(game: MultipleShapesExample()),
codeLink: baseLink('collision_detection/multiple_shapes.dart'),
info: multipleShapesInfo,
info: MultipleShapesExample.description,
)
..add(
'Simple Shapes',
(_) => GameWidget(game: OnlyShapes()),
codeLink: baseLink('collision_detection/only_shapes.dart'),
info: onlyShapesInfo,
(_) => GameWidget(game: SimpleShapesExample()),
codeLink: baseLink('collision_detection/simple_shapes.dart'),
info: SimpleShapesExample.description,
);
}

View File

@ -9,21 +9,95 @@ import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
const multipleShapesInfo = '''
An example with many hitboxes that move around on the screen and during
collisions they change color depending on what it is that they have collided
with.
The snowman, the component built with three circles on top of each other, works
a little bit differently than the other components to show that you can have
multiple hitboxes within one component.
On this example, you can "throw" the components by dragging them quickly in any
direction.
''';
enum Shapes { circle, rectangle, polygon }
class MultipleShapesExample extends FlameGame
with HasCollidables, HasDraggableComponents, FPSCounter {
static const description = '''
An example with many hitboxes that move around on the screen and during
collisions they change color depending on what it is that they have collided
with.
The snowman, the component built with three circles on top of each other,
works a little bit differently than the other components to show that you
can have multiple hitboxes within one component.
On this example, you can "throw" the components by dragging them quickly in
any direction.
''';
final TextPaint fpsTextPaint = TextPaint(
config: TextPaintConfig(
color: BasicPalette.white.color,
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final screenCollidable = ScreenCollidable();
final snowman = CollidableSnowman(
Vector2.all(150),
Vector2(100, 200),
Vector2(-100, 100),
screenCollidable,
);
MyCollidable lastToAdd = snowman;
add(screenCollidable);
add(snowman);
var totalAdded = 1;
while (totalAdded < 20) {
lastToAdd = nextRandomCollidable(lastToAdd, screenCollidable);
final lastBottomRight =
lastToAdd.toAbsoluteRect().bottomRight.toVector2();
if (lastBottomRight.x < size.x && lastBottomRight.y < size.y) {
add(lastToAdd);
totalAdded++;
} else {
break;
}
}
}
final _rng = Random();
final _distance = Vector2(100, 0);
MyCollidable nextRandomCollidable(
MyCollidable lastCollidable,
ScreenCollidable screenCollidable,
) {
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) - Vector2.random(_rng)) * 400;
return randomCollidable(
position,
collidableSize,
velocity,
screenCollidable,
rng: _rng,
);
}
@override
void render(Canvas canvas) {
super.render(canvas);
fpsTextPaint.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}
abstract class MyCollidable extends PositionComponent
with Draggable, HasHitboxes, Collidable {
double rotationSpeed = 0.0;
@ -41,11 +115,7 @@ abstract class MyCollidable extends PositionComponent
Vector2 size,
this.velocity,
this.screenCollidable,
) {
this.position = position;
this.size = size;
anchor = Anchor.center;
}
) : super(position: position, size: size, anchor: Anchor.center);
@override
Future<void> onLoad() async {
@ -137,7 +207,7 @@ class CollidablePolygon extends MyCollidable {
Vector2 velocity,
ScreenCollidable screenCollidable,
) : super(position, size, velocity, screenCollidable) {
final shape = HitboxPolygon([
final hitbox = HitboxPolygon([
Vector2(-1.0, 0.0),
Vector2(-0.8, 0.6),
Vector2(0.0, 1.0),
@ -147,7 +217,7 @@ class CollidablePolygon extends MyCollidable {
Vector2(0, -1.0),
Vector2(-0.8, -0.8),
]);
addHitbox(shape);
addHitbox(hitbox);
}
}
@ -169,8 +239,7 @@ class CollidableCircle extends MyCollidable {
Vector2 velocity,
ScreenCollidable screenCollidable,
) : super(position, size, velocity, screenCollidable) {
final shape = HitboxCircle();
addHitbox(shape);
addHitbox(HitboxCircle());
}
}
@ -179,7 +248,7 @@ class SnowmanPart extends HitboxCircle {
final hitPaint = Paint();
SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor)
: super(definition: definition) {
: super(normalizedRadius: definition) {
this.relativeOffset.setFrom(relativeOffset);
hitPaint..color = startColor;
onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) {
@ -245,77 +314,3 @@ MyCollidable randomCollidable(
..rotationSpeed = rotationSpeed;
}
}
class MultipleShapes extends FlameGame
with HasCollidables, HasDraggableComponents, FPSCounter {
final TextPaint fpsTextPaint = TextPaint(
config: TextPaintConfig(
color: BasicPalette.white.color,
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final screenCollidable = ScreenCollidable();
final snowman = CollidableSnowman(
Vector2.all(150),
Vector2(100, 200),
Vector2(-100, 100),
screenCollidable,
);
MyCollidable lastToAdd = snowman;
add(screenCollidable);
add(snowman);
var totalAdded = 1;
while (totalAdded < 20) {
lastToAdd = nextRandomCollidable(lastToAdd, screenCollidable);
final lastBottomRight =
lastToAdd.toAbsoluteRect().bottomRight.toVector2();
if (lastBottomRight.x < size.x && lastBottomRight.y < size.y) {
add(lastToAdd);
totalAdded++;
} else {
break;
}
}
}
final _rng = Random();
final _distance = Vector2(100, 0);
MyCollidable nextRandomCollidable(
MyCollidable lastCollidable,
ScreenCollidable screenCollidable,
) {
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) - Vector2.random(_rng)) * 400;
return randomCollidable(
position,
collidableSize,
velocity,
screenCollidable,
rng: _rng,
);
}
@override
void render(Canvas canvas) {
super.render(canvas);
fpsTextPaint.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}

View File

@ -1,70 +0,0 @@
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/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
const onlyShapesInfo = '''
An example which adds random shapes on the screen when you tap it, if you tap on
an already existing shape it will remove that shape and replace it with a new
one.
''';
enum Shapes { circle, rectangle, polygon }
class OnlyShapes extends FlameGame with HasTappableComponents {
final shapePaint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
final _rng = Random();
Shape randomShape(Vector2 position) {
final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];
const size = 50.0;
switch (shapeType) {
case Shapes.circle:
return Circle(radius: size / 2, position: position);
case Shapes.rectangle:
return Rectangle(
position: position,
size: Vector2.all(size),
angle: _rng.nextDouble() * 6,
);
case Shapes.polygon:
final points = [
Vector2.random(_rng),
Vector2.random(_rng)..y *= -1,
-Vector2.random(_rng),
Vector2.random(_rng)..x *= -1,
];
return Polygon.fromDefinition(
points,
position: position,
size: Vector2.all(size),
angle: _rng.nextDouble() * 6,
);
}
}
@override
void onTapDown(int pointerId, TapDownInfo info) {
super.onTapDown(pointerId, info);
final tapDownPoint = info.eventPosition.game;
final component = MyShapeComponent(randomShape(tapDownPoint), shapePaint);
add(component);
}
}
class MyShapeComponent extends ShapeComponent with Tappable {
MyShapeComponent(Shape shape, Paint shapePaint)
: super(shape, paint: shapePaint);
@override
bool onTapDown(TapDownInfo info) {
removeFromParent();
return true;
}
}

View File

@ -0,0 +1,105 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
enum Shapes { circle, rectangle, polygon }
class SimpleShapesExample extends FlameGame with HasTappableComponents {
static const description = '''
An example which adds random shapes on the screen when you tap it, if you
tap on an already existing shape it will remove that shape and replace it
with a new one.
''';
final _rng = Random();
MyShapeComponent randomShape(Vector2 position) {
final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];
final shapeSize =
Vector2.all(100) + Vector2.all(50.0).scaled(_rng.nextDouble());
final shapeAngle = _rng.nextDouble() * 6;
switch (shapeType) {
case Shapes.circle:
return MyShapeComponent(
HitboxCircle(),
position: position,
size: shapeSize,
angle: shapeAngle,
);
case Shapes.rectangle:
return MyShapeComponent(
HitboxRectangle(),
position: position,
size: shapeSize,
angle: shapeAngle,
);
case Shapes.polygon:
final points = [
Vector2.random(_rng),
Vector2.random(_rng)..y *= -1,
-Vector2.random(_rng),
Vector2.random(_rng)..x *= -1,
];
return MyShapeComponent(
HitboxPolygon(points),
position: position,
size: shapeSize,
angle: shapeAngle,
);
}
}
@override
void onTapDown(int pointerId, TapDownInfo info) {
super.onTapDown(pointerId, info);
final tapPosition = info.eventPosition.game;
final component = randomShape(tapPosition);
add(component);
component.add(
MoveEffect(
path: [size / 2],
speed: 30,
isAlternating: true,
isInfinite: true,
),
);
component.add(
RotateEffect(
angle: 3,
speed: 0.4,
isAlternating: true,
isInfinite: true,
),
);
}
}
class MyShapeComponent extends ShapeComponent with Tappable {
@override
final Paint paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
MyShapeComponent(
HitboxShape shape, {
Vector2? position,
Vector2? size,
double? angle,
}) : super(
shape,
position: position,
size: size,
angle: angle,
anchor: Anchor.center,
);
@override
bool onTapDown(TapDownInfo _) {
removeFromParent();
return true;
}
}