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 '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; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Lukas Klingsbo
					Lukas Klingsbo