mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-31 00:48:47 +08:00
Fix collision detection and rendering of local shape angles (#773)
* Fix collision detection with anchor other than center * Fix rotation around anchor * Simplify advanced collision detection example * Add some tests * Simplify multiple shapes example more * Move shapeCenter logic into Shape * Render center point * More debugging in MultipleShapes * Wtf. * Re-add "possibly" calculation * Rotate shape around parent center * Only consider the parent center * Format multiple shapes example * Add simple shapes example * Add caching in polygon * Fix rendering of polygon shapes * Remove print * Add changelog entry * Fix analyze complaints * Remove all shapes that contain the pressed point * Take zoom into consideration in multiple shapes example * Remove useless import * map instead of generate * Fix position component test * Simpler negative vector2 * "Correct" format * Add ShapeComponent instead of camera aware shapes * Fix formatting * Remove zoom from collision detection example * No need for gameRef in MultipleShapes example * Fix naming in only_shapes
This commit is contained in:
@ -4,6 +4,7 @@ import 'package:flame/game.dart';
|
||||
import '../../commons/commons.dart';
|
||||
import 'circles.dart';
|
||||
import 'multiple_shapes.dart';
|
||||
import 'only_shapes.dart';
|
||||
|
||||
void addCollisionDetectionStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Collision Detection')
|
||||
@ -16,5 +17,10 @@ void addCollisionDetectionStories(Dashbook dashbook) {
|
||||
'Multiple shapes',
|
||||
(_) => GameWidget(game: MultipleShapes()),
|
||||
codeLink: baseLink('collision_detection/multiple_shapes.dart'),
|
||||
)
|
||||
..add(
|
||||
'Shapes without components',
|
||||
(_) => GameWidget(game: OnlyShapes()),
|
||||
codeLink: baseLink('collision_detection/only_shapes.dart'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,9 +19,16 @@ abstract class MyCollidable extends PositionComponent
|
||||
double angleDelta = 0;
|
||||
bool _isDragged = false;
|
||||
final _activePaint = Paint()..color = Colors.amber;
|
||||
double _wallHitTime = double.infinity;
|
||||
late final Color _defaultDebugColor = debugColor;
|
||||
bool _isHit = false;
|
||||
final ScreenCollidable screenCollidable;
|
||||
|
||||
MyCollidable(Vector2 position, Vector2 size, this.velocity) {
|
||||
MyCollidable(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
this.velocity,
|
||||
this.screenCollidable,
|
||||
) {
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
anchor = Anchor.center;
|
||||
@ -33,51 +40,56 @@ abstract class MyCollidable extends PositionComponent
|
||||
if (_isDragged) {
|
||||
return;
|
||||
}
|
||||
_wallHitTime += dt;
|
||||
if (!_isHit) {
|
||||
debugColor = _defaultDebugColor;
|
||||
} else {
|
||||
_isHit = false;
|
||||
}
|
||||
delta.setFrom(velocity * dt);
|
||||
position.add(delta);
|
||||
angleDelta = dt * rotationSpeed;
|
||||
angle = (angle + angleDelta) % (2 * pi);
|
||||
// Takes rotation into consideration (which topLeftPosition doesn't)
|
||||
final topLeft = absoluteCenter - (size / 2);
|
||||
if (topLeft.x + size.x < 0 ||
|
||||
topLeft.y + size.y < 0 ||
|
||||
topLeft.x > screenCollidable.size.x ||
|
||||
topLeft.y > screenCollidable.size.y) {
|
||||
final moduloSize = screenCollidable.size + size;
|
||||
topLeftPosition = topLeftPosition % moduloSize;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
renderShapes(canvas);
|
||||
final localCenter = (size / 2).toOffset();
|
||||
if (_isDragged) {
|
||||
final localCenter = (size / 2).toOffset();
|
||||
canvas.drawCircle(localCenter, 5, _activePaint);
|
||||
}
|
||||
if (_wallHitTime < 1.0) {
|
||||
// Show a rectangle in the center for a second if we hit the wall
|
||||
canvas.drawRect(
|
||||
Rect.fromCenter(center: localCenter, width: 10, height: 10),
|
||||
debugPaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
|
||||
final averageIntersection = intersectionPoints.reduce((sum, v) => sum + v) /
|
||||
intersectionPoints.length.toDouble();
|
||||
final collisionDirection = (averageIntersection - absoluteCenter)
|
||||
..normalize()
|
||||
..round();
|
||||
if (velocity.angleToSigned(collisionDirection).abs() > 3) {
|
||||
// This entity got hit by something else
|
||||
return;
|
||||
}
|
||||
final angleToCollision = velocity.angleToSigned(collisionDirection);
|
||||
if (angleToCollision.abs() < pi / 8) {
|
||||
velocity.rotate(pi);
|
||||
} else {
|
||||
velocity.rotate(-pi / 2 * angleToCollision.sign);
|
||||
}
|
||||
position.sub(delta * 2);
|
||||
angle = (angle - angleDelta) % (2 * pi);
|
||||
if (other is ScreenCollidable) {
|
||||
_wallHitTime = 0;
|
||||
_isHit = true;
|
||||
switch (other.runtimeType) {
|
||||
case ScreenCollidable:
|
||||
debugColor = Colors.teal;
|
||||
break;
|
||||
case CollidablePolygon:
|
||||
debugColor = Colors.blue;
|
||||
break;
|
||||
case CollidableCircle:
|
||||
debugColor = Colors.green;
|
||||
break;
|
||||
case CollidableRectangle:
|
||||
debugColor = Colors.cyan;
|
||||
break;
|
||||
case CollidableSnowman:
|
||||
debugColor = Colors.amber;
|
||||
break;
|
||||
default:
|
||||
debugColor = Colors.pink;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,8 +108,12 @@ abstract class MyCollidable extends PositionComponent
|
||||
}
|
||||
|
||||
class CollidablePolygon extends MyCollidable {
|
||||
CollidablePolygon(Vector2 position, Vector2 size, Vector2 velocity)
|
||||
: super(position, size, velocity) {
|
||||
CollidablePolygon(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Vector2 velocity,
|
||||
ScreenCollidable screenCollidable,
|
||||
) : super(position, size, velocity, screenCollidable) {
|
||||
final shape = HitboxPolygon([
|
||||
Vector2(-1.0, 0.0),
|
||||
Vector2(-0.8, 0.6),
|
||||
@ -113,15 +129,23 @@ class CollidablePolygon extends MyCollidable {
|
||||
}
|
||||
|
||||
class CollidableRectangle extends MyCollidable {
|
||||
CollidableRectangle(Vector2 position, Vector2 size, Vector2 velocity)
|
||||
: super(position, size, velocity) {
|
||||
CollidableRectangle(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Vector2 velocity,
|
||||
ScreenCollidable screenCollidable,
|
||||
) : super(position, size, velocity, screenCollidable) {
|
||||
addShape(HitboxRectangle());
|
||||
}
|
||||
}
|
||||
|
||||
class CollidableCircle extends MyCollidable {
|
||||
CollidableCircle(Vector2 position, Vector2 size, Vector2 velocity)
|
||||
: super(position, size, velocity) {
|
||||
CollidableCircle(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Vector2 velocity,
|
||||
ScreenCollidable screenCollidable,
|
||||
) : super(position, size, velocity, screenCollidable) {
|
||||
final shape = HitboxCircle();
|
||||
addShape(shape);
|
||||
}
|
||||
@ -134,9 +158,9 @@ class SnowmanPart extends HitboxCircle {
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
SnowmanPart(double definition, Vector2 relativePosition, Color hitColor)
|
||||
SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor)
|
||||
: super(definition: definition) {
|
||||
this.relativePosition.setFrom(relativePosition);
|
||||
this.relativeOffset.setFrom(relativeOffset);
|
||||
onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) {
|
||||
if (other.component is ScreenCollidable) {
|
||||
hitPaint..color = startColor;
|
||||
@ -147,15 +171,20 @@ class SnowmanPart extends HitboxCircle {
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas, Paint paint) {
|
||||
void render(Canvas canvas, _) {
|
||||
super.render(canvas, hitPaint);
|
||||
}
|
||||
}
|
||||
|
||||
class CollidableSnowman extends MyCollidable {
|
||||
CollidableSnowman(Vector2 position, Vector2 size, Vector2 velocity)
|
||||
: super(position, size, velocity) {
|
||||
rotationSpeed = 0.2;
|
||||
CollidableSnowman(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
Vector2 velocity,
|
||||
ScreenCollidable screenCollidable,
|
||||
) : super(position, size, velocity, screenCollidable) {
|
||||
rotationSpeed = 0.3;
|
||||
anchor = Anchor.topLeft;
|
||||
final top = SnowmanPart(0.4, Vector2(0, -0.8), Colors.red);
|
||||
final middle = SnowmanPart(0.6, Vector2(0, -0.3), Colors.yellow);
|
||||
final bottom = SnowmanPart(1.0, Vector2(0, 0.5), Colors.green);
|
||||
@ -167,6 +196,9 @@ class CollidableSnowman extends MyCollidable {
|
||||
|
||||
class MultipleShapes extends BaseGame
|
||||
with HasCollidables, HasDraggableComponents {
|
||||
@override
|
||||
bool debugMode = true;
|
||||
|
||||
final TextPaint fpsTextPaint = TextPaint(
|
||||
config: TextPaintConfig(
|
||||
color: BasicPalette.white.color,
|
||||
@ -175,21 +207,25 @@ class MultipleShapes extends BaseGame
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final screen = ScreenCollidable();
|
||||
await super.onLoad();
|
||||
final screenCollidable = ScreenCollidable();
|
||||
final snowman = CollidableSnowman(
|
||||
Vector2.all(150),
|
||||
Vector2(100, 200),
|
||||
Vector2(-100, 100),
|
||||
screenCollidable,
|
||||
);
|
||||
MyCollidable lastToAdd = snowman;
|
||||
add(screen);
|
||||
add(screenCollidable);
|
||||
add(snowman);
|
||||
var totalAdded = 1;
|
||||
while (totalAdded < 20) {
|
||||
lastToAdd = createRandomCollidable(lastToAdd);
|
||||
while (totalAdded < 10) {
|
||||
lastToAdd = createRandomCollidable(lastToAdd, screenCollidable);
|
||||
final lastBottomRight =
|
||||
lastToAdd.toAbsoluteRect().bottomRight.toVector2();
|
||||
if (screen.containsPoint(lastBottomRight)) {
|
||||
final screenSize = size / camera.zoom;
|
||||
if (lastBottomRight.x < screenSize.x &&
|
||||
lastBottomRight.y < screenSize.y) {
|
||||
add(lastToAdd);
|
||||
totalAdded++;
|
||||
} else {
|
||||
@ -201,7 +237,10 @@ class MultipleShapes extends BaseGame
|
||||
final _rng = Random();
|
||||
final _distance = Vector2(100, 0);
|
||||
|
||||
MyCollidable createRandomCollidable(MyCollidable lastCollidable) {
|
||||
MyCollidable createRandomCollidable(
|
||||
MyCollidable lastCollidable,
|
||||
ScreenCollidable screen,
|
||||
) {
|
||||
final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100;
|
||||
final isXOverflow = lastCollidable.position.x +
|
||||
lastCollidable.size.x / 2 +
|
||||
@ -213,17 +252,18 @@ class MultipleShapes extends BaseGame
|
||||
position = (lastCollidable.position + _distance)
|
||||
..x += collidableSize.x / 2;
|
||||
}
|
||||
final velocity = Vector2.random(_rng) * 200;
|
||||
final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 400;
|
||||
final rotationSpeed = 0.5 - _rng.nextDouble();
|
||||
final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)];
|
||||
switch (shapeType) {
|
||||
case Shapes.circle:
|
||||
return CollidableCircle(position, collidableSize, velocity);
|
||||
return CollidableCircle(position, collidableSize, velocity, screen)
|
||||
..rotationSpeed = rotationSpeed;
|
||||
case Shapes.rectangle:
|
||||
return CollidableRectangle(position, collidableSize, velocity)
|
||||
return CollidableRectangle(position, collidableSize, velocity, screen)
|
||||
..rotationSpeed = rotationSpeed;
|
||||
case Shapes.polygon:
|
||||
return CollidablePolygon(position, collidableSize, velocity)
|
||||
return CollidablePolygon(position, collidableSize, velocity, screen)
|
||||
..rotationSpeed = rotationSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
63
examples/lib/stories/collision_detection/only_shapes.dart
Normal file
63
examples/lib/stories/collision_detection/only_shapes.dart
Normal file
@ -0,0 +1,63 @@
|
||||
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/gestures.dart';
|
||||
import 'package:flame/palette.dart';
|
||||
import 'package:flutter/material.dart' hide Image, Draggable;
|
||||
|
||||
enum Shapes { circle, rectangle, polygon }
|
||||
|
||||
class OnlyShapes extends BaseGame with HasTapableComponents {
|
||||
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 event) {
|
||||
super.onTapDown(pointerId, event);
|
||||
final tapDownPoint = event.eventPosition.game;
|
||||
final component = MyShapeComponent(randomShape(tapDownPoint), shapePaint);
|
||||
add(component);
|
||||
}
|
||||
}
|
||||
|
||||
class MyShapeComponent extends ShapeComponent with Tapable {
|
||||
MyShapeComponent(Shape shape, Paint shapePaint) : super(shape, shapePaint);
|
||||
|
||||
@override
|
||||
bool onTapDown(TapDownInfo event) {
|
||||
remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,8 @@
|
||||
- Abstracting the text api to allow custom text renderers on the framework
|
||||
- Set the same debug mode for children as for the parent when added
|
||||
- Fix camera projections when camera is zoomed
|
||||
- Fix collision detection system with angle and parentAngle
|
||||
- Fix rendering of shapes that aren't HitboxShape
|
||||
|
||||
## [1.0.0-rc9]
|
||||
- Fix input bug with other anchors than center
|
||||
|
||||
@ -14,6 +14,7 @@ export 'src/components/nine_tile_box_component.dart';
|
||||
export 'src/components/parallax_component.dart';
|
||||
export 'src/components/particle_component.dart';
|
||||
export 'src/components/position_component.dart';
|
||||
export 'src/components/shape_component.dart';
|
||||
export 'src/components/sprite_animation_component.dart';
|
||||
export 'src/components/sprite_animation_group_component.dart';
|
||||
export 'src/components/sprite_batch_component.dart';
|
||||
|
||||
@ -55,7 +55,12 @@ class Anchor {
|
||||
Anchor otherAnchor,
|
||||
Vector2 size,
|
||||
) {
|
||||
return position + ((otherAnchor.toVector2() - toVector2())..multiply(size));
|
||||
if (this == otherAnchor) {
|
||||
return position;
|
||||
} else {
|
||||
return position +
|
||||
((otherAnchor.toVector2() - toVector2())..multiply(size));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a string representation of this Anchor.
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import '../../../components.dart';
|
||||
import '../../../game.dart';
|
||||
import '../../components/position_component.dart';
|
||||
import '../../extensions/vector2.dart';
|
||||
import '../../geometry/rectangle.dart';
|
||||
@ -18,17 +20,33 @@ mixin Collidable on Hitbox {
|
||||
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {}
|
||||
}
|
||||
|
||||
class ScreenCollidable extends PositionComponent with Hitbox, Collidable {
|
||||
class ScreenCollidable extends PositionComponent
|
||||
with Hitbox, Collidable, HasGameRef<BaseGame> {
|
||||
@override
|
||||
CollidableType collidableType = CollidableType.passive;
|
||||
|
||||
ScreenCollidable() {
|
||||
final Vector2 _effectiveSize = Vector2.zero();
|
||||
double _zoom = 1.0;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
_updateSize();
|
||||
addShape(HitboxRectangle());
|
||||
}
|
||||
|
||||
@override
|
||||
void onGameResize(Vector2 gameSize) {
|
||||
super.onGameResize(gameSize);
|
||||
size = gameSize;
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
_updateSize();
|
||||
}
|
||||
|
||||
void _updateSize() {
|
||||
if (_effectiveSize != gameRef.viewport.effectiveSize ||
|
||||
_zoom != gameRef.camera.zoom) {
|
||||
_effectiveSize.setFrom(gameRef.viewport.effectiveSize);
|
||||
_zoom = gameRef.camera.zoom;
|
||||
size = _effectiveSize / _zoom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,8 +34,9 @@ mixin Hitbox on PositionComponent {
|
||||
/// check can be done first to see if it even is possible that the shapes can
|
||||
/// overlap, since the shapes have to be within the size of the component.
|
||||
bool possiblyOverlapping(Hitbox other) {
|
||||
return other.center.distanceToSquared(center) <=
|
||||
other.size.length2 + size.length2;
|
||||
final maxDistance = other.size.length + size.length;
|
||||
return other.absoluteCenter.distanceToSquared(absoluteCenter) <=
|
||||
maxDistance * maxDistance;
|
||||
}
|
||||
|
||||
/// Since this is a cheaper calculation than checking towards all shapes this
|
||||
@ -43,6 +44,6 @@ mixin Hitbox on PositionComponent {
|
||||
/// contain the point, since the shapes have to be within the size of the
|
||||
/// component.
|
||||
bool possiblyContainsPoint(Vector2 point) {
|
||||
return center.distanceToSquared(point) <= size.length2;
|
||||
return absoluteCenter.distanceToSquared(point) <= size.length2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,14 +88,17 @@ abstract class PositionComponent extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the position of the center of the component's bounding rectangle without rotation
|
||||
/// Get the position of the center of the component's bounding rectangle
|
||||
Vector2 get center {
|
||||
return anchor == Anchor.center
|
||||
? position
|
||||
: anchor.toOtherAnchorPosition(position, Anchor.center, size);
|
||||
if (anchor == Anchor.center) {
|
||||
return position;
|
||||
} else {
|
||||
return anchor.toOtherAnchorPosition(position, Anchor.center, size)
|
||||
..rotate(angle, center: absolutePosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the absolute center of the component without rotation
|
||||
/// Get the absolute center of the component
|
||||
Vector2 get absoluteCenter => absoluteParentPosition + center;
|
||||
|
||||
/// Angle (with respect to the x-axis) this component should be rendered with.
|
||||
@ -142,7 +145,7 @@ abstract class PositionComponent extends BaseComponent {
|
||||
@override
|
||||
bool containsPoint(Vector2 point) {
|
||||
final rectangle = Rectangle.fromRect(toAbsoluteRect(), angle: angle)
|
||||
..anchorPosition = absolutePosition;
|
||||
..position = absoluteCenter;
|
||||
return rectangle.containsPoint(point);
|
||||
}
|
||||
|
||||
|
||||
30
packages/flame/lib/src/components/shape_component.dart
Normal file
30
packages/flame/lib/src/components/shape_component.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'dart:ui' hide Offset;
|
||||
|
||||
import '../../components.dart';
|
||||
import '../../geometry.dart';
|
||||
import '../anchor.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
|
||||
class ShapeComponent extends PositionComponent {
|
||||
final Shape shape;
|
||||
final Paint shapePaint;
|
||||
|
||||
ShapeComponent(
|
||||
this.shape,
|
||||
this.shapePaint,
|
||||
) : super(
|
||||
position: shape.position,
|
||||
size: shape.size,
|
||||
angle: shape.angle,
|
||||
anchor: Anchor.center,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
super.render(canvas);
|
||||
shape.render(canvas, shapePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool containsPoint(Vector2 point) => shape.containsPoint(point);
|
||||
}
|
||||
@ -34,9 +34,19 @@ extension Vector2Extension on Vector2 {
|
||||
setFrom(this + (to - this) * t);
|
||||
}
|
||||
|
||||
/// Whether the [Vector2] is the zero vector or not
|
||||
bool isZero() => x == 0 && y == 0;
|
||||
|
||||
/// 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
|
||||
/// a clockwise fashion
|
||||
/// In a normal coordinate system it rotates in a counter-clockwise fashion
|
||||
void rotate(double angle, {Vector2? center}) {
|
||||
if (isZero() || angle == 0) {
|
||||
// No point in rotating the zero vector or to rotate with 0 as angle
|
||||
return;
|
||||
}
|
||||
if (center == null) {
|
||||
setValues(
|
||||
x * cos(angle) - y * sin(angle),
|
||||
|
||||
@ -120,8 +120,8 @@ class Camera extends Projector {
|
||||
/// add any non-smooth movement.
|
||||
Rect? worldBounds;
|
||||
|
||||
/// If set, the camera will zoom by this ratio. This can be greater than 1 (zoom in)
|
||||
/// or smaller (zoom out), but should always be greater than zero.
|
||||
/// If set, the camera will zoom by this ratio. This can be greater than 1
|
||||
/// (zoom in) or smaller (zoom out), but should always be greater than zero.
|
||||
///
|
||||
/// Note: do not confuse this with the zoom applied by the viewport. The
|
||||
/// viewport applies a (normally) fixed zoom to adapt multiple screens into
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import '../../game.dart';
|
||||
import '../../geometry.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import 'shape.dart';
|
||||
@ -33,23 +34,19 @@ class Circle extends Shape {
|
||||
double? angle,
|
||||
}) : super(position: position, size: size, angle: angle ?? 0);
|
||||
|
||||
double get radius => (min(size!.x, size!.y) / 2) * normalizedRadius;
|
||||
double get radius => (min(size.x, size.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.
|
||||
@override
|
||||
void render(Canvas canvas, Paint paint) {
|
||||
final localPosition = size! / 2 + position;
|
||||
final localRelativePosition = (size! / 2)..multiply(relativePosition);
|
||||
canvas.drawCircle(
|
||||
(localPosition + localRelativePosition).toOffset(),
|
||||
radius,
|
||||
paint,
|
||||
);
|
||||
canvas.drawCircle(localCenter.toOffset(), radius, paint);
|
||||
}
|
||||
|
||||
/// Checks whether the represented circle contains the [point].
|
||||
@override
|
||||
bool containsPoint(Vector2 point) {
|
||||
return shapeCenter.distanceToSquared(point) < radius * radius;
|
||||
return absoluteCenter.distanceToSquared(point) < radius * radius;
|
||||
}
|
||||
|
||||
/// Returns the locus of points in which the provided line segment intersect
|
||||
@ -63,8 +60,8 @@ class Circle extends Shape {
|
||||
}) {
|
||||
double sq(double x) => pow(x, 2).toDouble();
|
||||
|
||||
final cx = shapeCenter.x;
|
||||
final cy = shapeCenter.y;
|
||||
final cx = absoluteCenter.x;
|
||||
final cy = absoluteCenter.y;
|
||||
|
||||
final point1 = line.from;
|
||||
final point2 = line.to;
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'dart:ui' hide Canvas;
|
||||
|
||||
import '../../game.dart';
|
||||
import '../../geometry.dart';
|
||||
import '../extensions/canvas.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;
|
||||
|
||||
/// With this constructor you create your [Polygon] from positions in your
|
||||
/// intended space. It will automatically calculate the [size] and center
|
||||
@ -51,17 +57,27 @@ class Polygon extends Shape {
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
double? angle,
|
||||
}) : super(position: position, size: size, angle: angle ?? 0);
|
||||
}) : super(
|
||||
position: position,
|
||||
size: size,
|
||||
angle: angle ?? 0,
|
||||
) {
|
||||
_sizedVertices =
|
||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
||||
_hitboxVertices =
|
||||
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
|
||||
}
|
||||
|
||||
final _cachedScaledShape = ShapeCache<Iterable<Vector2>>();
|
||||
|
||||
/// Gives back the shape vectors multiplied by the size
|
||||
Iterable<Vector2> scaled() {
|
||||
if (!_cachedScaledShape.isCacheValid([size])) {
|
||||
_cachedScaledShape.updateCache(
|
||||
normalizedVertices.map((p) => p.clone()..multiply(size! / 2)),
|
||||
[size!.clone()],
|
||||
);
|
||||
for (var i = 0; i < _sizedVertices.length; i++) {
|
||||
final point = normalizedVertices[i];
|
||||
(_sizedVertices[i]..setFrom(point)).multiply(halfSize);
|
||||
}
|
||||
_cachedScaledShape.updateCache(_sizedVertices, [size.clone()]);
|
||||
}
|
||||
return _cachedScaledShape.value!;
|
||||
}
|
||||
@ -70,21 +86,25 @@ class Polygon extends Shape {
|
||||
|
||||
@override
|
||||
void render(Canvas canvas, Paint paint) {
|
||||
if (!_cachedRenderPath.isCacheValid([position, size])) {
|
||||
if (!_cachedRenderPath
|
||||
.isCacheValid([offsetPosition, relativeOffset, size, angle])) {
|
||||
final center = localCenter;
|
||||
_cachedRenderPath.updateCache(
|
||||
Path()
|
||||
..addPolygon(
|
||||
scaled()
|
||||
.map((point) => (point +
|
||||
(position + size! / 2) +
|
||||
((size! / 2)..multiply(relativePosition)))
|
||||
.toOffset())
|
||||
.map(
|
||||
(point) => ((center + point)..rotate(angle, center: center))
|
||||
.toOffset(),
|
||||
)
|
||||
.toList(),
|
||||
true,
|
||||
),
|
||||
[
|
||||
position.clone(),
|
||||
size!.clone(),
|
||||
offsetPosition.clone(),
|
||||
relativeOffset.clone(),
|
||||
size.clone(),
|
||||
angle,
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -97,13 +117,19 @@ class Polygon extends Shape {
|
||||
/// 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([shapeCenter, size, angle])) {
|
||||
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(
|
||||
scaled()
|
||||
.map((point) =>
|
||||
(point + shapeCenter)..rotate(angle, center: anchorPosition))
|
||||
.toList(growable: false),
|
||||
[shapeCenter, size!.clone(), angle],
|
||||
_hitboxVertices,
|
||||
[absoluteCenter, size.clone(), parentAngle, angle],
|
||||
);
|
||||
}
|
||||
return _cachedHitbox.value!;
|
||||
@ -114,7 +140,7 @@ class Polygon extends Shape {
|
||||
@override
|
||||
bool containsPoint(Vector2 point) {
|
||||
// If the size is 0 then it can't contain any points
|
||||
if (size!.x == 0 || size!.y == 0) {
|
||||
if (size.x == 0 || size.y == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import '../../extensions.dart';
|
||||
import '../../game.dart';
|
||||
import '../../geometry.dart';
|
||||
import 'shape.dart';
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import '../../components.dart';
|
||||
import '../../game.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import 'shape_intersections.dart' as intersection_system;
|
||||
|
||||
@ -9,34 +10,100 @@ import 'shape_intersections.dart' as intersection_system;
|
||||
/// center.
|
||||
/// A point can be determined to be within of outside of a shape.
|
||||
abstract class Shape {
|
||||
/// The position of your shape, it is up to you how you treat this
|
||||
Vector2 position;
|
||||
final ShapeCache<Vector2> _halfSizeCache = ShapeCache();
|
||||
final ShapeCache<Vector2> _localCenterCache = ShapeCache();
|
||||
final ShapeCache<Vector2> _absoluteCenterCache = ShapeCache();
|
||||
|
||||
/// The position of your shape in relation to its size
|
||||
Vector2 relativePosition = Vector2.zero();
|
||||
/// Should be the center of that [offsetPosition] and [relativeOffset]
|
||||
/// should be calculated from, if they are not set this is the center of the
|
||||
/// shape
|
||||
Vector2 position = Vector2.zero();
|
||||
|
||||
/// The size is the bounding box of the [Shape]
|
||||
Vector2? size;
|
||||
Vector2 size;
|
||||
|
||||
Vector2 get halfSize {
|
||||
if (!_halfSizeCache.isCacheValid([size])) {
|
||||
_halfSizeCache.updateCache(size / 2, [size.clone()]);
|
||||
}
|
||||
return _halfSizeCache.value!;
|
||||
}
|
||||
|
||||
/// The angle of the shape from its initial definition
|
||||
double angle;
|
||||
|
||||
Vector2 get shapeCenter => position;
|
||||
/// The local position of your shape, so the diff from the [position] of the
|
||||
/// shape
|
||||
Vector2 offsetPosition = Vector2.zero();
|
||||
|
||||
Vector2? _anchorPosition;
|
||||
Vector2 get anchorPosition => _anchorPosition ?? position;
|
||||
set anchorPosition(Vector2 position) => _anchorPosition = position;
|
||||
/// The position of your shape in relation to its size from (-1,-1) to (1,1)
|
||||
Vector2 relativeOffset = Vector2.zero();
|
||||
|
||||
/// The [relativeOffset] converted to a length vector
|
||||
Vector2 get relativePosition => (size / 2)..multiply(relativeOffset);
|
||||
|
||||
/// The angle of the parent that has to be taken into consideration for some
|
||||
/// applications of [Shape], for example [HitboxShape]
|
||||
double parentAngle;
|
||||
|
||||
/// The center position of the shape within itself, without rotation
|
||||
Vector2 get localCenter {
|
||||
final stateValues = [
|
||||
size,
|
||||
relativeOffset,
|
||||
offsetPosition,
|
||||
];
|
||||
if (!_localCenterCache.isCacheValid(stateValues)) {
|
||||
final center = (size / 2)..add(relativePosition)..add(offsetPosition);
|
||||
_localCenterCache.updateCache(
|
||||
center,
|
||||
stateValues.map((e) => e.clone()).toList(growable: false),
|
||||
);
|
||||
}
|
||||
return _localCenterCache.value!;
|
||||
}
|
||||
|
||||
/// The shape's absolute center with rotation taken into account
|
||||
Vector2 get absoluteCenter {
|
||||
final stateValues = [
|
||||
position,
|
||||
offsetPosition,
|
||||
relativeOffset,
|
||||
angle,
|
||||
parentAngle,
|
||||
];
|
||||
if (!_absoluteCenterCache.isCacheValid(stateValues)) {
|
||||
/// The center of the shape, before any rotation
|
||||
final center = position + offsetPosition;
|
||||
if (!relativeOffset.isZero()) {
|
||||
center.add(relativePosition);
|
||||
}
|
||||
if (angle != 0 || parentAngle != 0) {
|
||||
center.rotate(parentAngle + angle, center: position);
|
||||
}
|
||||
_absoluteCenterCache.updateCache(center, [
|
||||
position.clone(),
|
||||
offsetPosition.clone(),
|
||||
relativeOffset.clone(),
|
||||
angle,
|
||||
parentAngle,
|
||||
]);
|
||||
}
|
||||
return _absoluteCenterCache.value!;
|
||||
}
|
||||
|
||||
Shape({
|
||||
Vector2? position,
|
||||
this.size,
|
||||
Vector2? size,
|
||||
this.angle = 0,
|
||||
}) : position = position ?? Vector2.zero();
|
||||
this.parentAngle = 0,
|
||||
}) : position = position ?? Vector2.zero(),
|
||||
size = size ?? Vector2.zero();
|
||||
|
||||
/// Whether the point [p] is within the shapes boundaries or not
|
||||
bool containsPoint(Vector2 p);
|
||||
|
||||
void render(Canvas c, Paint paint);
|
||||
void render(Canvas canvas, Paint paint);
|
||||
|
||||
/// Where this Shape has intersection points with another shape
|
||||
Set<Vector2> intersections(Shape other) {
|
||||
@ -47,23 +114,14 @@ abstract class Shape {
|
||||
mixin HitboxShape on Shape {
|
||||
late PositionComponent component;
|
||||
|
||||
@override
|
||||
Vector2 get anchorPosition => component.absolutePosition;
|
||||
|
||||
@override
|
||||
Vector2 get size => component.size;
|
||||
|
||||
@override
|
||||
double get angle => component.angle;
|
||||
double get parentAngle => component.angle;
|
||||
|
||||
/// The shape's absolute center
|
||||
@override
|
||||
Vector2 get shapeCenter {
|
||||
return component.absoluteCenter +
|
||||
position +
|
||||
((size / 2)..multiply(relativePosition))
|
||||
..rotate(angle, center: anchorPosition);
|
||||
}
|
||||
Vector2 get position => component.absoluteCenter;
|
||||
|
||||
/// Assign your own [CollisionCallback] if you want a callback when this
|
||||
/// shape collides with another [HitboxShape]
|
||||
|
||||
@ -75,7 +75,7 @@ class CirclePolygonIntersections extends Intersections<Circle, Polygon> {
|
||||
class CircleCircleIntersections extends Intersections<Circle, Circle> {
|
||||
@override
|
||||
Set<Vector2> intersect(Circle shapeA, Circle shapeB) {
|
||||
final distance = shapeA.shapeCenter.distanceTo(shapeB.shapeCenter);
|
||||
final distance = shapeA.absoluteCenter.distanceTo(shapeB.absoluteCenter);
|
||||
final radiusA = shapeA.radius;
|
||||
final radiusB = shapeB.radius;
|
||||
if (distance > radiusA + radiusB) {
|
||||
@ -91,10 +91,10 @@ class CircleCircleIntersections extends Intersections<Circle, Circle> {
|
||||
// infinite number of solutions. Since it is problematic to return a
|
||||
// set of infinite size, we'll return 4 distinct points here.
|
||||
return {
|
||||
shapeA.shapeCenter + Vector2(radiusA, 0),
|
||||
shapeA.shapeCenter + Vector2(0, -radiusA),
|
||||
shapeA.shapeCenter + Vector2(-radiusA, 0),
|
||||
shapeA.shapeCenter + Vector2(0, radiusA),
|
||||
shapeA.absoluteCenter + Vector2(radiusA, 0),
|
||||
shapeA.absoluteCenter + Vector2(0, -radiusA),
|
||||
shapeA.absoluteCenter + Vector2(-radiusA, 0),
|
||||
shapeA.absoluteCenter + Vector2(0, radiusA),
|
||||
};
|
||||
} else {
|
||||
/// There are definitely collision points if we end up in here.
|
||||
@ -115,14 +115,14 @@ class CircleCircleIntersections extends Intersections<Circle, Circle> {
|
||||
final lengthA = (pow(radiusA, 2) - pow(radiusB, 2) + pow(distance, 2)) /
|
||||
(2 * distance);
|
||||
final lengthB = sqrt((pow(radiusA, 2) - pow(lengthA, 2)).abs());
|
||||
final centerPoint = shapeA.shapeCenter +
|
||||
(shapeB.shapeCenter - shapeA.shapeCenter) * lengthA / distance;
|
||||
final centerPoint = shapeA.absoluteCenter +
|
||||
(shapeB.absoluteCenter - shapeA.absoluteCenter) * lengthA / distance;
|
||||
final delta = Vector2(
|
||||
lengthB *
|
||||
(shapeB.shapeCenter.y - shapeA.shapeCenter.y).abs() /
|
||||
(shapeB.absoluteCenter.y - shapeA.absoluteCenter.y).abs() /
|
||||
distance,
|
||||
-lengthB *
|
||||
(shapeB.shapeCenter.x - shapeA.shapeCenter.x).abs() /
|
||||
(shapeB.absoluteCenter.x - shapeA.absoluteCenter.x).abs() /
|
||||
distance,
|
||||
);
|
||||
return {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/src/anchor.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@ -26,5 +27,27 @@ void main() {
|
||||
throwsA(const TypeMatcher<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('can convert topLeft anchor to another anchor positions', () {
|
||||
final position = Vector2(3, 1);
|
||||
final size = Vector2(2, 3);
|
||||
final center = Anchor.topLeft.toOtherAnchorPosition(
|
||||
position,
|
||||
Anchor.center,
|
||||
size,
|
||||
);
|
||||
expect(center, position + size / 2);
|
||||
});
|
||||
|
||||
test('can convert center anchor to another anchor positions', () {
|
||||
final position = Vector2(3, 1);
|
||||
final size = Vector2(2, 3);
|
||||
final topLeft = Anchor.center.toOtherAnchorPosition(
|
||||
position,
|
||||
Anchor.topLeft,
|
||||
size,
|
||||
);
|
||||
expect(topLeft, position - size / 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -172,5 +172,61 @@ void main() {
|
||||
final point = Vector2(2.0, 2.0);
|
||||
expect(component.containsPoint(point), false);
|
||||
});
|
||||
|
||||
test('component with zero size does not contain point', () {
|
||||
final PositionComponent component = MyComponent();
|
||||
component.position.setValues(2.0, 2.0);
|
||||
component.size.setValues(0.0, 0.0);
|
||||
component.angle = 0.0;
|
||||
component.anchor = Anchor.center;
|
||||
|
||||
final point = Vector2(2.0, 2.0);
|
||||
expect(component.containsPoint(point), false);
|
||||
});
|
||||
|
||||
test('component with anchor center has the same center and position', () {
|
||||
final PositionComponent component = MyComponent();
|
||||
component.position.setValues(2.0, 1.0);
|
||||
component.size.setValues(3.0, 1.0);
|
||||
component.angle = 2.0;
|
||||
component.anchor = Anchor.center;
|
||||
|
||||
expect(component.center, component.position);
|
||||
expect(component.absoluteCenter, component.position);
|
||||
expect(
|
||||
component.topLeftPosition,
|
||||
component.position - component.size / 2,
|
||||
);
|
||||
});
|
||||
|
||||
test('component with anchor topLeft has the correct center', () {
|
||||
final PositionComponent component = MyComponent();
|
||||
component.position.setValues(2.0, 1.0);
|
||||
component.size.setValues(3.0, 1.0);
|
||||
component.angle = 0.0;
|
||||
component.anchor = Anchor.topLeft;
|
||||
|
||||
expect(component.center, component.position + component.size / 2);
|
||||
expect(component.absoluteCenter, component.position + component.size / 2);
|
||||
});
|
||||
|
||||
test('component with parent has the correct center', () {
|
||||
final PositionComponent parent = MyComponent();
|
||||
parent.position.setValues(2.0, 1.0);
|
||||
parent.anchor = Anchor.topLeft;
|
||||
final PositionComponent child = MyComponent();
|
||||
child.position.setValues(2.0, 1.0);
|
||||
child.size.setValues(3.0, 1.0);
|
||||
child.angle = 0.0;
|
||||
child.anchor = Anchor.topLeft;
|
||||
parent.addChild(child);
|
||||
|
||||
expect(child.absoluteTopLeftPosition, child.position + parent.position);
|
||||
expect(
|
||||
child.absoluteTopLeftPosition,
|
||||
child.topLeftPosition + parent.topLeftPosition,
|
||||
);
|
||||
expect(child.absoluteCenter, parent.position + child.center);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import 'dart:math' as math;
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../util/expect_vector2.dart';
|
||||
|
||||
void expectDouble(double d1, double d2) {
|
||||
expect((d1 - d2).abs() <= 0.0001, true);
|
||||
}
|
||||
@ -100,6 +102,7 @@ void main() {
|
||||
expectDouble(p2.length, math.sqrt(2));
|
||||
expect(p2.x, p2.y);
|
||||
});
|
||||
|
||||
test('moveToTarget - fully horizontal', () {
|
||||
final current = Vector2(10.0, 0.0);
|
||||
final target = Vector2(20.0, 0.0);
|
||||
@ -116,6 +119,7 @@ void main() {
|
||||
current.moveToTarget(target, 5);
|
||||
expect(current, Vector2(20.0, 0.0));
|
||||
});
|
||||
|
||||
test('moveToTarget - fully vertical', () {
|
||||
final current = Vector2(10.0, 0.0);
|
||||
final target = Vector2(10.0, 100.0);
|
||||
@ -132,6 +136,7 @@ void main() {
|
||||
current.moveToTarget(target, 19);
|
||||
expect(current, Vector2(10.0, 100.0));
|
||||
});
|
||||
|
||||
test('moveToTarget - arbitrary direction', () {
|
||||
final current = Vector2(2.0, 2.0);
|
||||
final target = Vector2(4.0, 6.0); // direction is 1,2
|
||||
@ -145,5 +150,52 @@ void main() {
|
||||
current.moveToTarget(target, math.sqrt(5));
|
||||
expect(current, Vector2(4.0, 6.0));
|
||||
});
|
||||
|
||||
test('rotate - no center defined', () {
|
||||
final position = Vector2(0.0, 1.0);
|
||||
position.rotate(-math.pi / 2);
|
||||
expectVector2(position, Vector2(1.0, 0.0));
|
||||
});
|
||||
|
||||
test('rotate - no center defined, negative position', () {
|
||||
final position = Vector2(0.0, -1.0);
|
||||
position.rotate(-math.pi / 2);
|
||||
expectVector2(position, Vector2(-1.0, 0.0));
|
||||
});
|
||||
|
||||
test('rotate - with center defined', () {
|
||||
final position = Vector2(0.0, 1.0);
|
||||
final center = Vector2(1.0, 1.0);
|
||||
position.rotate(-math.pi / 2, center: center);
|
||||
expectVector2(position, Vector2(1.0, 2.0));
|
||||
});
|
||||
|
||||
test('rotate - with positive direction', () {
|
||||
final position = Vector2(0.0, 1.0);
|
||||
final center = Vector2(1.0, 1.0);
|
||||
position.rotate(math.pi / 2, center: center);
|
||||
expectVector2(position, Vector2(1.0, 0.0));
|
||||
});
|
||||
|
||||
test('rotate - with a negative y position', () {
|
||||
final position = Vector2(2.0, -3.0);
|
||||
final center = Vector2(1.0, 1.0);
|
||||
position.rotate(math.pi / 2, center: center);
|
||||
expectVector2(position, Vector2(5.0, 2.0));
|
||||
});
|
||||
|
||||
test('rotate - with a negative x position', () {
|
||||
final position = Vector2(-2.0, 3.0);
|
||||
final center = Vector2(1.0, 1.0);
|
||||
position.rotate(math.pi / 2, center: center);
|
||||
expectVector2(position, Vector2(-1.0, -2.0));
|
||||
});
|
||||
|
||||
test('rotate - with a negative position', () {
|
||||
final position = Vector2(-2.0, -3.0);
|
||||
final center = Vector2(1.0, 0.0);
|
||||
position.rotate(math.pi / 2, center: center);
|
||||
expectVector2(position, Vector2(4.0, -3.0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user