mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-10-31 08:56:01 +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 '../../commons/commons.dart'; | ||||||
| import 'circles.dart'; | import 'circles.dart'; | ||||||
| import 'multiple_shapes.dart'; | import 'multiple_shapes.dart'; | ||||||
|  | import 'only_shapes.dart'; | ||||||
|  |  | ||||||
| void addCollisionDetectionStories(Dashbook dashbook) { | void addCollisionDetectionStories(Dashbook dashbook) { | ||||||
|   dashbook.storiesOf('Collision Detection') |   dashbook.storiesOf('Collision Detection') | ||||||
| @ -16,5 +17,10 @@ void addCollisionDetectionStories(Dashbook dashbook) { | |||||||
|       'Multiple shapes', |       'Multiple shapes', | ||||||
|       (_) => GameWidget(game: MultipleShapes()), |       (_) => GameWidget(game: MultipleShapes()), | ||||||
|       codeLink: baseLink('collision_detection/multiple_shapes.dart'), |       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; |   double angleDelta = 0; | ||||||
|   bool _isDragged = false; |   bool _isDragged = false; | ||||||
|   final _activePaint = Paint()..color = Colors.amber; |   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.position = position; | ||||||
|     this.size = size; |     this.size = size; | ||||||
|     anchor = Anchor.center; |     anchor = Anchor.center; | ||||||
| @ -33,51 +40,56 @@ abstract class MyCollidable extends PositionComponent | |||||||
|     if (_isDragged) { |     if (_isDragged) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     _wallHitTime += dt; |     if (!_isHit) { | ||||||
|  |       debugColor = _defaultDebugColor; | ||||||
|  |     } else { | ||||||
|  |       _isHit = false; | ||||||
|  |     } | ||||||
|     delta.setFrom(velocity * dt); |     delta.setFrom(velocity * dt); | ||||||
|     position.add(delta); |     position.add(delta); | ||||||
|     angleDelta = dt * rotationSpeed; |     angleDelta = dt * rotationSpeed; | ||||||
|     angle = (angle + angleDelta) % (2 * pi); |     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 |   @override | ||||||
|   void render(Canvas canvas) { |   void render(Canvas canvas) { | ||||||
|     super.render(canvas); |     super.render(canvas); | ||||||
|     renderShapes(canvas); |  | ||||||
|     final localCenter = (size / 2).toOffset(); |  | ||||||
|     if (_isDragged) { |     if (_isDragged) { | ||||||
|  |       final localCenter = (size / 2).toOffset(); | ||||||
|       canvas.drawCircle(localCenter, 5, _activePaint); |       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 |   @override | ||||||
|   void onCollision(Set<Vector2> intersectionPoints, Collidable other) { |   void onCollision(Set<Vector2> intersectionPoints, Collidable other) { | ||||||
|     final averageIntersection = intersectionPoints.reduce((sum, v) => sum + v) / |     _isHit = true; | ||||||
|         intersectionPoints.length.toDouble(); |     switch (other.runtimeType) { | ||||||
|     final collisionDirection = (averageIntersection - absoluteCenter) |       case ScreenCollidable: | ||||||
|       ..normalize() |         debugColor = Colors.teal; | ||||||
|       ..round(); |         break; | ||||||
|     if (velocity.angleToSigned(collisionDirection).abs() > 3) { |       case CollidablePolygon: | ||||||
|       // This entity got hit by something else |         debugColor = Colors.blue; | ||||||
|       return; |         break; | ||||||
|     } |       case CollidableCircle: | ||||||
|     final angleToCollision = velocity.angleToSigned(collisionDirection); |         debugColor = Colors.green; | ||||||
|     if (angleToCollision.abs() < pi / 8) { |         break; | ||||||
|       velocity.rotate(pi); |       case CollidableRectangle: | ||||||
|     } else { |         debugColor = Colors.cyan; | ||||||
|       velocity.rotate(-pi / 2 * angleToCollision.sign); |         break; | ||||||
|     } |       case CollidableSnowman: | ||||||
|     position.sub(delta * 2); |         debugColor = Colors.amber; | ||||||
|     angle = (angle - angleDelta) % (2 * pi); |         break; | ||||||
|     if (other is ScreenCollidable) { |       default: | ||||||
|       _wallHitTime = 0; |         debugColor = Colors.pink; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @ -96,8 +108,12 @@ abstract class MyCollidable extends PositionComponent | |||||||
| } | } | ||||||
|  |  | ||||||
| class CollidablePolygon extends MyCollidable { | class CollidablePolygon extends MyCollidable { | ||||||
|   CollidablePolygon(Vector2 position, Vector2 size, Vector2 velocity) |   CollidablePolygon( | ||||||
|       : super(position, size, velocity) { |     Vector2 position, | ||||||
|  |     Vector2 size, | ||||||
|  |     Vector2 velocity, | ||||||
|  |     ScreenCollidable screenCollidable, | ||||||
|  |   ) : super(position, size, velocity, screenCollidable) { | ||||||
|     final shape = HitboxPolygon([ |     final shape = HitboxPolygon([ | ||||||
|       Vector2(-1.0, 0.0), |       Vector2(-1.0, 0.0), | ||||||
|       Vector2(-0.8, 0.6), |       Vector2(-0.8, 0.6), | ||||||
| @ -113,15 +129,23 @@ class CollidablePolygon extends MyCollidable { | |||||||
| } | } | ||||||
|  |  | ||||||
| class CollidableRectangle extends MyCollidable { | class CollidableRectangle extends MyCollidable { | ||||||
|   CollidableRectangle(Vector2 position, Vector2 size, Vector2 velocity) |   CollidableRectangle( | ||||||
|       : super(position, size, velocity) { |     Vector2 position, | ||||||
|  |     Vector2 size, | ||||||
|  |     Vector2 velocity, | ||||||
|  |     ScreenCollidable screenCollidable, | ||||||
|  |   ) : super(position, size, velocity, screenCollidable) { | ||||||
|     addShape(HitboxRectangle()); |     addShape(HitboxRectangle()); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class CollidableCircle extends MyCollidable { | class CollidableCircle extends MyCollidable { | ||||||
|   CollidableCircle(Vector2 position, Vector2 size, Vector2 velocity) |   CollidableCircle( | ||||||
|       : super(position, size, velocity) { |     Vector2 position, | ||||||
|  |     Vector2 size, | ||||||
|  |     Vector2 velocity, | ||||||
|  |     ScreenCollidable screenCollidable, | ||||||
|  |   ) : super(position, size, velocity, screenCollidable) { | ||||||
|     final shape = HitboxCircle(); |     final shape = HitboxCircle(); | ||||||
|     addShape(shape); |     addShape(shape); | ||||||
|   } |   } | ||||||
| @ -134,9 +158,9 @@ class SnowmanPart extends HitboxCircle { | |||||||
|     ..strokeWidth = 1 |     ..strokeWidth = 1 | ||||||
|     ..style = PaintingStyle.stroke; |     ..style = PaintingStyle.stroke; | ||||||
|  |  | ||||||
|   SnowmanPart(double definition, Vector2 relativePosition, Color hitColor) |   SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor) | ||||||
|       : super(definition: definition) { |       : super(definition: definition) { | ||||||
|     this.relativePosition.setFrom(relativePosition); |     this.relativeOffset.setFrom(relativeOffset); | ||||||
|     onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) { |     onCollision = (Set<Vector2> intersectionPoints, HitboxShape other) { | ||||||
|       if (other.component is ScreenCollidable) { |       if (other.component is ScreenCollidable) { | ||||||
|         hitPaint..color = startColor; |         hitPaint..color = startColor; | ||||||
| @ -147,15 +171,20 @@ class SnowmanPart extends HitboxCircle { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void render(Canvas canvas, Paint paint) { |   void render(Canvas canvas, _) { | ||||||
|     super.render(canvas, hitPaint); |     super.render(canvas, hitPaint); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class CollidableSnowman extends MyCollidable { | class CollidableSnowman extends MyCollidable { | ||||||
|   CollidableSnowman(Vector2 position, Vector2 size, Vector2 velocity) |   CollidableSnowman( | ||||||
|       : super(position, size, velocity) { |     Vector2 position, | ||||||
|     rotationSpeed = 0.2; |     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 top = SnowmanPart(0.4, Vector2(0, -0.8), Colors.red); | ||||||
|     final middle = SnowmanPart(0.6, Vector2(0, -0.3), Colors.yellow); |     final middle = SnowmanPart(0.6, Vector2(0, -0.3), Colors.yellow); | ||||||
|     final bottom = SnowmanPart(1.0, Vector2(0, 0.5), Colors.green); |     final bottom = SnowmanPart(1.0, Vector2(0, 0.5), Colors.green); | ||||||
| @ -167,6 +196,9 @@ class CollidableSnowman extends MyCollidable { | |||||||
|  |  | ||||||
| class MultipleShapes extends BaseGame | class MultipleShapes extends BaseGame | ||||||
|     with HasCollidables, HasDraggableComponents { |     with HasCollidables, HasDraggableComponents { | ||||||
|  |   @override | ||||||
|  |   bool debugMode = true; | ||||||
|  |  | ||||||
|   final TextPaint fpsTextPaint = TextPaint( |   final TextPaint fpsTextPaint = TextPaint( | ||||||
|     config: TextPaintConfig( |     config: TextPaintConfig( | ||||||
|       color: BasicPalette.white.color, |       color: BasicPalette.white.color, | ||||||
| @ -175,21 +207,25 @@ class MultipleShapes extends BaseGame | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<void> onLoad() async { |   Future<void> onLoad() async { | ||||||
|     final screen = ScreenCollidable(); |     await super.onLoad(); | ||||||
|  |     final screenCollidable = ScreenCollidable(); | ||||||
|     final snowman = CollidableSnowman( |     final snowman = CollidableSnowman( | ||||||
|       Vector2.all(150), |       Vector2.all(150), | ||||||
|       Vector2(100, 200), |       Vector2(100, 200), | ||||||
|       Vector2(-100, 100), |       Vector2(-100, 100), | ||||||
|  |       screenCollidable, | ||||||
|     ); |     ); | ||||||
|     MyCollidable lastToAdd = snowman; |     MyCollidable lastToAdd = snowman; | ||||||
|     add(screen); |     add(screenCollidable); | ||||||
|     add(snowman); |     add(snowman); | ||||||
|     var totalAdded = 1; |     var totalAdded = 1; | ||||||
|     while (totalAdded < 20) { |     while (totalAdded < 10) { | ||||||
|       lastToAdd = createRandomCollidable(lastToAdd); |       lastToAdd = createRandomCollidable(lastToAdd, screenCollidable); | ||||||
|       final lastBottomRight = |       final lastBottomRight = | ||||||
|           lastToAdd.toAbsoluteRect().bottomRight.toVector2(); |           lastToAdd.toAbsoluteRect().bottomRight.toVector2(); | ||||||
|       if (screen.containsPoint(lastBottomRight)) { |       final screenSize = size / camera.zoom; | ||||||
|  |       if (lastBottomRight.x < screenSize.x && | ||||||
|  |           lastBottomRight.y < screenSize.y) { | ||||||
|         add(lastToAdd); |         add(lastToAdd); | ||||||
|         totalAdded++; |         totalAdded++; | ||||||
|       } else { |       } else { | ||||||
| @ -201,7 +237,10 @@ class MultipleShapes extends BaseGame | |||||||
|   final _rng = Random(); |   final _rng = Random(); | ||||||
|   final _distance = Vector2(100, 0); |   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 collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100; | ||||||
|     final isXOverflow = lastCollidable.position.x + |     final isXOverflow = lastCollidable.position.x + | ||||||
|             lastCollidable.size.x / 2 + |             lastCollidable.size.x / 2 + | ||||||
| @ -213,17 +252,18 @@ class MultipleShapes extends BaseGame | |||||||
|       position = (lastCollidable.position + _distance) |       position = (lastCollidable.position + _distance) | ||||||
|         ..x += collidableSize.x / 2; |         ..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 rotationSpeed = 0.5 - _rng.nextDouble(); | ||||||
|     final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)]; |     final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)]; | ||||||
|     switch (shapeType) { |     switch (shapeType) { | ||||||
|       case Shapes.circle: |       case Shapes.circle: | ||||||
|         return CollidableCircle(position, collidableSize, velocity); |         return CollidableCircle(position, collidableSize, velocity, screen) | ||||||
|  |           ..rotationSpeed = rotationSpeed; | ||||||
|       case Shapes.rectangle: |       case Shapes.rectangle: | ||||||
|         return CollidableRectangle(position, collidableSize, velocity) |         return CollidableRectangle(position, collidableSize, velocity, screen) | ||||||
|           ..rotationSpeed = rotationSpeed; |           ..rotationSpeed = rotationSpeed; | ||||||
|       case Shapes.polygon: |       case Shapes.polygon: | ||||||
|         return CollidablePolygon(position, collidableSize, velocity) |         return CollidablePolygon(position, collidableSize, velocity, screen) | ||||||
|           ..rotationSpeed = rotationSpeed; |           ..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 |  - 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 |  - Set the same debug mode for children as for the parent when added | ||||||
|  - Fix camera projections when camera is zoomed |  - 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] | ## [1.0.0-rc9] | ||||||
|  - Fix input bug with other anchors than center |  - 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/parallax_component.dart'; | ||||||
| export 'src/components/particle_component.dart'; | export 'src/components/particle_component.dart'; | ||||||
| export 'src/components/position_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_component.dart'; | ||||||
| export 'src/components/sprite_animation_group_component.dart'; | export 'src/components/sprite_animation_group_component.dart'; | ||||||
| export 'src/components/sprite_batch_component.dart'; | export 'src/components/sprite_batch_component.dart'; | ||||||
|  | |||||||
| @ -55,7 +55,12 @@ class Anchor { | |||||||
|     Anchor otherAnchor, |     Anchor otherAnchor, | ||||||
|     Vector2 size, |     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. |   /// Returns a string representation of this Anchor. | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | import '../../../components.dart'; | ||||||
|  | import '../../../game.dart'; | ||||||
| import '../../components/position_component.dart'; | import '../../components/position_component.dart'; | ||||||
| import '../../extensions/vector2.dart'; | import '../../extensions/vector2.dart'; | ||||||
| import '../../geometry/rectangle.dart'; | import '../../geometry/rectangle.dart'; | ||||||
| @ -18,17 +20,33 @@ mixin Collidable on Hitbox { | |||||||
|   void onCollision(Set<Vector2> intersectionPoints, Collidable other) {} |   void onCollision(Set<Vector2> intersectionPoints, Collidable other) {} | ||||||
| } | } | ||||||
|  |  | ||||||
| class ScreenCollidable extends PositionComponent with Hitbox, Collidable { | class ScreenCollidable extends PositionComponent | ||||||
|  |     with Hitbox, Collidable, HasGameRef<BaseGame> { | ||||||
|   @override |   @override | ||||||
|   CollidableType collidableType = CollidableType.passive; |   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()); |     addShape(HitboxRectangle()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void onGameResize(Vector2 gameSize) { |   void update(double dt) { | ||||||
|     super.onGameResize(gameSize); |     super.update(dt); | ||||||
|     size = gameSize; |     _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 |   /// 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. |   /// overlap, since the shapes have to be within the size of the component. | ||||||
|   bool possiblyOverlapping(Hitbox other) { |   bool possiblyOverlapping(Hitbox other) { | ||||||
|     return other.center.distanceToSquared(center) <= |     final maxDistance = other.size.length + size.length; | ||||||
|         other.size.length2 + size.length2; |     return other.absoluteCenter.distanceToSquared(absoluteCenter) <= | ||||||
|  |         maxDistance * maxDistance; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /// Since this is a cheaper calculation than checking towards all shapes this |   /// 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 |   /// contain the point, since the shapes have to be within the size of the | ||||||
|   /// component. |   /// component. | ||||||
|   bool possiblyContainsPoint(Vector2 point) { |   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 { |   Vector2 get center { | ||||||
|     return anchor == Anchor.center |     if (anchor == Anchor.center) { | ||||||
|         ? position |       return position; | ||||||
|         : anchor.toOtherAnchorPosition(position, Anchor.center, size); |     } 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; |   Vector2 get absoluteCenter => absoluteParentPosition + center; | ||||||
|  |  | ||||||
|   /// Angle (with respect to the x-axis) this component should be rendered with. |   /// Angle (with respect to the x-axis) this component should be rendered with. | ||||||
| @ -142,7 +145,7 @@ abstract class PositionComponent extends BaseComponent { | |||||||
|   @override |   @override | ||||||
|   bool containsPoint(Vector2 point) { |   bool containsPoint(Vector2 point) { | ||||||
|     final rectangle = Rectangle.fromRect(toAbsoluteRect(), angle: angle) |     final rectangle = Rectangle.fromRect(toAbsoluteRect(), angle: angle) | ||||||
|       ..anchorPosition = absolutePosition; |       ..position = absoluteCenter; | ||||||
|     return rectangle.containsPoint(point); |     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); |     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 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 | ||||||
|  |   /// a clockwise fashion | ||||||
|  |   /// In a normal coordinate system it rotates in a counter-clockwise fashion | ||||||
|   void rotate(double angle, {Vector2? center}) { |   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) { |     if (center == null) { | ||||||
|       setValues( |       setValues( | ||||||
|         x * cos(angle) - y * sin(angle), |         x * cos(angle) - y * sin(angle), | ||||||
|  | |||||||
| @ -120,8 +120,8 @@ class Camera extends Projector { | |||||||
|   /// add any non-smooth movement. |   /// add any non-smooth movement. | ||||||
|   Rect? worldBounds; |   Rect? worldBounds; | ||||||
|  |  | ||||||
|   /// If set, the camera will zoom by this ratio. This can be greater than 1 (zoom in) |   /// If set, the camera will zoom by this ratio. This can be greater than 1 | ||||||
|   /// or smaller (zoom out), but should always be greater than zero. |   /// (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 |   /// Note: do not confuse this with the zoom applied by the viewport. The | ||||||
|   /// viewport applies a (normally) fixed zoom to adapt multiple screens into |   /// viewport applies a (normally) fixed zoom to adapt multiple screens into | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
|  | import '../../game.dart'; | ||||||
| import '../../geometry.dart'; | import '../../geometry.dart'; | ||||||
| import '../extensions/vector2.dart'; | import '../extensions/vector2.dart'; | ||||||
| import 'shape.dart'; | import 'shape.dart'; | ||||||
| @ -33,23 +34,19 @@ class Circle extends Shape { | |||||||
|     double? angle, |     double? angle, | ||||||
|   }) : super(position: position, size: size, angle: angle ?? 0); |   }) : 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 |   @override | ||||||
|   void render(Canvas canvas, Paint paint) { |   void render(Canvas canvas, Paint paint) { | ||||||
|     final localPosition = size! / 2 + position; |     canvas.drawCircle(localCenter.toOffset(), radius, paint); | ||||||
|     final localRelativePosition = (size! / 2)..multiply(relativePosition); |  | ||||||
|     canvas.drawCircle( |  | ||||||
|       (localPosition + localRelativePosition).toOffset(), |  | ||||||
|       radius, |  | ||||||
|       paint, |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /// Checks whether the represented circle contains the [point]. |   /// Checks whether the represented circle contains the [point]. | ||||||
|   @override |   @override | ||||||
|   bool containsPoint(Vector2 point) { |   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 |   /// 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(); |     double sq(double x) => pow(x, 2).toDouble(); | ||||||
|  |  | ||||||
|     final cx = shapeCenter.x; |     final cx = absoluteCenter.x; | ||||||
|     final cy = shapeCenter.y; |     final cy = absoluteCenter.y; | ||||||
|  |  | ||||||
|     final point1 = line.from; |     final point1 = line.from; | ||||||
|     final point2 = line.to; |     final point2 = line.to; | ||||||
|  | |||||||
| @ -1,13 +1,19 @@ | |||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
| import 'dart:ui'; | import 'dart:ui' hide Canvas; | ||||||
|  |  | ||||||
|  | import '../../game.dart'; | ||||||
| import '../../geometry.dart'; | import '../../geometry.dart'; | ||||||
|  | import '../extensions/canvas.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 | ||||||
|  |   // 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 |   /// 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 | ||||||
| @ -51,17 +57,27 @@ class Polygon extends Shape { | |||||||
|     Vector2? position, |     Vector2? position, | ||||||
|     Vector2? size, |     Vector2? size, | ||||||
|     double? angle, |     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>>(); |   final _cachedScaledShape = ShapeCache<Iterable<Vector2>>(); | ||||||
|  |  | ||||||
|   /// Gives back the shape vectors multiplied by the size |   /// Gives back the shape vectors multiplied by the size | ||||||
|   Iterable<Vector2> scaled() { |   Iterable<Vector2> scaled() { | ||||||
|     if (!_cachedScaledShape.isCacheValid([size])) { |     if (!_cachedScaledShape.isCacheValid([size])) { | ||||||
|       _cachedScaledShape.updateCache( |       for (var i = 0; i < _sizedVertices.length; i++) { | ||||||
|         normalizedVertices.map((p) => p.clone()..multiply(size! / 2)), |         final point = normalizedVertices[i]; | ||||||
|         [size!.clone()], |         (_sizedVertices[i]..setFrom(point)).multiply(halfSize); | ||||||
|       ); |       } | ||||||
|  |       _cachedScaledShape.updateCache(_sizedVertices, [size.clone()]); | ||||||
|     } |     } | ||||||
|     return _cachedScaledShape.value!; |     return _cachedScaledShape.value!; | ||||||
|   } |   } | ||||||
| @ -70,21 +86,25 @@ class Polygon extends Shape { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void render(Canvas canvas, Paint paint) { |   void render(Canvas canvas, Paint paint) { | ||||||
|     if (!_cachedRenderPath.isCacheValid([position, size])) { |     if (!_cachedRenderPath | ||||||
|  |         .isCacheValid([offsetPosition, relativeOffset, size, angle])) { | ||||||
|  |       final center = localCenter; | ||||||
|       _cachedRenderPath.updateCache( |       _cachedRenderPath.updateCache( | ||||||
|         Path() |         Path() | ||||||
|           ..addPolygon( |           ..addPolygon( | ||||||
|             scaled() |             scaled() | ||||||
|                 .map((point) => (point + |                 .map( | ||||||
|                         (position + size! / 2) + |                   (point) => ((center + point)..rotate(angle, center: center)) | ||||||
|                         ((size! / 2)..multiply(relativePosition))) |                       .toOffset(), | ||||||
|                     .toOffset()) |                 ) | ||||||
|                 .toList(), |                 .toList(), | ||||||
|             true, |             true, | ||||||
|           ), |           ), | ||||||
|         [ |         [ | ||||||
|           position.clone(), |           offsetPosition.clone(), | ||||||
|           size!.clone(), |           relativeOffset.clone(), | ||||||
|  |           size.clone(), | ||||||
|  |           angle, | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| @ -97,13 +117,19 @@ class Polygon extends Shape { | |||||||
|   /// are the "corners" of the hitbox rotated with [angle]. |   /// are the "corners" of the hitbox rotated with [angle]. | ||||||
|   List<Vector2> hitbox() { |   List<Vector2> hitbox() { | ||||||
|     // Use cached bounding vertices if state of the component hasn't changed |     // 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( |       _cachedHitbox.updateCache( | ||||||
|         scaled() |         _hitboxVertices, | ||||||
|             .map((point) => |         [absoluteCenter, size.clone(), parentAngle, angle], | ||||||
|                 (point + shapeCenter)..rotate(angle, center: anchorPosition)) |  | ||||||
|             .toList(growable: false), |  | ||||||
|         [shapeCenter, size!.clone(), angle], |  | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return _cachedHitbox.value!; |     return _cachedHitbox.value!; | ||||||
| @ -114,7 +140,7 @@ class Polygon extends Shape { | |||||||
|   @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 | ||||||
|     if (size!.x == 0 || size!.y == 0) { |     if (size.x == 0 || size.y == 0) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
| import '../../extensions.dart'; | import '../../extensions.dart'; | ||||||
|  | import '../../game.dart'; | ||||||
| import '../../geometry.dart'; | import '../../geometry.dart'; | ||||||
| import 'shape.dart'; | import 'shape.dart'; | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import 'dart:ui'; | import 'dart:ui'; | ||||||
|  |  | ||||||
| import '../../components.dart'; | import '../../components.dart'; | ||||||
|  | import '../../game.dart'; | ||||||
| import '../extensions/vector2.dart'; | import '../extensions/vector2.dart'; | ||||||
| import 'shape_intersections.dart' as intersection_system; | import 'shape_intersections.dart' as intersection_system; | ||||||
|  |  | ||||||
| @ -9,34 +10,100 @@ import 'shape_intersections.dart' as intersection_system; | |||||||
| /// center. | /// center. | ||||||
| /// A point can be determined to be within of outside of a shape. | /// A point can be determined to be within of outside of a shape. | ||||||
| abstract class Shape { | abstract class Shape { | ||||||
|   /// The position of your shape, it is up to you how you treat this |   final ShapeCache<Vector2> _halfSizeCache = ShapeCache(); | ||||||
|   Vector2 position; |   final ShapeCache<Vector2> _localCenterCache = ShapeCache(); | ||||||
|  |   final ShapeCache<Vector2> _absoluteCenterCache = ShapeCache(); | ||||||
|  |  | ||||||
|   /// The position of your shape in relation to its size |   /// Should be the center of that [offsetPosition] and [relativeOffset] | ||||||
|   Vector2 relativePosition = Vector2.zero(); |   /// 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] |   /// 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 |   /// The angle of the shape from its initial definition | ||||||
|   double angle; |   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; |   /// The position of your shape in relation to its size from (-1,-1) to (1,1) | ||||||
|   Vector2 get anchorPosition => _anchorPosition ?? position; |   Vector2 relativeOffset = Vector2.zero(); | ||||||
|   set anchorPosition(Vector2 position) => _anchorPosition = position; |  | ||||||
|  |   /// 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({ |   Shape({ | ||||||
|     Vector2? position, |     Vector2? position, | ||||||
|     this.size, |     Vector2? size, | ||||||
|     this.angle = 0, |     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 |   /// Whether the point [p] is within the shapes boundaries or not | ||||||
|   bool containsPoint(Vector2 p); |   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 |   /// Where this Shape has intersection points with another shape | ||||||
|   Set<Vector2> intersections(Shape other) { |   Set<Vector2> intersections(Shape other) { | ||||||
| @ -47,23 +114,14 @@ abstract class Shape { | |||||||
| mixin HitboxShape on Shape { | mixin HitboxShape on Shape { | ||||||
|   late PositionComponent component; |   late PositionComponent component; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Vector2 get anchorPosition => component.absolutePosition; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Vector2 get size => component.size; |   Vector2 get size => component.size; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   double get angle => component.angle; |   double get parentAngle => component.angle; | ||||||
|  |  | ||||||
|   /// The shape's absolute center |  | ||||||
|   @override |   @override | ||||||
|   Vector2 get shapeCenter { |   Vector2 get position => component.absoluteCenter; | ||||||
|     return component.absoluteCenter + |  | ||||||
|         position + |  | ||||||
|         ((size / 2)..multiply(relativePosition)) |  | ||||||
|       ..rotate(angle, center: anchorPosition); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /// Assign your own [CollisionCallback] if you want a callback when this |   /// Assign your own [CollisionCallback] if you want a callback when this | ||||||
|   /// shape collides with another [HitboxShape] |   /// shape collides with another [HitboxShape] | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ class CirclePolygonIntersections extends Intersections<Circle, Polygon> { | |||||||
| class CircleCircleIntersections extends Intersections<Circle, Circle> { | class CircleCircleIntersections extends Intersections<Circle, Circle> { | ||||||
|   @override |   @override | ||||||
|   Set<Vector2> intersect(Circle shapeA, Circle shapeB) { |   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 radiusA = shapeA.radius; | ||||||
|     final radiusB = shapeB.radius; |     final radiusB = shapeB.radius; | ||||||
|     if (distance > radiusA + radiusB) { |     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 |       // infinite number of solutions. Since it is problematic to return a | ||||||
|       // set of infinite size, we'll return 4 distinct points here. |       // set of infinite size, we'll return 4 distinct points here. | ||||||
|       return { |       return { | ||||||
|         shapeA.shapeCenter + Vector2(radiusA, 0), |         shapeA.absoluteCenter + Vector2(radiusA, 0), | ||||||
|         shapeA.shapeCenter + Vector2(0, -radiusA), |         shapeA.absoluteCenter + Vector2(0, -radiusA), | ||||||
|         shapeA.shapeCenter + Vector2(-radiusA, 0), |         shapeA.absoluteCenter + Vector2(-radiusA, 0), | ||||||
|         shapeA.shapeCenter + Vector2(0, radiusA), |         shapeA.absoluteCenter + Vector2(0, radiusA), | ||||||
|       }; |       }; | ||||||
|     } else { |     } else { | ||||||
|       /// There are definitely collision points if we end up in here. |       /// 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)) / |       final lengthA = (pow(radiusA, 2) - pow(radiusB, 2) + pow(distance, 2)) / | ||||||
|           (2 * distance); |           (2 * distance); | ||||||
|       final lengthB = sqrt((pow(radiusA, 2) - pow(lengthA, 2)).abs()); |       final lengthB = sqrt((pow(radiusA, 2) - pow(lengthA, 2)).abs()); | ||||||
|       final centerPoint = shapeA.shapeCenter + |       final centerPoint = shapeA.absoluteCenter + | ||||||
|           (shapeB.shapeCenter - shapeA.shapeCenter) * lengthA / distance; |           (shapeB.absoluteCenter - shapeA.absoluteCenter) * lengthA / distance; | ||||||
|       final delta = Vector2( |       final delta = Vector2( | ||||||
|         lengthB * |         lengthB * | ||||||
|             (shapeB.shapeCenter.y - shapeA.shapeCenter.y).abs() / |             (shapeB.absoluteCenter.y - shapeA.absoluteCenter.y).abs() / | ||||||
|             distance, |             distance, | ||||||
|         -lengthB * |         -lengthB * | ||||||
|             (shapeB.shapeCenter.x - shapeA.shapeCenter.x).abs() / |             (shapeB.absoluteCenter.x - shapeA.absoluteCenter.x).abs() / | ||||||
|             distance, |             distance, | ||||||
|       ); |       ); | ||||||
|       return { |       return { | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import 'package:flame/extensions.dart'; | ||||||
| import 'package:flame/src/anchor.dart'; | import 'package:flame/src/anchor.dart'; | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
|  |  | ||||||
| @ -26,5 +27,27 @@ void main() { | |||||||
|         throwsA(const TypeMatcher<AssertionError>()), |         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); |       final point = Vector2(2.0, 2.0); | ||||||
|       expect(component.containsPoint(point), false); |       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:flame/extensions.dart'; | ||||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||||
|  |  | ||||||
|  | import '../util/expect_vector2.dart'; | ||||||
|  |  | ||||||
| void expectDouble(double d1, double d2) { | void expectDouble(double d1, double d2) { | ||||||
|   expect((d1 - d2).abs() <= 0.0001, true); |   expect((d1 - d2).abs() <= 0.0001, true); | ||||||
| } | } | ||||||
| @ -100,6 +102,7 @@ void main() { | |||||||
|       expectDouble(p2.length, math.sqrt(2)); |       expectDouble(p2.length, math.sqrt(2)); | ||||||
|       expect(p2.x, p2.y); |       expect(p2.x, p2.y); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     test('moveToTarget - fully horizontal', () { |     test('moveToTarget - fully horizontal', () { | ||||||
|       final current = Vector2(10.0, 0.0); |       final current = Vector2(10.0, 0.0); | ||||||
|       final target = Vector2(20.0, 0.0); |       final target = Vector2(20.0, 0.0); | ||||||
| @ -116,6 +119,7 @@ void main() { | |||||||
|       current.moveToTarget(target, 5); |       current.moveToTarget(target, 5); | ||||||
|       expect(current, Vector2(20.0, 0.0)); |       expect(current, Vector2(20.0, 0.0)); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     test('moveToTarget - fully vertical', () { |     test('moveToTarget - fully vertical', () { | ||||||
|       final current = Vector2(10.0, 0.0); |       final current = Vector2(10.0, 0.0); | ||||||
|       final target = Vector2(10.0, 100.0); |       final target = Vector2(10.0, 100.0); | ||||||
| @ -132,6 +136,7 @@ void main() { | |||||||
|       current.moveToTarget(target, 19); |       current.moveToTarget(target, 19); | ||||||
|       expect(current, Vector2(10.0, 100.0)); |       expect(current, Vector2(10.0, 100.0)); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     test('moveToTarget - arbitrary direction', () { |     test('moveToTarget - arbitrary direction', () { | ||||||
|       final current = Vector2(2.0, 2.0); |       final current = Vector2(2.0, 2.0); | ||||||
|       final target = Vector2(4.0, 6.0); // direction is 1,2 |       final target = Vector2(4.0, 6.0); // direction is 1,2 | ||||||
| @ -145,5 +150,52 @@ void main() { | |||||||
|       current.moveToTarget(target, math.sqrt(5)); |       current.moveToTarget(target, math.sqrt(5)); | ||||||
|       expect(current, Vector2(4.0, 6.0)); |       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
	 Lukas Klingsbo
					Lukas Klingsbo