mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 10:38:17 +08:00
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:
@ -30,6 +30,9 @@ that overshoot each other into account, this could happen when they either move
|
||||
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.
|
||||
|
||||
Also note that the collision detection system doesn't work properly if you scale ancestors of the
|
||||
component that is `Collidable`.
|
||||
|
||||
## Mixins
|
||||
### HasHitboxes
|
||||
The `HasHitboxes` mixin is mainly used for two things; to make detection of collisions with other
|
||||
|
||||
@ -422,6 +422,100 @@ It is also possible to create custom renderers by extending the `ParallaxRendere
|
||||
Three example implementations can be found in the
|
||||
[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax).
|
||||
|
||||
## ShapeComponents
|
||||
The `ShapeComponent` is a basic component that can be used if you want to draw geometrical shapes as
|
||||
components on the screen. Since the `ShapeComponent` is a `PositionComponent`s you can use effects
|
||||
on it. All `ShapeComponent`s take a `Paint` as an argument and then arguments to define
|
||||
the shape of the specific component, it also takes all the arguments that can be passed to the
|
||||
`PositionComponent`.
|
||||
|
||||
There are three implementations of `ShapeComponent`, which are the following:
|
||||
|
||||
### CircleComponent
|
||||
A `CircleComponent` can be created only by defining its `radius`, but you most likely want to pass it
|
||||
a `position` and maybe `paint` (the default is white) too.
|
||||
|
||||
Example:
|
||||
```dart
|
||||
final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
|
||||
final circle = CircleComponent(radius: 200.0, position: Vector2(100, 200), paint: paint);
|
||||
```
|
||||
|
||||
### RectangleComponent
|
||||
A `RectangleComponent` can be created in two ways, depending on if it's a square or not.
|
||||
To create a `RectangleComponent` that is 300 in width and 200 in height you can do the following:
|
||||
|
||||
Example:
|
||||
```dart
|
||||
final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
|
||||
final rectangle = RectangleComponent(
|
||||
size: Vector2(300.0, 200.0),
|
||||
position: Vector2(100, 200),
|
||||
paint: paint,
|
||||
);
|
||||
```
|
||||
|
||||
To create a square you can instead use the slightly simpler named constructor
|
||||
`RectangleComponent.square`. This is an example of how to create a red square with width and height
|
||||
200:
|
||||
|
||||
```dart
|
||||
final paint = BasicPalette.red.paint()..style = PaintingStyle.stroke;
|
||||
final square = RectangleComponent.square(
|
||||
size: 200.0,
|
||||
position: Vector2(100, 200),
|
||||
paint: paint,
|
||||
);
|
||||
```
|
||||
|
||||
### PolygonComponent
|
||||
The `PolygonComponent` is the most complicated of the `ShapeComponent`s since you'll have to define
|
||||
all the "corners" of your polygon. You can create the `PolygonComponent` in two different ways,
|
||||
either you use the default constructor which takes a list of `Vector2` where each of them should be
|
||||
between -1.0 and 1.0 that describes the ration of the length from the center to the edge of the size
|
||||
of the component. So
|
||||
`[Vector2(1.0, 1.0), Vector2(1.0, -1.0), Vector2(-1.0, -1.0), Vector2(-1.0, 1.0)]`
|
||||
would describe a rectangle that fills the full size of the component. Remember to define the list in
|
||||
a counter clockwise manner (if you think in the screen coordinate system where the y-axis is
|
||||
flipped, otherwise it is clockwise).
|
||||
|
||||
So to create a diamond shaped `PolygonComponent` which is slightly smaller than the defined size you
|
||||
would do this:
|
||||
|
||||
```dart
|
||||
final vertices = ([
|
||||
Vector2(0.0, 0.9), // Middle of top wall
|
||||
Vector2(-0.9, 0.0), // Middle of left wall
|
||||
Vector2(0.0, -0.9), // Middle of bottom wall
|
||||
Vector2(0.9, 0.0), // Middle of right wall
|
||||
]);
|
||||
|
||||
final diamond = PolygonComponent(
|
||||
normalizedVertices: vertices,
|
||||
size: Vector2(200, 300),
|
||||
position: Vector2.all(500),
|
||||
)
|
||||
```
|
||||
|
||||
If you instead want to define your polygon from absolute points you can do that too with the
|
||||
`PolygonComponent.fromPoints` factory. When using that one you don't have to define a `size` or a
|
||||
`position` either since it will be calculated for you, but if you decide to add those arguments
|
||||
anyways they will override what has been calculated from your list of vertices.
|
||||
|
||||
Example (diamond shape again):
|
||||
|
||||
```dart
|
||||
final vertices = ([
|
||||
Vector2(100, 100), // Middle of top wall
|
||||
Vector2(50, 150), // Middle of left wall
|
||||
Vector2(100, 200), // Middle of bottom wall
|
||||
Vector2(200, 150), // Middle of right wall
|
||||
]);
|
||||
|
||||
final diamond = PolygonComponent.fromPoints(vertices);
|
||||
)
|
||||
```
|
||||
|
||||
## SpriteBodyComponent
|
||||
|
||||
See [SpriteBodyComponent](forge2d.md#SpriteBodyComponent) in the Forge2D documentation.
|
||||
|
||||
@ -10,8 +10,8 @@ class SquareComponent extends RectangleComponent with EffectsHelper {
|
||||
Paint? paint,
|
||||
int priority = 0,
|
||||
}) : super(
|
||||
Vector2.all(size),
|
||||
position: position,
|
||||
size: Vector2.all(size),
|
||||
paint: paint,
|
||||
priority: priority,
|
||||
);
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -12,11 +11,7 @@ import '../../commons/square_component.dart';
|
||||
final R = Random();
|
||||
|
||||
class MovableSquare extends SquareComponent
|
||||
with
|
||||
HasHitboxes,
|
||||
Collidable,
|
||||
HasGameRef<CameraAndViewportGame>,
|
||||
KeyboardHandler {
|
||||
with Collidable, HasGameRef<CameraAndViewportGame>, KeyboardHandler {
|
||||
static const double speed = 300;
|
||||
static final TextPaint textRenderer = TextPaint(
|
||||
config: const TextPaintConfig(
|
||||
@ -27,8 +22,11 @@ class MovableSquare extends SquareComponent
|
||||
final Vector2 velocity = Vector2.zero();
|
||||
late Timer timer;
|
||||
|
||||
MovableSquare() : super(priority: 1) {
|
||||
addHitbox(HitboxRectangle());
|
||||
MovableSquare() : super(priority: 1);
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
timer = Timer(3.0)
|
||||
..stop()
|
||||
..callback = () {
|
||||
@ -104,7 +102,7 @@ class Map extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
class Rock extends SquareComponent with HasHitboxes, Collidable, Tappable {
|
||||
class Rock extends SquareComponent with Collidable, Tappable {
|
||||
static final unpressedPaint = Paint()..color = const Color(0xFF2222FF);
|
||||
static final pressedPaint = Paint()..color = const Color(0xFF414175);
|
||||
|
||||
@ -114,9 +112,7 @@ class Rock extends SquareComponent with HasHitboxes, Collidable, Tappable {
|
||||
size: 50,
|
||||
priority: 2,
|
||||
paint: unpressedPaint,
|
||||
) {
|
||||
addHitbox(HitboxRectangle());
|
||||
}
|
||||
);
|
||||
|
||||
@override
|
||||
bool onTapDown(_) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
105
examples/lib/stories/collision_detection/simple_shapes.dart
Normal file
105
examples/lib/stories/collision_detection/simple_shapes.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,8 @@ class MoveEffectGame extends FlameGame with TapDetector {
|
||||
await super.onLoad();
|
||||
square = SquareComponent(size: 50, position: Vector2(200, 150));
|
||||
add(square);
|
||||
final pathMarkers = path.map((p) => CircleComponent(3, position: p));
|
||||
final pathMarkers =
|
||||
path.map((p) => CircleComponent(radius: 3, position: p));
|
||||
addAll(pathMarkers);
|
||||
}
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ class MoveEffectExample extends FlameGame {
|
||||
}
|
||||
for (var i = 0; i < 40; i++) {
|
||||
add(
|
||||
CircleComponent(5)
|
||||
CircleComponent(radius: 5)
|
||||
..add(
|
||||
MoveEffect.along(
|
||||
path1,
|
||||
|
||||
@ -25,7 +25,7 @@ class RemoveEffectExample extends FlameGame with HasTappableComponents {
|
||||
|
||||
class _RandomCircle extends CircleComponent with Tappable {
|
||||
_RandomCircle(double radius, {Vector2? position, Paint? paint})
|
||||
: super(radius, position: position, paint: paint);
|
||||
: super(radius: radius, position: position, paint: paint);
|
||||
|
||||
factory _RandomCircle.random(Random rng) {
|
||||
final radius = rng.nextDouble() * 30 + 10;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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/painting.dart';
|
||||
@ -17,8 +16,8 @@ class JoystickGame extends FlameGame with HasDraggableComponents {
|
||||
final knobPaint = BasicPalette.blue.withAlpha(200).paint();
|
||||
final backgroundPaint = BasicPalette.blue.withAlpha(100).paint();
|
||||
joystick = JoystickComponent(
|
||||
knob: Circle(radius: 30).toComponent(paint: knobPaint),
|
||||
background: Circle(radius: 100).toComponent(paint: backgroundPaint),
|
||||
knob: CircleComponent(radius: 30, paint: knobPaint),
|
||||
background: CircleComponent(radius: 100, paint: backgroundPaint),
|
||||
margin: const EdgeInsets.only(left: 40, bottom: 40),
|
||||
);
|
||||
player = JoystickPlayer(joystick);
|
||||
|
||||
@ -4,7 +4,6 @@ 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';
|
||||
import 'package:flame/sprite.dart';
|
||||
@ -89,9 +88,11 @@ class JoystickAdvancedGame extends FlameGame
|
||||
// A button, created from a shape, that adds a rotation effect to the player
|
||||
// when it is pressed.
|
||||
final shapeButton = HudButtonComponent(
|
||||
button: Circle(radius: 35).toComponent(paint: BasicPalette.white.paint()),
|
||||
buttonDown: Rectangle(size: buttonSize)
|
||||
.toComponent(paint: BasicPalette.blue.paint()),
|
||||
button: CircleComponent(radius: 35),
|
||||
buttonDown: RectangleComponent(
|
||||
size: buttonSize,
|
||||
paint: BasicPalette.blue.paint(),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 85,
|
||||
bottom: 150,
|
||||
|
||||
@ -22,6 +22,8 @@
|
||||
- Components that manipulate canvas state are now responsible for saving/restoring that state
|
||||
- Remove `super.render` calls that are no longer needed
|
||||
- Fixed typo in error message
|
||||
- Underlying `Shape`s in `ShapeComponent` transform with components position, size and angle
|
||||
- `HitboxShape` takes parents ancestors transformations into consideration (not scaling)
|
||||
|
||||
## [1.0.0-releasecandidate.16]
|
||||
- `changePriority` no longer breaks game loop iteration
|
||||
|
||||
@ -176,6 +176,20 @@ class Component with Loadable {
|
||||
nextParent = component;
|
||||
}
|
||||
|
||||
final List<Component> _ancestors = [];
|
||||
|
||||
/// A list containing the current parent and its parent, and so on, until it
|
||||
/// reaches a component without a parent.
|
||||
List<Component> ancestors() {
|
||||
_ancestors.clear();
|
||||
for (var currentParent = parent;
|
||||
currentParent != null;
|
||||
currentParent = currentParent.parent) {
|
||||
_ancestors.add(currentParent);
|
||||
}
|
||||
return _ancestors;
|
||||
}
|
||||
|
||||
/// It receives the new game size.
|
||||
/// Executed right after the component is attached to a game and right before
|
||||
/// [onLoad] is called.
|
||||
@ -321,7 +335,7 @@ class Component with Loadable {
|
||||
parentGame.hasLayout,
|
||||
'"prepare/add" called before the game is ready. '
|
||||
'Did you try to access it on the Game constructor? '
|
||||
'Use the "onLoad" or "onParentMethod" method instead.',
|
||||
'Use the "onLoad" or "onMount" method instead.',
|
||||
);
|
||||
if (parentGame is FlameGame) {
|
||||
parentGame.prepareComponent(this);
|
||||
|
||||
@ -163,8 +163,17 @@ class PositionComponent extends Component {
|
||||
/// component as seen from the parent's perspective, and it is equal to
|
||||
/// [size] * [scale]. This is a computed property and cannot be
|
||||
/// modified by the user.
|
||||
Vector2 get scaledSize =>
|
||||
Vector2(width * scale.x.abs(), height * scale.y.abs());
|
||||
Vector2 get scaledSize {
|
||||
return Vector2(width * scale.x.abs(), height * scale.y.abs());
|
||||
}
|
||||
|
||||
/// The resulting angle after all the ancestors and the components own angle
|
||||
/// has been applied.
|
||||
double get absoluteAngle {
|
||||
return ancestors()
|
||||
.whereType<PositionComponent>()
|
||||
.fold<double>(angle, (totalAngle, c) => totalAngle + c.angle);
|
||||
}
|
||||
|
||||
/// Measure the distance (in parent's coordinate space) between this
|
||||
/// component's anchor and the [other] component's anchor.
|
||||
|
||||
@ -8,96 +8,155 @@ import '../../palette.dart';
|
||||
import '../anchor.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
|
||||
class ShapeComponent extends PositionComponent {
|
||||
final Shape shape;
|
||||
/// A [ShapeComponent] is a [Shape] wrapped in a [PositionComponent] so that it
|
||||
/// can be added to a component tree and take the camera and viewport into
|
||||
/// consideration when rendering.
|
||||
class ShapeComponent extends PositionComponent with HasHitboxes {
|
||||
final HitboxShape shape;
|
||||
Paint paint;
|
||||
|
||||
/// Currently the [anchor] can only be center for [ShapeComponent], since
|
||||
/// shape doesn't take any anchor into consideration.
|
||||
@override
|
||||
final Anchor anchor = Anchor.center;
|
||||
|
||||
ShapeComponent(
|
||||
this.shape, {
|
||||
Paint? paint,
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) : paint = paint ?? BasicPalette.white.paint(),
|
||||
super(
|
||||
position: shape.position,
|
||||
size: shape.size,
|
||||
angle: shape.angle,
|
||||
anchor: Anchor.center,
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
) {
|
||||
shape.isCanvasPrepared = true;
|
||||
addHitbox(shape);
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
shape.render(canvas, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool containsPoint(Vector2 point) => shape.containsPoint(point);
|
||||
}
|
||||
|
||||
class CircleComponent extends ShapeComponent {
|
||||
CircleComponent(
|
||||
double radius, {
|
||||
Vector2? position,
|
||||
CircleComponent({
|
||||
required double radius,
|
||||
Paint? paint,
|
||||
Vector2? position,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) : super(
|
||||
Circle(radius: radius, position: position),
|
||||
HitboxCircle(),
|
||||
paint: paint,
|
||||
position: position,
|
||||
size: Vector2.all(radius * 2),
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
class RectangleComponent extends ShapeComponent {
|
||||
RectangleComponent(
|
||||
Vector2 size, {
|
||||
Vector2? position,
|
||||
RectangleComponent({
|
||||
required Vector2 size,
|
||||
Paint? paint,
|
||||
Vector2? position,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) : super(
|
||||
Rectangle(size: size, position: position),
|
||||
HitboxRectangle(),
|
||||
paint: paint,
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
);
|
||||
|
||||
RectangleComponent.square(
|
||||
double size, {
|
||||
Vector2? position,
|
||||
RectangleComponent.square({
|
||||
required double size,
|
||||
Paint? paint,
|
||||
Vector2? position,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) : super(
|
||||
Rectangle(size: Vector2.all(size), position: position),
|
||||
HitboxRectangle(),
|
||||
paint: paint,
|
||||
position: position,
|
||||
size: Vector2.all(size),
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
|
||||
class PolygonComponent extends ShapeComponent {
|
||||
PolygonComponent(
|
||||
List<Vector2> points, {
|
||||
/// The [normalizedVertices] should be a list of points that range between
|
||||
/// [-1.0, 1.0] which defines the relation of the vertices in the polygon
|
||||
/// from the center of the component to the size of the component.
|
||||
PolygonComponent({
|
||||
required List<Vector2> normalizedVertices,
|
||||
Paint? paint,
|
||||
int? priority,
|
||||
}) : super(Polygon(points), paint: paint, priority: priority);
|
||||
|
||||
PolygonComponent.fromDefinition(
|
||||
List<Vector2> normalizedVertices, {
|
||||
Vector2? size,
|
||||
Vector2? position,
|
||||
Paint? paint,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) : super(
|
||||
Polygon.fromDefinition(
|
||||
normalizedVertices,
|
||||
HitboxPolygon(normalizedVertices),
|
||||
paint: paint,
|
||||
position: position,
|
||||
size: size,
|
||||
),
|
||||
paint: paint,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
);
|
||||
|
||||
/// Instead of using vertices that are in relation to the size of the
|
||||
/// component you can use this factory with absolute points which will set the
|
||||
/// position and size of the component and calculate the normalized vertices.
|
||||
factory PolygonComponent.fromPoints(
|
||||
List<Vector2> points, {
|
||||
Paint? paint,
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double? angle,
|
||||
Anchor? anchor,
|
||||
int? priority,
|
||||
}) {
|
||||
final polygon = Polygon(points);
|
||||
final anchorPosition = position ??
|
||||
Anchor.center.toOtherAnchorPosition(
|
||||
polygon.position,
|
||||
anchor ?? Anchor.topLeft,
|
||||
size ?? polygon.size,
|
||||
);
|
||||
return PolygonComponent(
|
||||
normalizedVertices: polygon.normalizedVertices,
|
||||
paint: paint,
|
||||
position: anchorPosition,
|
||||
size: size ?? polygon.size,
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
priority: priority,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,9 @@ extension Vector2Extension on Vector2 {
|
||||
/// Whether the [Vector2] is the zero vector or not
|
||||
bool isZero() => x == 0 && y == 0;
|
||||
|
||||
/// Whether the [Vector2] is the identity vector or not
|
||||
bool isIdentity() => x == 1 && y == 1;
|
||||
|
||||
/// Rotates the [Vector2] with [angle] in radians
|
||||
/// rotates around [center] if it is defined
|
||||
/// In a screen coordinate system (where the y-axis is flipped) it rotates in
|
||||
@ -99,7 +102,7 @@ extension Vector2Extension on Vector2 {
|
||||
///
|
||||
/// Since on a canvas/screen y is smaller the further up you go, instead of
|
||||
/// larger like on a normal coordinate system, to get an angle that is in that
|
||||
/// coordinate system we have to flip the Y-axis of the [Vector].
|
||||
/// coordinate system we have to flip the Y-axis of the [Vector2].
|
||||
///
|
||||
/// Example:
|
||||
/// Up: Vector(0.0, -1.0).screenAngle == 0
|
||||
@ -119,4 +122,7 @@ extension Vector2Extension on Vector2 {
|
||||
|
||||
/// Creates a heading [Vector2] with the given angle in degrees.
|
||||
static Vector2 fromDegrees(double d) => fromRadians(d * degrees2Radians);
|
||||
|
||||
/// Creates a new identity [Vector2] (1.0, 1.0).
|
||||
static Vector2 identity() => Vector2.all(1.0);
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ import '../extensions/vector2.dart';
|
||||
import 'shape.dart';
|
||||
|
||||
class Circle extends Shape {
|
||||
/// The [normalizedRadius] is how many percentages of the shortest edge of
|
||||
/// [size] that the circle should cover.
|
||||
double normalizedRadius = 1;
|
||||
/// The [normalizedRadius] is what ratio (0.0, 1.0] of the shortest edge of
|
||||
/// [size]/2 that the circle should cover.
|
||||
double normalizedRadius = 1.0;
|
||||
|
||||
/// With this constructor you can create your [Circle] from a radius and
|
||||
/// a position. It will also calculate the bounding rectangle [size] for the
|
||||
@ -25,16 +25,26 @@ class Circle extends Shape {
|
||||
);
|
||||
|
||||
/// This constructor is used by [HitboxCircle]
|
||||
/// definition is the percentages of the shortest edge of [size] that the
|
||||
/// circle should fill.
|
||||
/// [relation] is the relation [0.0, 1.0] of the shortest edge of [size] that
|
||||
/// the circle should fill.
|
||||
Circle.fromDefinition({
|
||||
this.normalizedRadius = 1.0,
|
||||
double? relation,
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
double? angle,
|
||||
}) : super(position: position, size: size, angle: angle ?? 0);
|
||||
}) : normalizedRadius = relation ?? 1.0,
|
||||
super(position: position, size: size, angle: angle ?? 0);
|
||||
|
||||
double get radius => (min(size.x, size.y) / 2) * normalizedRadius;
|
||||
// Used to not create new Vector2 objects every time radius is called.
|
||||
final Vector2 _scaledSize = Vector2.zero();
|
||||
|
||||
/// Get the radius of the circle after it has been sized and scaled.
|
||||
double get radius {
|
||||
_scaledSize
|
||||
..setFrom(size)
|
||||
..multiply(scale);
|
||||
return (min(_scaledSize.x, _scaledSize.y) / 2) * normalizedRadius;
|
||||
}
|
||||
|
||||
/// This render method doesn't rotate the canvas according to angle since a
|
||||
/// circle will look the same rotated as not rotated.
|
||||
@ -100,9 +110,15 @@ class Circle extends Shape {
|
||||
}
|
||||
|
||||
class HitboxCircle extends Circle with HitboxShape {
|
||||
@override
|
||||
HitboxCircle({double definition = 1})
|
||||
: super.fromDefinition(
|
||||
normalizedRadius: definition,
|
||||
HitboxCircle({
|
||||
double? normalizedRadius,
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
double? angle,
|
||||
}) : super.fromDefinition(
|
||||
relation: normalizedRadius,
|
||||
position: position,
|
||||
size: size,
|
||||
angle: angle ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ class LineSegment {
|
||||
|
||||
LineSegment(this.from, this.to);
|
||||
|
||||
factory LineSegment.zero() => LineSegment(Vector2.zero(), Vector2.zero());
|
||||
|
||||
/// Returns an empty list if there are no intersections between the segments
|
||||
/// If the segments are concurrent, the intersecting point is returned as a
|
||||
/// list with a single point
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui' hide Canvas;
|
||||
|
||||
import '../../game.dart';
|
||||
import '../../geometry.dart';
|
||||
import '../components/cache/value_cache.dart';
|
||||
import '../extensions/canvas.dart';
|
||||
import '../extensions/offset.dart';
|
||||
import '../extensions/rect.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import 'shape.dart';
|
||||
|
||||
class Polygon extends Shape {
|
||||
final List<Vector2> normalizedVertices;
|
||||
// These lists are used to minimize the amount of [Vector2] objects that are
|
||||
// created, only change them if the cache is deemed invalid
|
||||
late final List<Vector2> _sizedVertices;
|
||||
late final List<Vector2> _hitboxVertices;
|
||||
// These lists are used to minimize the amount of objects that are created,
|
||||
// and only change the contained object if the corresponding `ValueCache` is
|
||||
// deemed outdated.
|
||||
late final List<Vector2> _localVertices;
|
||||
late final List<Vector2> _globalVertices;
|
||||
late final List<Offset> _renderVertices;
|
||||
late final List<LineSegment> _lineSegments;
|
||||
final _path = Path();
|
||||
|
||||
final _cachedLocalVertices = ValueCache<Iterable<Vector2>>();
|
||||
final _cachedGlobalVertices = ValueCache<List<Vector2>>();
|
||||
final _cachedRenderPath = ValueCache<Path>();
|
||||
|
||||
/// With this constructor you create your [Polygon] from positions in your
|
||||
/// intended space. It will automatically calculate the [size] and center
|
||||
@ -23,21 +31,19 @@ class Polygon extends Shape {
|
||||
List<Vector2> points, {
|
||||
double angle = 0,
|
||||
}) {
|
||||
final center = points.fold<Vector2>(
|
||||
Vector2.zero(),
|
||||
(sum, v) => sum + v,
|
||||
) /
|
||||
points.length.toDouble();
|
||||
final bottomRight = points.fold<Vector2>(
|
||||
Vector2.zero(),
|
||||
(bottomRight, v) {
|
||||
return Vector2(
|
||||
max(bottomRight.x, v.x),
|
||||
max(bottomRight.y, v.y),
|
||||
assert(
|
||||
points.length > 2,
|
||||
'List of points is too short to create a polygon',
|
||||
);
|
||||
},
|
||||
final path = Path()
|
||||
..addPolygon(
|
||||
points.map((p) => p.toOffset()).toList(growable: false),
|
||||
true,
|
||||
);
|
||||
final halfSize = bottomRight - center;
|
||||
final boundingRect = path.getBounds();
|
||||
final centerOffset = boundingRect.center;
|
||||
final center = centerOffset.toVector2();
|
||||
final halfSize = (boundingRect.bottomRight - centerOffset).toVector2();
|
||||
final definition =
|
||||
points.map<Vector2>((v) => (v - center)..divide(halfSize)).toList();
|
||||
return Polygon.fromDefinition(
|
||||
@ -50,9 +56,10 @@ class Polygon extends Shape {
|
||||
|
||||
/// With this constructor you define the [Polygon] from the center of and with
|
||||
/// percentages of the size of the shape.
|
||||
/// Example: [[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]]
|
||||
/// Example: [[1.0, 0.0], [0.0, -1.0], [-1.0, 0.0], [0.0, 1.0]]
|
||||
/// This will form a diamond shape within the bounding size box.
|
||||
/// NOTE: Always define your shape in a clockwise fashion
|
||||
/// NOTE: Always define your shape in a counter-clockwise fashion (in the
|
||||
/// screen coordinate system).
|
||||
Polygon.fromDefinition(
|
||||
this.normalizedVertices, {
|
||||
Vector2? position,
|
||||
@ -63,51 +70,113 @@ class Polygon extends Shape {
|
||||
size: size,
|
||||
angle: angle ?? 0,
|
||||
) {
|
||||
_sizedVertices =
|
||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
||||
_hitboxVertices =
|
||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
||||
List<Vector2> generateList() {
|
||||
return List.generate(
|
||||
normalizedVertices.length,
|
||||
(_) => Vector2.zero(),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
|
||||
final _cachedScaledShape = ValueCache<Iterable<Vector2>>();
|
||||
_localVertices = generateList();
|
||||
_globalVertices = generateList();
|
||||
_renderVertices = List.filled(
|
||||
normalizedVertices.length,
|
||||
Offset.zero,
|
||||
growable: false,
|
||||
);
|
||||
_lineSegments = List.generate(
|
||||
normalizedVertices.length,
|
||||
(_) => LineSegment.zero(),
|
||||
growable: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Gives back the shape vectors multiplied by the size
|
||||
Iterable<Vector2> scaled() {
|
||||
if (!_cachedScaledShape.isCacheValid([size])) {
|
||||
for (var i = 0; i < _sizedVertices.length; i++) {
|
||||
Iterable<Vector2> localVertices() {
|
||||
final center = localCenter;
|
||||
if (!_cachedLocalVertices.isCacheValid([size, center])) {
|
||||
final halfSize = this.halfSize;
|
||||
for (var i = 0; i < _localVertices.length; i++) {
|
||||
final point = normalizedVertices[i];
|
||||
(_sizedVertices[i]..setFrom(point)).multiply(halfSize);
|
||||
(_localVertices[i]..setFrom(point))
|
||||
..multiply(halfSize)
|
||||
..add(center)
|
||||
..rotate(angle, center: center);
|
||||
}
|
||||
_cachedScaledShape.updateCache(_sizedVertices, [size.clone()]);
|
||||
_cachedLocalVertices.updateCache(_localVertices, [
|
||||
size.clone(),
|
||||
center.clone(),
|
||||
]);
|
||||
}
|
||||
return _cachedScaledShape.value!;
|
||||
return _cachedLocalVertices.value!;
|
||||
}
|
||||
|
||||
final _cachedRenderPath = ValueCache<Path>();
|
||||
/// Gives back the shape vectors multiplied by the size and scale
|
||||
List<Vector2> globalVertices() {
|
||||
final scale = this.scale;
|
||||
if (!_cachedGlobalVertices.isCacheValid([
|
||||
position,
|
||||
offsetPosition,
|
||||
relativeOffset,
|
||||
size,
|
||||
scale,
|
||||
parentAngle,
|
||||
angle,
|
||||
])) {
|
||||
var i = 0;
|
||||
final center = absoluteCenter;
|
||||
final halfSize = this.halfSize;
|
||||
for (final normalizedPoint in normalizedVertices) {
|
||||
_globalVertices[i]
|
||||
..setFrom(normalizedPoint)
|
||||
..multiply(halfSize)
|
||||
..multiply(scale)
|
||||
..add(center)
|
||||
..rotate(parentAngle + angle, center: center);
|
||||
i++;
|
||||
}
|
||||
if (scale.y.isNegative || scale.x.isNegative) {
|
||||
// Since the list will be clockwise we have to reverse it for it to
|
||||
// become counterclockwise.
|
||||
_reverseList(_globalVertices);
|
||||
}
|
||||
_cachedGlobalVertices.updateCache(_globalVertices, [
|
||||
position.clone(),
|
||||
offsetPosition.clone(),
|
||||
relativeOffset.clone(),
|
||||
size.clone(),
|
||||
scale.clone(),
|
||||
parentAngle,
|
||||
angle,
|
||||
]);
|
||||
}
|
||||
return _cachedGlobalVertices.value!;
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas, Paint paint) {
|
||||
if (!_cachedRenderPath
|
||||
.isCacheValid([offsetPosition, relativeOffset, size, angle])) {
|
||||
final center = localCenter;
|
||||
if (!_cachedRenderPath.isCacheValid([
|
||||
offsetPosition,
|
||||
relativeOffset,
|
||||
size,
|
||||
parentAngle,
|
||||
angle,
|
||||
])) {
|
||||
var i = 0;
|
||||
(isCanvasPrepared ? localVertices() : globalVertices()).forEach((point) {
|
||||
_renderVertices[i] = point.toOffset();
|
||||
i++;
|
||||
});
|
||||
_cachedRenderPath.updateCache(
|
||||
Path()
|
||||
..addPolygon(
|
||||
scaled().map(
|
||||
(point) {
|
||||
final pathPoint = center + point;
|
||||
if (!isCanvasPrepared) {
|
||||
pathPoint.rotate(angle, center: center);
|
||||
}
|
||||
return pathPoint.toOffset();
|
||||
},
|
||||
).toList(),
|
||||
true,
|
||||
),
|
||||
_path
|
||||
..reset()
|
||||
..addPolygon(_renderVertices, true),
|
||||
[
|
||||
offsetPosition.clone(),
|
||||
relativeOffset.clone(),
|
||||
size.clone(),
|
||||
parentAngle,
|
||||
angle,
|
||||
],
|
||||
);
|
||||
@ -115,32 +184,13 @@ class Polygon extends Shape {
|
||||
canvas.drawPath(_cachedRenderPath.value!, paint);
|
||||
}
|
||||
|
||||
final _cachedHitbox = ValueCache<List<Vector2>>();
|
||||
|
||||
/// Gives back the vertices represented as a list of points which
|
||||
/// are the "corners" of the hitbox rotated with [angle].
|
||||
List<Vector2> hitbox() {
|
||||
// Use cached bounding vertices if state of the component hasn't changed
|
||||
if (!_cachedHitbox
|
||||
.isCacheValid([absoluteCenter, size, parentAngle, angle])) {
|
||||
final scaledVertices = scaled().toList(growable: false);
|
||||
final center = absoluteCenter;
|
||||
for (var i = 0; i < _hitboxVertices.length; i++) {
|
||||
_hitboxVertices[i]
|
||||
..setFrom(center)
|
||||
..add(scaledVertices[i])
|
||||
..rotate(parentAngle + angle, center: center);
|
||||
}
|
||||
_cachedHitbox.updateCache(
|
||||
_hitboxVertices,
|
||||
[absoluteCenter, size.clone(), parentAngle, angle],
|
||||
);
|
||||
}
|
||||
return _cachedHitbox.value!;
|
||||
}
|
||||
/// These are in the global hitbox coordinate space since all hitboxes are
|
||||
/// compared towards each other.
|
||||
|
||||
/// Checks whether the polygon represented by the list of [Vector2] contains
|
||||
/// the [point].
|
||||
/// Checks whether the polygon contains the [point].
|
||||
/// Note: The polygon needs to be convex for this to work.
|
||||
@override
|
||||
bool containsPoint(Vector2 point) {
|
||||
// If the size is 0 then it can't contain any points
|
||||
@ -148,7 +198,7 @@ class Polygon extends Shape {
|
||||
return false;
|
||||
}
|
||||
|
||||
final vertices = hitbox();
|
||||
final vertices = globalVertices();
|
||||
for (var i = 0; i < vertices.length; i++) {
|
||||
final edge = getEdge(i, vertices: vertices);
|
||||
final isOutside = (edge.to.x - edge.from.x) * (point.y - edge.from.y) -
|
||||
@ -166,7 +216,7 @@ class Polygon extends Shape {
|
||||
/// is null return all vertices as [LineSegment]s.
|
||||
List<LineSegment> possibleIntersectionVertices(Rect? rect) {
|
||||
final rectIntersections = <LineSegment>[];
|
||||
final vertices = hitbox();
|
||||
final vertices = globalVertices();
|
||||
for (var i = 0; i < vertices.length; i++) {
|
||||
final edge = getEdge(i, vertices: vertices);
|
||||
if (rect?.intersectsSegment(edge.from, edge.to) ?? true) {
|
||||
@ -177,21 +227,28 @@ class Polygon extends Shape {
|
||||
}
|
||||
|
||||
LineSegment getEdge(int i, {required List<Vector2> vertices}) {
|
||||
return LineSegment(
|
||||
getVertex(i, vertices: vertices),
|
||||
getVertex(
|
||||
i + 1,
|
||||
vertices: vertices,
|
||||
),
|
||||
);
|
||||
_lineSegments[i].from.setFrom(getVertex(i, vertices: vertices));
|
||||
_lineSegments[i].to.setFrom(getVertex(i + 1, vertices: vertices));
|
||||
return _lineSegments[i];
|
||||
}
|
||||
|
||||
Vector2 getVertex(int i, {List<Vector2>? vertices}) {
|
||||
vertices ??= hitbox();
|
||||
vertices ??= globalVertices();
|
||||
return vertices[i % vertices.length];
|
||||
}
|
||||
|
||||
void _reverseList(List<Object> list) {
|
||||
for (var i = 0; i < list.length / 2; i++) {
|
||||
final temp = list[i];
|
||||
list[i] = list[list.length - 1 - i];
|
||||
list[list.length - 1 - i] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HitboxPolygon extends Polygon with HitboxShape {
|
||||
HitboxPolygon(List<Vector2> definition) : super.fromDefinition(definition);
|
||||
|
||||
factory HitboxPolygon.fromPolygon(Polygon polygon) =>
|
||||
HitboxPolygon(polygon.normalizedVertices);
|
||||
}
|
||||
|
||||
@ -14,6 +14,10 @@ abstract class Shape {
|
||||
final ValueCache<Vector2> _halfSizeCache = ValueCache();
|
||||
final ValueCache<Vector2> _localCenterCache = ValueCache();
|
||||
final ValueCache<Vector2> _absoluteCenterCache = ValueCache();
|
||||
final ValueCache<Vector2> _relativePositionCache = ValueCache();
|
||||
|
||||
// These are used to avoid creating new vector objects on some method calls
|
||||
final Vector2 _identityVector2 = Vector2Extension.identity();
|
||||
|
||||
/// Should be the center of that [offsetPosition] and [relativeOffset]
|
||||
/// should be calculated from, if they are not set this is the center of the
|
||||
@ -23,6 +27,10 @@ abstract class Shape {
|
||||
/// The size is the bounding box of the [Shape]
|
||||
Vector2 size;
|
||||
|
||||
/// The scaled size of the bounding box of the [Shape], if no scaling of the
|
||||
/// parent is supported this will return [size].
|
||||
Vector2 get scale => _identityVector2;
|
||||
|
||||
Vector2 get halfSize {
|
||||
if (!_halfSizeCache.isCacheValid([size])) {
|
||||
_halfSizeCache.updateCache(size / 2, [size.clone()]);
|
||||
@ -41,11 +49,19 @@ abstract class Shape {
|
||||
Vector2 relativeOffset = Vector2.zero();
|
||||
|
||||
/// The [relativeOffset] converted to a length vector
|
||||
Vector2 get relativePosition => (size / 2)..multiply(relativeOffset);
|
||||
Vector2 get relativePosition {
|
||||
if (!_relativePositionCache.isCacheValid([size, relativeOffset])) {
|
||||
_relativePositionCache.updateCache(
|
||||
(size / 2)..multiply(relativeOffset),
|
||||
[size.clone(), relativeOffset.clone()],
|
||||
);
|
||||
}
|
||||
return _relativePositionCache.value!;
|
||||
}
|
||||
|
||||
/// The angle of the parent that has to be taken into consideration for some
|
||||
/// applications of [Shape], for example [HitboxShape]
|
||||
double parentAngle;
|
||||
double parentAngle = 0;
|
||||
|
||||
/// Whether the context that the shape is in has already prepared (rotated
|
||||
/// and translated) the canvas before coming to the shape's render method.
|
||||
@ -103,7 +119,6 @@ abstract class Shape {
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
this.angle = 0,
|
||||
this.parentAngle = 0,
|
||||
}) : position = position ?? Vector2.zero(),
|
||||
size = size ?? Vector2.zero();
|
||||
|
||||
@ -116,25 +131,22 @@ abstract class Shape {
|
||||
Set<Vector2> intersections(Shape other) {
|
||||
return intersection_system.intersections(this, other);
|
||||
}
|
||||
|
||||
/// Turns a [Shape] into a [ShapeComponent]
|
||||
///
|
||||
/// Do note that while a [Shape] is defined from the center, a
|
||||
/// [ShapeComponent] like all other components default to an [Anchor] in the
|
||||
/// top left corner.
|
||||
ShapeComponent toComponent({Paint? paint}) {
|
||||
return ShapeComponent(this, paint: paint);
|
||||
}
|
||||
}
|
||||
|
||||
mixin HitboxShape on Shape {
|
||||
late PositionComponent component;
|
||||
|
||||
@override
|
||||
Vector2 get size => component.scaledSize;
|
||||
bool isCanvasPrepared = true;
|
||||
|
||||
@override
|
||||
double get parentAngle => component.angle;
|
||||
Vector2 get size => component.size;
|
||||
|
||||
@override
|
||||
Vector2 get scale => component.scale;
|
||||
|
||||
@override
|
||||
double get parentAngle => component.absoluteAngle;
|
||||
|
||||
@override
|
||||
Vector2 get position => component.absoluteCenter;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/geometry.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -14,7 +13,7 @@ void main() {
|
||||
group('JoystickDirection tests', () {
|
||||
test('Can convert angle to JoystickDirection', () {
|
||||
final joystick = JoystickComponent(
|
||||
knob: Circle(radius: 5.0).toComponent(),
|
||||
knob: CircleComponent(radius: 5.0),
|
||||
size: 20,
|
||||
margin: const EdgeInsets.only(left: 20, bottom: 20),
|
||||
);
|
||||
@ -47,7 +46,7 @@ void main() {
|
||||
'knob should stay on correct side when the total delta is larger than the size and then the knob is moved slightly back again',
|
||||
(game) async {
|
||||
final joystick = JoystickComponent(
|
||||
knob: Circle(radius: 5.0).toComponent(),
|
||||
knob: CircleComponent(radius: 5.0),
|
||||
size: 20,
|
||||
margin: const EdgeInsets.only(left: 20, top: 20),
|
||||
);
|
||||
|
||||
420
packages/flame/test/components/shape_component_test.dart
Normal file
420
packages/flame/test/components/shape_component_test.dart
Normal file
@ -0,0 +1,420 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('ShapeComponent.containsPoint tests', () {
|
||||
test('Simple circle contains point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(1, 1),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.5)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Simple rectangle contains point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2(1, 1),
|
||||
size: Vector2(1, 1),
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.5)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Simple polygon contains point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 2),
|
||||
Vector2(2, 1),
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
],
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(2.0, 1.9)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated circle does not contain point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(1, 1),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated rectangle does not contain point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated polygon does not contain point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 2),
|
||||
Vector2(2, 1),
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
],
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated circle contains point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(1, 1),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(1.0, 1.9)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated rectangle contains point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(1.0, 2.1)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated polygon contains point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
],
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(2.7, 1.7)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Horizontally flipped rectangle contains point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
anchor: Anchor.center,
|
||||
)..flipVerticallyAroundCenter();
|
||||
expect(
|
||||
component.containsPoint(Vector2(2.0, 2.0)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Initially rotated CircleComponent does not contain point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(1, 1),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Initially rotated RectangleComponent does not contain point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Initially rotated PolygonComponent does not contain point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
],
|
||||
angle: pi / 4,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(1.9)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Rotated PolygonComponent contains point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
],
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
component.angle = pi / 4;
|
||||
expect(
|
||||
component.containsPoint(Vector2(2.7, 1.7)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Moved CircleComponent contains point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(2, 2),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(2.1)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Moved RectangleComponent contains point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2(2, 2),
|
||||
size: Vector2(1, 1),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(2.1)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Moved PolygonComponent contains point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
],
|
||||
position: Vector2.all(1.0),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(0.9, 1.0)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('Sized up CircleComponent does not contain point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2(1, 1),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
component.size += Vector2.all(1.0);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(2.1)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Sized up RectangleComponent does not contain point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2(1, 1),
|
||||
size: Vector2(2, 2),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2.all(2.1)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('Sized PolygonComponent does not contain point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
],
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
component.size += Vector2.all(1.0);
|
||||
expect(
|
||||
component.containsPoint(Vector2(2.0, 2.6)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('CircleComponent with default anchor (topLeft) contains point', () {
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2.all(1.0),
|
||||
angle: pi / 4,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(0.9, 2.0)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('RectangleComponent with default anchor (topLeft) contains point', () {
|
||||
final component = RectangleComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(1.0),
|
||||
angle: pi / 4,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(0.9, 2.0)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('PolygonComponent with default anchor (topLeft) contains point', () {
|
||||
final component = PolygonComponent.fromPoints(
|
||||
[
|
||||
Vector2(2, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(2, 2),
|
||||
Vector2(3, 1),
|
||||
],
|
||||
angle: pi / 4,
|
||||
);
|
||||
expect(
|
||||
component.containsPoint(Vector2(1.0, 0.99)),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
flameGame.test(
|
||||
'CircleComponent with multiple parents contains point',
|
||||
(game) async {
|
||||
PositionComponent createParent() {
|
||||
return PositionComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 2,
|
||||
);
|
||||
}
|
||||
|
||||
final component = CircleComponent(
|
||||
radius: 1.0,
|
||||
position: Vector2.all(1.0),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
final grandParent = createParent();
|
||||
final parent = createParent();
|
||||
grandParent.add(parent);
|
||||
parent.add(component);
|
||||
await game.add(grandParent);
|
||||
expect(
|
||||
component.containsPoint(Vector2(-1.0, 1.0)),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'RectangleComponent with multiple parents contains point',
|
||||
(game) async {
|
||||
PositionComponent createParent() {
|
||||
return PositionComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 2,
|
||||
);
|
||||
}
|
||||
|
||||
final component = RectangleComponent(
|
||||
size: Vector2.all(1.0),
|
||||
position: Vector2.all(1.0),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
final grandParent = createParent();
|
||||
final parent = createParent();
|
||||
grandParent.add(parent);
|
||||
parent.add(component);
|
||||
await game.add(grandParent);
|
||||
expect(
|
||||
component.containsPoint(Vector2(-1.0, 1.0)),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
flameGame.test(
|
||||
'PolygonComponent with multiple parents contains point',
|
||||
(game) async {
|
||||
PositionComponent createParent() {
|
||||
return PositionComponent(
|
||||
position: Vector2.all(1.0),
|
||||
size: Vector2.all(2.0),
|
||||
angle: pi / 2,
|
||||
);
|
||||
}
|
||||
|
||||
final component = PolygonComponent(
|
||||
normalizedVertices: [
|
||||
Vector2(1, 0),
|
||||
Vector2(0, -1),
|
||||
Vector2(-1, 0),
|
||||
Vector2(0, 1),
|
||||
],
|
||||
size: Vector2.all(1.0),
|
||||
position: Vector2.all(1.0),
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
final grandParent = createParent();
|
||||
final parent = createParent();
|
||||
grandParent.add(parent);
|
||||
parent.add(component);
|
||||
await game.add(grandParent);
|
||||
expect(
|
||||
component.containsPoint(Vector2(-1.0, 1.0)),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -229,7 +229,7 @@ void main() {
|
||||
|
||||
const message = '"prepare/add" called before the game is ready. '
|
||||
'Did you try to access it on the Game constructor? '
|
||||
'Use the "onLoad" or "onParentMethod" method instead.';
|
||||
'Use the "onLoad" or "onMount" method instead.';
|
||||
|
||||
expect(
|
||||
() => game.add(component),
|
||||
|
||||
@ -77,19 +77,3 @@ class SkillsAnimationComponent extends RiveComponent with Tappable {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Square extends PositionComponent with HasGameRef<RiveExampleGame> {
|
||||
late final Paint paint;
|
||||
|
||||
Square(Vector2 position) {
|
||||
this.position.setFrom(position);
|
||||
size.setValues(100, 100);
|
||||
paint = PaintExtension.random(withAlpha: 0.9, base: 100);
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
canvas.drawRect(size.toRect(), paint);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user