mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +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
|
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.
|
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
|
## Mixins
|
||||||
### HasHitboxes
|
### HasHitboxes
|
||||||
The `HasHitboxes` mixin is mainly used for two things; to make detection of collisions with other
|
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
|
Three example implementations can be found in the
|
||||||
[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax).
|
[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
|
## SpriteBodyComponent
|
||||||
|
|
||||||
See [SpriteBodyComponent](forge2d.md#SpriteBodyComponent) in the Forge2D documentation.
|
See [SpriteBodyComponent](forge2d.md#SpriteBodyComponent) in the Forge2D documentation.
|
||||||
|
|||||||
@ -10,8 +10,8 @@ class SquareComponent extends RectangleComponent with EffectsHelper {
|
|||||||
Paint? paint,
|
Paint? paint,
|
||||||
int priority = 0,
|
int priority = 0,
|
||||||
}) : super(
|
}) : super(
|
||||||
Vector2.all(size),
|
|
||||||
position: position,
|
position: position,
|
||||||
|
size: Vector2.all(size),
|
||||||
paint: paint,
|
paint: paint,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/geometry.dart';
|
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -12,11 +11,7 @@ import '../../commons/square_component.dart';
|
|||||||
final R = Random();
|
final R = Random();
|
||||||
|
|
||||||
class MovableSquare extends SquareComponent
|
class MovableSquare extends SquareComponent
|
||||||
with
|
with Collidable, HasGameRef<CameraAndViewportGame>, KeyboardHandler {
|
||||||
HasHitboxes,
|
|
||||||
Collidable,
|
|
||||||
HasGameRef<CameraAndViewportGame>,
|
|
||||||
KeyboardHandler {
|
|
||||||
static const double speed = 300;
|
static const double speed = 300;
|
||||||
static final TextPaint textRenderer = TextPaint(
|
static final TextPaint textRenderer = TextPaint(
|
||||||
config: const TextPaintConfig(
|
config: const TextPaintConfig(
|
||||||
@ -27,8 +22,11 @@ class MovableSquare extends SquareComponent
|
|||||||
final Vector2 velocity = Vector2.zero();
|
final Vector2 velocity = Vector2.zero();
|
||||||
late Timer timer;
|
late Timer timer;
|
||||||
|
|
||||||
MovableSquare() : super(priority: 1) {
|
MovableSquare() : super(priority: 1);
|
||||||
addHitbox(HitboxRectangle());
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
timer = Timer(3.0)
|
timer = Timer(3.0)
|
||||||
..stop()
|
..stop()
|
||||||
..callback = () {
|
..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 unpressedPaint = Paint()..color = const Color(0xFF2222FF);
|
||||||
static final pressedPaint = Paint()..color = const Color(0xFF414175);
|
static final pressedPaint = Paint()..color = const Color(0xFF414175);
|
||||||
|
|
||||||
@ -114,9 +112,7 @@ class Rock extends SquareComponent with HasHitboxes, Collidable, Tappable {
|
|||||||
size: 50,
|
size: 50,
|
||||||
priority: 2,
|
priority: 2,
|
||||||
paint: unpressedPaint,
|
paint: unpressedPaint,
|
||||||
) {
|
);
|
||||||
addHitbox(HitboxRectangle());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool onTapDown(_) {
|
bool onTapDown(_) {
|
||||||
|
|||||||
@ -7,14 +7,27 @@ import 'package:flame/geometry.dart';
|
|||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flutter/material.dart' hide Image, Draggable;
|
import 'package:flutter/material.dart' hide Image, Draggable;
|
||||||
|
|
||||||
const circlesInfo = '''
|
class CirclesExample extends FlameGame with HasCollidables, TapDetector {
|
||||||
This example will create a circle every time you tap on the screen. It will have
|
static const description = '''
|
||||||
the initial velocity towards the center of the screen and if it touches another
|
This example will create a circle every time you tap on the screen. It will
|
||||||
circle both of them will change color.
|
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
|
class MyCollidable extends PositionComponent
|
||||||
with HasGameRef<Circles>, HasHitboxes, Collidable {
|
with HasGameRef<CirclesExample>, HasHitboxes, Collidable {
|
||||||
late Vector2 velocity;
|
late Vector2 velocity;
|
||||||
final _collisionColor = Colors.amber;
|
final _collisionColor = Colors.amber;
|
||||||
final _defaultColor = Colors.cyan;
|
final _defaultColor = Colors.cyan;
|
||||||
@ -63,16 +76,3 @@ class MyCollidable extends PositionComponent
|
|||||||
_isCollision = true;
|
_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 '../../commons/commons.dart';
|
||||||
import 'circles.dart';
|
import 'circles.dart';
|
||||||
|
import 'collidable_animation.dart';
|
||||||
import 'multiple_shapes.dart';
|
import 'multiple_shapes.dart';
|
||||||
import 'only_shapes.dart';
|
import 'simple_shapes.dart';
|
||||||
|
|
||||||
void addCollisionDetectionStories(Dashbook dashbook) {
|
void addCollisionDetectionStories(Dashbook dashbook) {
|
||||||
dashbook.storiesOf('Collision Detection')
|
dashbook.storiesOf('Collision Detection')
|
||||||
|
..add(
|
||||||
|
'Collidable AnimationComponent',
|
||||||
|
(_) => GameWidget(game: CollidableAnimationExample()),
|
||||||
|
codeLink: baseLink('collision_detection/collidable_animation.dart'),
|
||||||
|
info: CollidableAnimationExample.description,
|
||||||
|
)
|
||||||
..add(
|
..add(
|
||||||
'Circles',
|
'Circles',
|
||||||
(_) => GameWidget(game: Circles()),
|
(_) => GameWidget(game: CirclesExample()),
|
||||||
codeLink: baseLink('collision_detection/circles.dart'),
|
codeLink: baseLink('collision_detection/circles.dart'),
|
||||||
info: circlesInfo,
|
info: CirclesExample.description,
|
||||||
)
|
)
|
||||||
..add(
|
..add(
|
||||||
'Multiple shapes',
|
'Multiple shapes',
|
||||||
(_) => GameWidget(game: MultipleShapes()),
|
(_) => GameWidget(game: MultipleShapesExample()),
|
||||||
codeLink: baseLink('collision_detection/multiple_shapes.dart'),
|
codeLink: baseLink('collision_detection/multiple_shapes.dart'),
|
||||||
info: multipleShapesInfo,
|
info: MultipleShapesExample.description,
|
||||||
)
|
)
|
||||||
..add(
|
..add(
|
||||||
'Simple Shapes',
|
'Simple Shapes',
|
||||||
(_) => GameWidget(game: OnlyShapes()),
|
(_) => GameWidget(game: SimpleShapesExample()),
|
||||||
codeLink: baseLink('collision_detection/only_shapes.dart'),
|
codeLink: baseLink('collision_detection/simple_shapes.dart'),
|
||||||
info: onlyShapesInfo,
|
info: SimpleShapesExample.description,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,21 +9,95 @@ import 'package:flame/input.dart';
|
|||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
import 'package:flutter/material.dart' hide Image, Draggable;
|
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 }
|
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
|
abstract class MyCollidable extends PositionComponent
|
||||||
with Draggable, HasHitboxes, Collidable {
|
with Draggable, HasHitboxes, Collidable {
|
||||||
double rotationSpeed = 0.0;
|
double rotationSpeed = 0.0;
|
||||||
@ -41,11 +115,7 @@ abstract class MyCollidable extends PositionComponent
|
|||||||
Vector2 size,
|
Vector2 size,
|
||||||
this.velocity,
|
this.velocity,
|
||||||
this.screenCollidable,
|
this.screenCollidable,
|
||||||
) {
|
) : super(position: position, size: size, anchor: Anchor.center);
|
||||||
this.position = position;
|
|
||||||
this.size = size;
|
|
||||||
anchor = Anchor.center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
@ -137,7 +207,7 @@ class CollidablePolygon extends MyCollidable {
|
|||||||
Vector2 velocity,
|
Vector2 velocity,
|
||||||
ScreenCollidable screenCollidable,
|
ScreenCollidable screenCollidable,
|
||||||
) : super(position, size, velocity, screenCollidable) {
|
) : super(position, size, velocity, screenCollidable) {
|
||||||
final shape = HitboxPolygon([
|
final hitbox = HitboxPolygon([
|
||||||
Vector2(-1.0, 0.0),
|
Vector2(-1.0, 0.0),
|
||||||
Vector2(-0.8, 0.6),
|
Vector2(-0.8, 0.6),
|
||||||
Vector2(0.0, 1.0),
|
Vector2(0.0, 1.0),
|
||||||
@ -147,7 +217,7 @@ class CollidablePolygon extends MyCollidable {
|
|||||||
Vector2(0, -1.0),
|
Vector2(0, -1.0),
|
||||||
Vector2(-0.8, -0.8),
|
Vector2(-0.8, -0.8),
|
||||||
]);
|
]);
|
||||||
addHitbox(shape);
|
addHitbox(hitbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,8 +239,7 @@ class CollidableCircle extends MyCollidable {
|
|||||||
Vector2 velocity,
|
Vector2 velocity,
|
||||||
ScreenCollidable screenCollidable,
|
ScreenCollidable screenCollidable,
|
||||||
) : super(position, size, velocity, screenCollidable) {
|
) : super(position, size, velocity, screenCollidable) {
|
||||||
final shape = HitboxCircle();
|
addHitbox(HitboxCircle());
|
||||||
addHitbox(shape);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +248,7 @@ class SnowmanPart extends HitboxCircle {
|
|||||||
final hitPaint = Paint();
|
final hitPaint = Paint();
|
||||||
|
|
||||||
SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor)
|
SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor)
|
||||||
: super(definition: definition) {
|
: super(normalizedRadius: definition) {
|
||||||
this.relativeOffset.setFrom(relativeOffset);
|
this.relativeOffset.setFrom(relativeOffset);
|
||||||
hitPaint..color = startColor;
|
hitPaint..color = startColor;
|
||||||
onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) {
|
onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) {
|
||||||
@ -245,77 +314,3 @@ MyCollidable randomCollidable(
|
|||||||
..rotationSpeed = rotationSpeed;
|
..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();
|
await super.onLoad();
|
||||||
square = SquareComponent(size: 50, position: Vector2(200, 150));
|
square = SquareComponent(size: 50, position: Vector2(200, 150));
|
||||||
add(square);
|
add(square);
|
||||||
final pathMarkers = path.map((p) => CircleComponent(3, position: p));
|
final pathMarkers =
|
||||||
|
path.map((p) => CircleComponent(radius: 3, position: p));
|
||||||
addAll(pathMarkers);
|
addAll(pathMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,7 @@ class MoveEffectExample extends FlameGame {
|
|||||||
}
|
}
|
||||||
for (var i = 0; i < 40; i++) {
|
for (var i = 0; i < 40; i++) {
|
||||||
add(
|
add(
|
||||||
CircleComponent(5)
|
CircleComponent(radius: 5)
|
||||||
..add(
|
..add(
|
||||||
MoveEffect.along(
|
MoveEffect.along(
|
||||||
path1,
|
path1,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class RemoveEffectExample extends FlameGame with HasTappableComponents {
|
|||||||
|
|
||||||
class _RandomCircle extends CircleComponent with Tappable {
|
class _RandomCircle extends CircleComponent with Tappable {
|
||||||
_RandomCircle(double radius, {Vector2? position, Paint? paint})
|
_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) {
|
factory _RandomCircle.random(Random rng) {
|
||||||
final radius = rng.nextDouble() * 30 + 10;
|
final radius = rng.nextDouble() * 30 + 10;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/geometry.dart';
|
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
@ -17,8 +16,8 @@ class JoystickGame extends FlameGame with HasDraggableComponents {
|
|||||||
final knobPaint = BasicPalette.blue.withAlpha(200).paint();
|
final knobPaint = BasicPalette.blue.withAlpha(200).paint();
|
||||||
final backgroundPaint = BasicPalette.blue.withAlpha(100).paint();
|
final backgroundPaint = BasicPalette.blue.withAlpha(100).paint();
|
||||||
joystick = JoystickComponent(
|
joystick = JoystickComponent(
|
||||||
knob: Circle(radius: 30).toComponent(paint: knobPaint),
|
knob: CircleComponent(radius: 30, paint: knobPaint),
|
||||||
background: Circle(radius: 100).toComponent(paint: backgroundPaint),
|
background: CircleComponent(radius: 100, paint: backgroundPaint),
|
||||||
margin: const EdgeInsets.only(left: 40, bottom: 40),
|
margin: const EdgeInsets.only(left: 40, bottom: 40),
|
||||||
);
|
);
|
||||||
player = JoystickPlayer(joystick);
|
player = JoystickPlayer(joystick);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import 'package:flame/components.dart';
|
|||||||
import 'package:flame/effects.dart';
|
import 'package:flame/effects.dart';
|
||||||
import 'package:flame/extensions.dart';
|
import 'package:flame/extensions.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/geometry.dart';
|
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
import 'package:flame/sprite.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
|
// A button, created from a shape, that adds a rotation effect to the player
|
||||||
// when it is pressed.
|
// when it is pressed.
|
||||||
final shapeButton = HudButtonComponent(
|
final shapeButton = HudButtonComponent(
|
||||||
button: Circle(radius: 35).toComponent(paint: BasicPalette.white.paint()),
|
button: CircleComponent(radius: 35),
|
||||||
buttonDown: Rectangle(size: buttonSize)
|
buttonDown: RectangleComponent(
|
||||||
.toComponent(paint: BasicPalette.blue.paint()),
|
size: buttonSize,
|
||||||
|
paint: BasicPalette.blue.paint(),
|
||||||
|
),
|
||||||
margin: const EdgeInsets.only(
|
margin: const EdgeInsets.only(
|
||||||
right: 85,
|
right: 85,
|
||||||
bottom: 150,
|
bottom: 150,
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
- Components that manipulate canvas state are now responsible for saving/restoring that state
|
- Components that manipulate canvas state are now responsible for saving/restoring that state
|
||||||
- Remove `super.render` calls that are no longer needed
|
- Remove `super.render` calls that are no longer needed
|
||||||
- Fixed typo in error message
|
- 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]
|
## [1.0.0-releasecandidate.16]
|
||||||
- `changePriority` no longer breaks game loop iteration
|
- `changePriority` no longer breaks game loop iteration
|
||||||
|
|||||||
@ -176,6 +176,20 @@ class Component with Loadable {
|
|||||||
nextParent = component;
|
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.
|
/// It receives the new game size.
|
||||||
/// Executed right after the component is attached to a game and right before
|
/// Executed right after the component is attached to a game and right before
|
||||||
/// [onLoad] is called.
|
/// [onLoad] is called.
|
||||||
@ -321,7 +335,7 @@ class Component with Loadable {
|
|||||||
parentGame.hasLayout,
|
parentGame.hasLayout,
|
||||||
'"prepare/add" called before the game is ready. '
|
'"prepare/add" called before the game is ready. '
|
||||||
'Did you try to access it on the Game constructor? '
|
'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) {
|
if (parentGame is FlameGame) {
|
||||||
parentGame.prepareComponent(this);
|
parentGame.prepareComponent(this);
|
||||||
|
|||||||
@ -163,8 +163,17 @@ class PositionComponent extends Component {
|
|||||||
/// component as seen from the parent's perspective, and it is equal to
|
/// component as seen from the parent's perspective, and it is equal to
|
||||||
/// [size] * [scale]. This is a computed property and cannot be
|
/// [size] * [scale]. This is a computed property and cannot be
|
||||||
/// modified by the user.
|
/// modified by the user.
|
||||||
Vector2 get scaledSize =>
|
Vector2 get scaledSize {
|
||||||
Vector2(width * scale.x.abs(), height * scale.y.abs());
|
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
|
/// Measure the distance (in parent's coordinate space) between this
|
||||||
/// component's anchor and the [other] component's anchor.
|
/// component's anchor and the [other] component's anchor.
|
||||||
|
|||||||
@ -8,96 +8,155 @@ import '../../palette.dart';
|
|||||||
import '../anchor.dart';
|
import '../anchor.dart';
|
||||||
import '../extensions/vector2.dart';
|
import '../extensions/vector2.dart';
|
||||||
|
|
||||||
class ShapeComponent extends PositionComponent {
|
/// A [ShapeComponent] is a [Shape] wrapped in a [PositionComponent] so that it
|
||||||
final Shape shape;
|
/// 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;
|
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(
|
ShapeComponent(
|
||||||
this.shape, {
|
this.shape, {
|
||||||
Paint? paint,
|
Paint? paint,
|
||||||
|
Vector2? position,
|
||||||
|
Vector2? size,
|
||||||
|
Vector2? scale,
|
||||||
|
double? angle,
|
||||||
|
Anchor? anchor,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) : paint = paint ?? BasicPalette.white.paint(),
|
}) : paint = paint ?? BasicPalette.white.paint(),
|
||||||
super(
|
super(
|
||||||
position: shape.position,
|
position: position,
|
||||||
size: shape.size,
|
size: size,
|
||||||
angle: shape.angle,
|
scale: scale,
|
||||||
anchor: Anchor.center,
|
angle: angle,
|
||||||
|
anchor: anchor,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
) {
|
) {
|
||||||
shape.isCanvasPrepared = true;
|
shape.isCanvasPrepared = true;
|
||||||
|
addHitbox(shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
shape.render(canvas, paint);
|
shape.render(canvas, paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool containsPoint(Vector2 point) => shape.containsPoint(point);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CircleComponent extends ShapeComponent {
|
class CircleComponent extends ShapeComponent {
|
||||||
CircleComponent(
|
CircleComponent({
|
||||||
double radius, {
|
required double radius,
|
||||||
Vector2? position,
|
|
||||||
Paint? paint,
|
Paint? paint,
|
||||||
|
Vector2? position,
|
||||||
|
Vector2? scale,
|
||||||
|
double? angle,
|
||||||
|
Anchor? anchor,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) : super(
|
}) : super(
|
||||||
Circle(radius: radius, position: position),
|
HitboxCircle(),
|
||||||
paint: paint,
|
paint: paint,
|
||||||
|
position: position,
|
||||||
|
size: Vector2.all(radius * 2),
|
||||||
|
scale: scale,
|
||||||
|
angle: angle,
|
||||||
|
anchor: anchor,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RectangleComponent extends ShapeComponent {
|
class RectangleComponent extends ShapeComponent {
|
||||||
RectangleComponent(
|
RectangleComponent({
|
||||||
Vector2 size, {
|
required Vector2 size,
|
||||||
Vector2? position,
|
|
||||||
Paint? paint,
|
Paint? paint,
|
||||||
|
Vector2? position,
|
||||||
|
Vector2? scale,
|
||||||
|
double? angle,
|
||||||
|
Anchor? anchor,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) : super(
|
}) : super(
|
||||||
Rectangle(size: size, position: position),
|
HitboxRectangle(),
|
||||||
paint: paint,
|
paint: paint,
|
||||||
|
position: position,
|
||||||
|
size: size,
|
||||||
|
scale: scale,
|
||||||
|
angle: angle,
|
||||||
|
anchor: anchor,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
);
|
);
|
||||||
|
|
||||||
RectangleComponent.square(
|
RectangleComponent.square({
|
||||||
double size, {
|
required double size,
|
||||||
Vector2? position,
|
|
||||||
Paint? paint,
|
Paint? paint,
|
||||||
|
Vector2? position,
|
||||||
|
Vector2? scale,
|
||||||
|
double? angle,
|
||||||
|
Anchor? anchor,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) : super(
|
}) : super(
|
||||||
Rectangle(size: Vector2.all(size), position: position),
|
HitboxRectangle(),
|
||||||
paint: paint,
|
paint: paint,
|
||||||
|
position: position,
|
||||||
|
size: Vector2.all(size),
|
||||||
|
scale: scale,
|
||||||
|
angle: angle,
|
||||||
|
anchor: anchor,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PolygonComponent extends ShapeComponent {
|
class PolygonComponent extends ShapeComponent {
|
||||||
PolygonComponent(
|
/// The [normalizedVertices] should be a list of points that range between
|
||||||
List<Vector2> points, {
|
/// [-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,
|
Paint? paint,
|
||||||
int? priority,
|
|
||||||
}) : super(Polygon(points), paint: paint, priority: priority);
|
|
||||||
|
|
||||||
PolygonComponent.fromDefinition(
|
|
||||||
List<Vector2> normalizedVertices, {
|
|
||||||
Vector2? size,
|
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
Paint? paint,
|
Vector2? size,
|
||||||
|
Vector2? scale,
|
||||||
|
double? angle,
|
||||||
|
Anchor? anchor,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) : super(
|
}) : super(
|
||||||
Polygon.fromDefinition(
|
HitboxPolygon(normalizedVertices),
|
||||||
normalizedVertices,
|
|
||||||
position: position,
|
|
||||||
size: size,
|
|
||||||
),
|
|
||||||
paint: paint,
|
paint: paint,
|
||||||
|
position: position,
|
||||||
|
size: size,
|
||||||
|
scale: scale,
|
||||||
|
angle: angle,
|
||||||
|
anchor: anchor,
|
||||||
priority: priority,
|
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
|
/// Whether the [Vector2] is the zero vector or not
|
||||||
bool isZero() => x == 0 && y == 0;
|
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 the [Vector2] with [angle] in radians
|
||||||
/// rotates around [center] if it is defined
|
/// rotates around [center] if it is defined
|
||||||
/// In a screen coordinate system (where the y-axis is flipped) it rotates in
|
/// 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
|
/// 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
|
/// 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:
|
/// Example:
|
||||||
/// Up: Vector(0.0, -1.0).screenAngle == 0
|
/// 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.
|
/// Creates a heading [Vector2] with the given angle in degrees.
|
||||||
static Vector2 fromDegrees(double d) => fromRadians(d * degrees2Radians);
|
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';
|
import 'shape.dart';
|
||||||
|
|
||||||
class Circle extends Shape {
|
class Circle extends Shape {
|
||||||
/// The [normalizedRadius] is how many percentages of the shortest edge of
|
/// The [normalizedRadius] is what ratio (0.0, 1.0] of the shortest edge of
|
||||||
/// [size] that the circle should cover.
|
/// [size]/2 that the circle should cover.
|
||||||
double normalizedRadius = 1;
|
double normalizedRadius = 1.0;
|
||||||
|
|
||||||
/// With this constructor you can create your [Circle] from a radius and
|
/// With this constructor you can create your [Circle] from a radius and
|
||||||
/// a position. It will also calculate the bounding rectangle [size] for the
|
/// 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]
|
/// This constructor is used by [HitboxCircle]
|
||||||
/// definition is the percentages of the shortest edge of [size] that the
|
/// [relation] is the relation [0.0, 1.0] of the shortest edge of [size] that
|
||||||
/// circle should fill.
|
/// the circle should fill.
|
||||||
Circle.fromDefinition({
|
Circle.fromDefinition({
|
||||||
this.normalizedRadius = 1.0,
|
double? relation,
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
Vector2? size,
|
Vector2? size,
|
||||||
double? angle,
|
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
|
/// This render method doesn't rotate the canvas according to angle since a
|
||||||
/// circle will look the same rotated as not rotated.
|
/// circle will look the same rotated as not rotated.
|
||||||
@ -100,9 +110,15 @@ class Circle extends Shape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class HitboxCircle extends Circle with HitboxShape {
|
class HitboxCircle extends Circle with HitboxShape {
|
||||||
@override
|
HitboxCircle({
|
||||||
HitboxCircle({double definition = 1})
|
double? normalizedRadius,
|
||||||
: super.fromDefinition(
|
Vector2? position,
|
||||||
normalizedRadius: definition,
|
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);
|
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
|
/// Returns an empty list if there are no intersections between the segments
|
||||||
/// If the segments are concurrent, the intersecting point is returned as a
|
/// If the segments are concurrent, the intersecting point is returned as a
|
||||||
/// list with a single point
|
/// list with a single point
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
import 'dart:math';
|
|
||||||
import 'dart:ui' hide Canvas;
|
import 'dart:ui' hide Canvas;
|
||||||
|
|
||||||
import '../../game.dart';
|
import '../../game.dart';
|
||||||
import '../../geometry.dart';
|
import '../../geometry.dart';
|
||||||
import '../components/cache/value_cache.dart';
|
import '../components/cache/value_cache.dart';
|
||||||
import '../extensions/canvas.dart';
|
import '../extensions/canvas.dart';
|
||||||
|
import '../extensions/offset.dart';
|
||||||
import '../extensions/rect.dart';
|
import '../extensions/rect.dart';
|
||||||
import '../extensions/vector2.dart';
|
import '../extensions/vector2.dart';
|
||||||
import 'shape.dart';
|
import 'shape.dart';
|
||||||
|
|
||||||
class Polygon extends Shape {
|
class Polygon extends Shape {
|
||||||
final List<Vector2> normalizedVertices;
|
final List<Vector2> normalizedVertices;
|
||||||
// These lists are used to minimize the amount of [Vector2] objects that are
|
// These lists are used to minimize the amount of objects that are created,
|
||||||
// created, only change them if the cache is deemed invalid
|
// and only change the contained object if the corresponding `ValueCache` is
|
||||||
late final List<Vector2> _sizedVertices;
|
// deemed outdated.
|
||||||
late final List<Vector2> _hitboxVertices;
|
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
|
/// With this constructor you create your [Polygon] from positions in your
|
||||||
/// intended space. It will automatically calculate the [size] and center
|
/// intended space. It will automatically calculate the [size] and center
|
||||||
@ -23,21 +31,19 @@ class Polygon extends Shape {
|
|||||||
List<Vector2> points, {
|
List<Vector2> points, {
|
||||||
double angle = 0,
|
double angle = 0,
|
||||||
}) {
|
}) {
|
||||||
final center = points.fold<Vector2>(
|
assert(
|
||||||
Vector2.zero(),
|
points.length > 2,
|
||||||
(sum, v) => sum + v,
|
'List of points is too short to create a polygon',
|
||||||
) /
|
|
||||||
points.length.toDouble();
|
|
||||||
final bottomRight = points.fold<Vector2>(
|
|
||||||
Vector2.zero(),
|
|
||||||
(bottomRight, v) {
|
|
||||||
return Vector2(
|
|
||||||
max(bottomRight.x, v.x),
|
|
||||||
max(bottomRight.y, v.y),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
final halfSize = bottomRight - center;
|
final path = Path()
|
||||||
|
..addPolygon(
|
||||||
|
points.map((p) => p.toOffset()).toList(growable: false),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
final boundingRect = path.getBounds();
|
||||||
|
final centerOffset = boundingRect.center;
|
||||||
|
final center = centerOffset.toVector2();
|
||||||
|
final halfSize = (boundingRect.bottomRight - centerOffset).toVector2();
|
||||||
final definition =
|
final definition =
|
||||||
points.map<Vector2>((v) => (v - center)..divide(halfSize)).toList();
|
points.map<Vector2>((v) => (v - center)..divide(halfSize)).toList();
|
||||||
return Polygon.fromDefinition(
|
return Polygon.fromDefinition(
|
||||||
@ -50,9 +56,10 @@ class Polygon extends Shape {
|
|||||||
|
|
||||||
/// With this constructor you define the [Polygon] from the center of and with
|
/// With this constructor you define the [Polygon] from the center of and with
|
||||||
/// percentages of the size of the shape.
|
/// 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.
|
/// 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(
|
Polygon.fromDefinition(
|
||||||
this.normalizedVertices, {
|
this.normalizedVertices, {
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
@ -63,51 +70,113 @@ class Polygon extends Shape {
|
|||||||
size: size,
|
size: size,
|
||||||
angle: angle ?? 0,
|
angle: angle ?? 0,
|
||||||
) {
|
) {
|
||||||
_sizedVertices =
|
List<Vector2> generateList() {
|
||||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
return List.generate(
|
||||||
_hitboxVertices =
|
normalizedVertices.length,
|
||||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
(_) => 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
|
/// Gives back the shape vectors multiplied by the size
|
||||||
Iterable<Vector2> scaled() {
|
Iterable<Vector2> localVertices() {
|
||||||
if (!_cachedScaledShape.isCacheValid([size])) {
|
final center = localCenter;
|
||||||
for (var i = 0; i < _sizedVertices.length; i++) {
|
if (!_cachedLocalVertices.isCacheValid([size, center])) {
|
||||||
|
final halfSize = this.halfSize;
|
||||||
|
for (var i = 0; i < _localVertices.length; i++) {
|
||||||
final point = normalizedVertices[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
|
@override
|
||||||
void render(Canvas canvas, Paint paint) {
|
void render(Canvas canvas, Paint paint) {
|
||||||
if (!_cachedRenderPath
|
if (!_cachedRenderPath.isCacheValid([
|
||||||
.isCacheValid([offsetPosition, relativeOffset, size, angle])) {
|
offsetPosition,
|
||||||
final center = localCenter;
|
relativeOffset,
|
||||||
|
size,
|
||||||
|
parentAngle,
|
||||||
|
angle,
|
||||||
|
])) {
|
||||||
|
var i = 0;
|
||||||
|
(isCanvasPrepared ? localVertices() : globalVertices()).forEach((point) {
|
||||||
|
_renderVertices[i] = point.toOffset();
|
||||||
|
i++;
|
||||||
|
});
|
||||||
_cachedRenderPath.updateCache(
|
_cachedRenderPath.updateCache(
|
||||||
Path()
|
_path
|
||||||
..addPolygon(
|
..reset()
|
||||||
scaled().map(
|
..addPolygon(_renderVertices, true),
|
||||||
(point) {
|
|
||||||
final pathPoint = center + point;
|
|
||||||
if (!isCanvasPrepared) {
|
|
||||||
pathPoint.rotate(angle, center: center);
|
|
||||||
}
|
|
||||||
return pathPoint.toOffset();
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
[
|
[
|
||||||
offsetPosition.clone(),
|
offsetPosition.clone(),
|
||||||
relativeOffset.clone(),
|
relativeOffset.clone(),
|
||||||
size.clone(),
|
size.clone(),
|
||||||
|
parentAngle,
|
||||||
angle,
|
angle,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -115,32 +184,13 @@ class Polygon extends Shape {
|
|||||||
canvas.drawPath(_cachedRenderPath.value!, paint);
|
canvas.drawPath(_cachedRenderPath.value!, paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _cachedHitbox = ValueCache<List<Vector2>>();
|
|
||||||
|
|
||||||
/// Gives back the vertices represented as a list of points which
|
/// Gives back the vertices represented as a list of points which
|
||||||
/// are the "corners" of the hitbox rotated with [angle].
|
/// are the "corners" of the hitbox rotated with [angle].
|
||||||
List<Vector2> hitbox() {
|
/// These are in the global hitbox coordinate space since all hitboxes are
|
||||||
// Use cached bounding vertices if state of the component hasn't changed
|
/// compared towards each other.
|
||||||
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!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the polygon represented by the list of [Vector2] contains
|
/// Checks whether the polygon contains the [point].
|
||||||
/// the [point].
|
/// Note: The polygon needs to be convex for this to work.
|
||||||
@override
|
@override
|
||||||
bool containsPoint(Vector2 point) {
|
bool containsPoint(Vector2 point) {
|
||||||
// If the size is 0 then it can't contain any points
|
// If the size is 0 then it can't contain any points
|
||||||
@ -148,7 +198,7 @@ class Polygon extends Shape {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final vertices = hitbox();
|
final vertices = globalVertices();
|
||||||
for (var i = 0; i < vertices.length; i++) {
|
for (var i = 0; i < vertices.length; i++) {
|
||||||
final edge = getEdge(i, vertices: vertices);
|
final edge = getEdge(i, vertices: vertices);
|
||||||
final isOutside = (edge.to.x - edge.from.x) * (point.y - edge.from.y) -
|
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.
|
/// is null return all vertices as [LineSegment]s.
|
||||||
List<LineSegment> possibleIntersectionVertices(Rect? rect) {
|
List<LineSegment> possibleIntersectionVertices(Rect? rect) {
|
||||||
final rectIntersections = <LineSegment>[];
|
final rectIntersections = <LineSegment>[];
|
||||||
final vertices = hitbox();
|
final vertices = globalVertices();
|
||||||
for (var i = 0; i < vertices.length; i++) {
|
for (var i = 0; i < vertices.length; i++) {
|
||||||
final edge = getEdge(i, vertices: vertices);
|
final edge = getEdge(i, vertices: vertices);
|
||||||
if (rect?.intersectsSegment(edge.from, edge.to) ?? true) {
|
if (rect?.intersectsSegment(edge.from, edge.to) ?? true) {
|
||||||
@ -177,21 +227,28 @@ class Polygon extends Shape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LineSegment getEdge(int i, {required List<Vector2> vertices}) {
|
LineSegment getEdge(int i, {required List<Vector2> vertices}) {
|
||||||
return LineSegment(
|
_lineSegments[i].from.setFrom(getVertex(i, vertices: vertices));
|
||||||
getVertex(i, vertices: vertices),
|
_lineSegments[i].to.setFrom(getVertex(i + 1, vertices: vertices));
|
||||||
getVertex(
|
return _lineSegments[i];
|
||||||
i + 1,
|
|
||||||
vertices: vertices,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector2 getVertex(int i, {List<Vector2>? vertices}) {
|
Vector2 getVertex(int i, {List<Vector2>? vertices}) {
|
||||||
vertices ??= hitbox();
|
vertices ??= globalVertices();
|
||||||
return vertices[i % vertices.length];
|
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 {
|
class HitboxPolygon extends Polygon with HitboxShape {
|
||||||
HitboxPolygon(List<Vector2> definition) : super.fromDefinition(definition);
|
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> _halfSizeCache = ValueCache();
|
||||||
final ValueCache<Vector2> _localCenterCache = ValueCache();
|
final ValueCache<Vector2> _localCenterCache = ValueCache();
|
||||||
final ValueCache<Vector2> _absoluteCenterCache = 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 the center of that [offsetPosition] and [relativeOffset]
|
||||||
/// should be calculated from, if they are not set this is the center of the
|
/// 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]
|
/// The size is the bounding box of the [Shape]
|
||||||
Vector2 size;
|
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 {
|
Vector2 get halfSize {
|
||||||
if (!_halfSizeCache.isCacheValid([size])) {
|
if (!_halfSizeCache.isCacheValid([size])) {
|
||||||
_halfSizeCache.updateCache(size / 2, [size.clone()]);
|
_halfSizeCache.updateCache(size / 2, [size.clone()]);
|
||||||
@ -41,11 +49,19 @@ abstract class Shape {
|
|||||||
Vector2 relativeOffset = Vector2.zero();
|
Vector2 relativeOffset = Vector2.zero();
|
||||||
|
|
||||||
/// The [relativeOffset] converted to a length vector
|
/// 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
|
/// The angle of the parent that has to be taken into consideration for some
|
||||||
/// applications of [Shape], for example [HitboxShape]
|
/// applications of [Shape], for example [HitboxShape]
|
||||||
double parentAngle;
|
double parentAngle = 0;
|
||||||
|
|
||||||
/// Whether the context that the shape is in has already prepared (rotated
|
/// Whether the context that the shape is in has already prepared (rotated
|
||||||
/// and translated) the canvas before coming to the shape's render method.
|
/// and translated) the canvas before coming to the shape's render method.
|
||||||
@ -103,7 +119,6 @@ abstract class Shape {
|
|||||||
Vector2? position,
|
Vector2? position,
|
||||||
Vector2? size,
|
Vector2? size,
|
||||||
this.angle = 0,
|
this.angle = 0,
|
||||||
this.parentAngle = 0,
|
|
||||||
}) : position = position ?? Vector2.zero(),
|
}) : position = position ?? Vector2.zero(),
|
||||||
size = size ?? Vector2.zero();
|
size = size ?? Vector2.zero();
|
||||||
|
|
||||||
@ -116,25 +131,22 @@ abstract class Shape {
|
|||||||
Set<Vector2> intersections(Shape other) {
|
Set<Vector2> intersections(Shape other) {
|
||||||
return intersection_system.intersections(this, 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 {
|
mixin HitboxShape on Shape {
|
||||||
late PositionComponent component;
|
late PositionComponent component;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Vector2 get size => component.scaledSize;
|
bool isCanvasPrepared = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double get parentAngle => component.angle;
|
Vector2 get size => component.size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Vector2 get scale => component.scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get parentAngle => component.absoluteAngle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Vector2 get position => component.absoluteCenter;
|
Vector2 get position => component.absoluteCenter;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/geometry.dart';
|
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flame_test/flame_test.dart';
|
import 'package:flame_test/flame_test.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -14,7 +13,7 @@ void main() {
|
|||||||
group('JoystickDirection tests', () {
|
group('JoystickDirection tests', () {
|
||||||
test('Can convert angle to JoystickDirection', () {
|
test('Can convert angle to JoystickDirection', () {
|
||||||
final joystick = JoystickComponent(
|
final joystick = JoystickComponent(
|
||||||
knob: Circle(radius: 5.0).toComponent(),
|
knob: CircleComponent(radius: 5.0),
|
||||||
size: 20,
|
size: 20,
|
||||||
margin: const EdgeInsets.only(left: 20, bottom: 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',
|
'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 {
|
(game) async {
|
||||||
final joystick = JoystickComponent(
|
final joystick = JoystickComponent(
|
||||||
knob: Circle(radius: 5.0).toComponent(),
|
knob: CircleComponent(radius: 5.0),
|
||||||
size: 20,
|
size: 20,
|
||||||
margin: const EdgeInsets.only(left: 20, top: 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. '
|
const message = '"prepare/add" called before the game is ready. '
|
||||||
'Did you try to access it on the Game constructor? '
|
'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(
|
expect(
|
||||||
() => game.add(component),
|
() => game.add(component),
|
||||||
|
|||||||
@ -77,19 +77,3 @@ class SkillsAnimationComponent extends RiveComponent with Tappable {
|
|||||||
return true;
|
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