Files
Lukas Klingsbo cd7a0bbb65 ShapeComponent and Hitbox to take transform of parents full ancestor tree into consideration (#1076)
* `ShapeComponent` changes size, position and angle of underlying Shape

* Added description to ShapeComponent

* Fix test

* Update packages/flame/lib/src/components/shape_component.dart

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Add absoluteScale and absoluteAngle to PositionComponent

* Refactor ShapeComponent

* Should be scaled by total scale, not scaled size

* Premature optimization for creation for objects in Polygon

* Use path for default Polygon constructor

* Do not sync component and hitbox shape

* Fix analyze issue

* Add example for flipping with collision detection

* Don't use absoluteScale

* Fix examples

* Fix examples

* Doesn't need super.render

* Fix Circle dartdoc

* Update changelog

* Update names of vertices caches in Polygon

* Update text docs

* Revert "Update text docs"

This reverts commit 73a68a465d76eb0eb50bb3753e57b2f4e3b5a7f4.

* Fix examples

* ShapeComponents docs

* Move example games to the top

* Fix dartdoc comment about polygon vertex relation

* Fix order of polygon vertices in dartdoc

* Fix anchor for PolygonComponent.fromPoints

* Add test with ancestors

* Update doc/components.md

Co-authored-by: Pasha Stetsenko <stpasha@google.com>

* Update doc/components.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Rename example classes

* Fix linting issues in examples

* Don't use px

* Use isTrue and isFalse

* Update doc/components.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Fixed comments on PR

Co-authored-by: Erick <erickzanardoo@gmail.com>
Co-authored-by: Pasha Stetsenko <stpasha@google.com>
2021-11-13 16:00:24 +01:00

317 lines
8.6 KiB
Dart

import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart' hide Image, Draggable;
enum Shapes { circle, rectangle, polygon }
class MultipleShapesExample extends FlameGame
with HasCollidables, HasDraggableComponents, FPSCounter {
static const description = '''
An example with many hitboxes that move around on the screen and during
collisions they change color depending on what it is that they have collided
with.
The snowman, the component built with three circles on top of each other,
works a little bit differently than the other components to show that you
can have multiple hitboxes within one component.
On this example, you can "throw" the components by dragging them quickly in
any direction.
''';
final TextPaint fpsTextPaint = TextPaint(
config: TextPaintConfig(
color: BasicPalette.white.color,
),
);
@override
Future<void> onLoad() async {
await super.onLoad();
final screenCollidable = ScreenCollidable();
final snowman = CollidableSnowman(
Vector2.all(150),
Vector2(100, 200),
Vector2(-100, 100),
screenCollidable,
);
MyCollidable lastToAdd = snowman;
add(screenCollidable);
add(snowman);
var totalAdded = 1;
while (totalAdded < 20) {
lastToAdd = nextRandomCollidable(lastToAdd, screenCollidable);
final lastBottomRight =
lastToAdd.toAbsoluteRect().bottomRight.toVector2();
if (lastBottomRight.x < size.x && lastBottomRight.y < size.y) {
add(lastToAdd);
totalAdded++;
} else {
break;
}
}
}
final _rng = Random();
final _distance = Vector2(100, 0);
MyCollidable nextRandomCollidable(
MyCollidable lastCollidable,
ScreenCollidable screenCollidable,
) {
final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100;
final isXOverflow = lastCollidable.position.x +
lastCollidable.size.x / 2 +
_distance.x +
collidableSize.x >
size.x;
var position = _distance + Vector2(0, lastCollidable.position.y + 200);
if (!isXOverflow) {
position = (lastCollidable.position + _distance)
..x += collidableSize.x / 2;
}
final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 400;
return randomCollidable(
position,
collidableSize,
velocity,
screenCollidable,
rng: _rng,
);
}
@override
void render(Canvas canvas) {
super.render(canvas);
fpsTextPaint.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}
abstract class MyCollidable extends PositionComponent
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<Collidable> _activeCollisions = {};
final ScreenCollidable screenCollidable;
MyCollidable(
Vector2 position,
Vector2 size,
this.velocity,
this.screenCollidable,
) : super(position: position, size: size, anchor: Anchor.center);
@override
Future<void> onLoad() async {
await super.onLoad();
_activePaint = Paint()..color = _defaultColor;
}
@override
void update(double dt) {
super.update(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<Vector2> 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<Vector2> 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;
}
}