import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/geometry.dart'; import 'package:flame/input.dart'; import 'package:flutter/material.dart' hide Image, Draggable; enum Shapes { circle, rectangle, polygon } class MultipleShapesExample extends FlameGame with HasCollidables, HasDraggables, FPSCounter { static const description = ''' An example with many hitboxes that move around on the screen and during collisions they change color depending on what it is that they have collided with. The snowman, the component built with three circles on top of each other, works a little bit differently than the other components to show that you can have multiple hitboxes within one component. On this example, you can "throw" the components by dragging them quickly in any direction. '''; final TextPaint fpsTextPaint = TextPaint(); @override Future onLoad() async { await super.onLoad(); final screenCollidable = ScreenCollidable(); final snowman = CollidableSnowman( Vector2.all(150), Vector2(100, 200), Vector2(-100, 100), screenCollidable, ); MyCollidable lastToAdd = snowman; add(screenCollidable); add(snowman); var totalAdded = 1; while (totalAdded < 20) { lastToAdd = nextRandomCollidable(lastToAdd, screenCollidable); final lastBottomRight = lastToAdd.toAbsoluteRect().bottomRight.toVector2(); if (lastBottomRight.x < size.x && lastBottomRight.y < size.y) { add(lastToAdd); totalAdded++; } else { break; } } } final _rng = Random(); final _distance = Vector2(100, 0); MyCollidable nextRandomCollidable( MyCollidable lastCollidable, ScreenCollidable screenCollidable, ) { final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100; final isXOverflow = lastCollidable.position.x + lastCollidable.size.x / 2 + _distance.x + collidableSize.x > size.x; var position = _distance + Vector2(0, lastCollidable.position.y + 200); if (!isXOverflow) { position = (lastCollidable.position + _distance) ..x += collidableSize.x / 2; } final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 400; return randomCollidable( position, collidableSize, velocity, screenCollidable, rng: _rng, ); } @override void render(Canvas canvas) { super.render(canvas); fpsTextPaint.render( canvas, '${fps(120).toStringAsFixed(2)}fps', Vector2(0, size.y - 24), ); } } abstract class MyCollidable extends PositionComponent with Draggable, HasHitboxes, Collidable { double rotationSpeed = 0.0; final Vector2 velocity; final delta = Vector2.zero(); double angleDelta = 0; bool _isDragged = false; late final Paint _activePaint; final Color _defaultColor = Colors.blue.withOpacity(0.8); final Set _activeCollisions = {}; final ScreenCollidable screenCollidable; MyCollidable( Vector2 position, Vector2 size, this.velocity, this.screenCollidable, ) : super(position: position, size: size, anchor: Anchor.center); @override Future onLoad() async { await super.onLoad(); _activePaint = Paint()..color = _defaultColor; } @override void update(double dt) { if (_isDragged) { return; } 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 - (scaledSize / 2); if (topLeft.x + scaledSize.x < 0 || topLeft.y + scaledSize.y < 0 || topLeft.x > screenCollidable.scaledSize.x || topLeft.y > screenCollidable.scaledSize.y) { final moduloSize = screenCollidable.scaledSize + scaledSize; topLeftPosition = topLeftPosition % moduloSize; } } @override void render(Canvas canvas) { renderHitboxes(canvas, paint: _activePaint); if (_isDragged) { final localCenter = (scaledSize / 2).toOffset(); canvas.drawCircle(localCenter, 5, _activePaint); } } @override void onCollision(Set intersectionPoints, Collidable other) { final isNew = _activeCollisions.add(other); if (isNew) { _activePaint.color = collisionColor(other).withOpacity(0.8); } } @override void onCollisionEnd(Collidable other) { _activeCollisions.remove(other); if (_activeCollisions.isEmpty) { _activePaint.color = _defaultColor; } } Color collisionColor(Collidable other) { switch (other.runtimeType) { case ScreenCollidable: return Colors.teal; case CollidablePolygon: return Colors.deepOrange; case CollidableCircle: return Colors.green; case CollidableRectangle: return Colors.cyan; case CollidableSnowman: return Colors.amber; default: return Colors.pink; } } @override bool onDragUpdate(int pointerId, _) { _isDragged = true; return true; } @override bool onDragEnd(int pointerId, DragEndInfo info) { velocity.setFrom(info.velocity / 10); _isDragged = false; return true; } } class CollidablePolygon extends MyCollidable { CollidablePolygon( Vector2 position, Vector2 size, Vector2 velocity, ScreenCollidable screenCollidable, ) : super(position, size, velocity, screenCollidable) { final hitbox = HitboxPolygon([ Vector2(-1.0, 0.0), Vector2(-0.8, 0.6), Vector2(0.0, 1.0), Vector2(0.6, 0.9), Vector2(1.0, 0.0), Vector2(0.6, -0.8), Vector2(0, -1.0), Vector2(-0.8, -0.8), ]); addHitbox(hitbox); } } class CollidableRectangle extends MyCollidable { CollidableRectangle( Vector2 position, Vector2 size, Vector2 velocity, ScreenCollidable screenCollidable, ) : super(position, size, velocity, screenCollidable) { addHitbox(HitboxRectangle()); } } class CollidableCircle extends MyCollidable { CollidableCircle( Vector2 position, Vector2 size, Vector2 velocity, ScreenCollidable screenCollidable, ) : super(position, size, velocity, screenCollidable) { addHitbox(HitboxCircle()); } } class SnowmanPart extends HitboxCircle { final startColor = Colors.blue.withOpacity(0.8); final hitPaint = Paint(); SnowmanPart(double definition, Vector2 relativeOffset, Color hitColor) : super(normalizedRadius: definition) { this.relativeOffset.setFrom(relativeOffset); hitPaint.color = startColor; onCollision = (Set intersectionPoints, HitboxShape other) { if (other.component is ScreenCollidable) { hitPaint.color = startColor; } else { hitPaint.color = hitColor.withOpacity(0.8); } }; } @override void render(Canvas canvas, _) { super.render(canvas, hitPaint); } } class CollidableSnowman extends MyCollidable { 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); addHitbox(top); addHitbox(middle); addHitbox(bottom); add( randomCollidable( Vector2(size.x / 2, size.y * 0.75), size / 4, Vector2.zero(), screenCollidable, ), ); } } MyCollidable randomCollidable( Vector2 position, Vector2 size, Vector2 velocity, ScreenCollidable screenCollidable, { Random? rng, }) { final _rng = rng ?? Random(); final rotationSpeed = 0.5 - _rng.nextDouble(); final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)]; switch (shapeType) { case Shapes.circle: return CollidableCircle(position, size, velocity, screenCollidable) ..rotationSpeed = rotationSpeed; case Shapes.rectangle: return CollidableRectangle(position, size, velocity, screenCollidable) ..rotationSpeed = rotationSpeed; case Shapes.polygon: return CollidablePolygon(position, size, velocity, screenCollidable) ..rotationSpeed = rotationSpeed; } }