feat: SpawnComponent (#2709)

This PR introduces the `SpawnComponent`, which randomly spawns
components within a set area.
This commit is contained in:
Lukas Klingsbo
2023-09-10 17:55:30 +02:00
committed by GitHub
parent b3d78f5883
commit 83f5ea45dc
39 changed files with 593 additions and 40 deletions

View File

@ -340,6 +340,12 @@ void onDragUpdate(DragUpdateInfo info) {
### PositionType
```{note}
If you are using the `CameraComponent` you should not use `PositionType`, but
instead adding your components directly to the viewport for example if you
want to use them as a HUD.
```
If you want to create a HUD (Head-up display) or another component that isn't positioned in relation
to the game coordinates, you can change the `PositionType` of the component.
The default `PositionType` is `positionType = PositionType.game` and that can be changed to
@ -810,6 +816,44 @@ class ButtonComponent extends SpriteGroupComponent<ButtonState>
```
## SpawnComponent
This component is a non-visual component that spawns other components inside of the parent of the
`SpawnComponent`. It's great if you for example want to spawn enemies or power-ups randomly within
an area.
The `SpawnComponent` takes a factory function that it uses to create new components and an area
where the components should be spawned within (or along the edges of).
For the area, you can use the `Circle`, `Rectangle` or `Polygon` class, and if you want to only
spawn components along the edges of the shape set the `within` argument to false (defaults to true).
This would for example spawn new components of the type `MyComponent` every 0.5 seconds randomly
within the defined circle:
```dart
SpawnComponent(
factory: () => MyComponent(size: Vector2(10, 20)),
period: 0.5,
area: Circle(Vector2(100, 200), 150),
);
```
If you don't want the spawning rate to be static, you can use the `SpawnComponent.periodRange`
constructor with the `minPeriod` and `maxPeriod` arguments instead.
In the following example the component would be spawned randomly within the circle and the time
between each new spawned component is between 0.5 to 10 seconds.
```dart
SpawnComponent.periodRange(
factory: () => MyComponent(size: Vector2(10, 20)),
minPeriod: 0.5,
maxPeriod: 10,
area: Circle(Vector2(100, 200), 150),
);
```
## SvgComponent
**Note**: To use SVG with Flame, use the [`flame_svg`](https://github.com/flame-engine/flame_svg)

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flutter/rendering.dart';
class DragEventsGame extends FlameGame {
@ -242,5 +243,3 @@ class Star extends PositionComponent with DragCallbacks {
position += event.delta;
}
}
const tau = 2 * pi;

View File

@ -2,6 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/rendering.dart';
import 'package:flutter/rendering.dart';
@ -355,7 +356,7 @@ class Orbit extends PositionComponent {
@override
void update(double dt) {
_angle += dt / revolutionPeriod * Transform2D.tau;
_angle += dt / revolutionPeriod * tau;
planet.position = Vector2(radius, 0)..rotate(_angle);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
class ValueRouteExample extends FlameGame {
late final RouterComponent router;
@ -130,5 +131,3 @@ class Star extends PositionComponent with TapCallbacks {
}
}
}
const tau = pi * 2;

View File

@ -4,6 +4,7 @@ import 'package:flame/camera.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart' show OffsetExtension;
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flutter/painting.dart';
@ -258,7 +259,6 @@ class Ant extends PositionComponent {
late final Color color;
final Random random;
static const black = Color(0xFF000000);
static const tau = Transform2D.tau;
late final Paint bodyPaint;
late final Paint eyesPaint;
late final Paint legsPaint;

View File

@ -10,6 +10,7 @@ import 'package:examples/stories/components/keys_example.dart';
import 'package:examples/stories/components/look_at_example.dart';
import 'package:examples/stories/components/look_at_smooth_example.dart';
import 'package:examples/stories/components/priority_example.dart';
import 'package:examples/stories/components/spawn_component_example.dart';
import 'package:examples/stories/components/time_scale_example.dart';
import 'package:flame/game.dart';
@ -64,6 +65,14 @@ void addComponentsStories(Dashbook dashbook) {
baseLink('components/components_notifier_provider_example.dart'),
info: ComponentsNotifierProviderExampleWidget.description,
)
..add(
'Spawn Component',
(_) => const GameWidget.controlled(
gameFactory: SpawnComponentExample.new,
),
codeLink: baseLink('components/spawn_component_example.dart'),
info: SpawnComponentExample.description,
)
..add(
'Time Scale',
(_) => const GameWidget.controlled(

View File

@ -0,0 +1,60 @@
import 'package:examples/commons/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/math.dart';
class SpawnComponentExample extends FlameGame with TapDetector {
static String description =
'Tap on the screen to start spawning Embers within different shapes.';
@override
void onTapDown(TapDownInfo info) {
final shapeType = Shapes.values.random();
final Shape shape;
final position = info.eventPosition.game;
switch (shapeType) {
case Shapes.rectangle:
shape = Rectangle.fromCenter(
center: info.eventPosition.game,
size: Vector2.all(200),
);
case Shapes.circle:
shape = Circle(info.eventPosition.game, 150);
case Shapes.polygon:
shape = Polygon(
[
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.3, -0.2),
Vector2(0.0, -1.0),
Vector2(-0.8, -0.5),
].map((vertex) {
return vertex
..scale(200)
..add(position);
}).toList(),
);
}
add(
SpawnComponent(
factory: (_) => Ember(),
period: 0.5,
area: shape,
within: randomFallback.nextBool(),
),
);
}
}
enum Shapes {
rectangle,
circle,
polygon,
}

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame_noise/flame_noise.dart';
import 'package:flutter/material.dart';
@ -26,7 +27,6 @@ class MoveEffectExample extends FlameGame {
@override
void onLoad() {
const tau = Transform2D.tau;
cameraComponent = CameraComponent.withFixedResolution(
world: world,
width: 400,

View File

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flutter/animation.dart';
class RotateEffectExample extends FlameGame {
@ -45,7 +46,7 @@ class RotateEffectExample extends FlameGame {
compass.arrow
..add(
RotateEffect.to(
Transform2D.tau,
tau,
EffectController(
duration: 20,
infinite: true,
@ -54,7 +55,7 @@ class RotateEffectExample extends FlameGame {
)
..add(
RotateEffect.by(
Transform2D.tau * 0.015,
tau * 0.015,
EffectController(
duration: 0.1,
reverseDuration: 0.1,
@ -64,7 +65,7 @@ class RotateEffectExample extends FlameGame {
)
..add(
RotateEffect.by(
Transform2D.tau * 0.021,
tau * 0.021,
EffectController(
duration: 0.13,
reverseDuration: 0.13,
@ -98,7 +99,7 @@ class Compass extends PositionComponent {
Future<void> onLoad() async {
_marksPath = Path();
for (var i = 0; i < 12; i++) {
final angle = Transform2D.tau * (i / 12);
final angle = tau * (i / 12);
// Note: rim takes up 0.1radius, so the lengths must be > than that
final markLength = (i % 3 == 0) ? _radius * 0.2 : _radius * 0.15;
_marksPath.moveTo(
@ -189,7 +190,7 @@ class CompassRim extends PositionComponent {
final innerRadius = _radius - _width;
final midRadius = _radius - _width / 3;
for (var i = 0; i < numberOfNotches; i++) {
final angle = Transform2D.tau * (i / numberOfNotches);
final angle = tau * (i / numberOfNotches);
_marksPath.moveTo(
_radius + innerRadius * sin(angle),
_radius + innerRadius * cos(angle),

View File

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/animation.dart';
@ -76,7 +77,6 @@ class Star extends PositionComponent {
Star() {
const smallR = 15.0;
const bigR = 30.0;
const tau = 2 * pi;
shape = Path()..moveTo(bigR, 0);
for (var i = 1; i < 10; i++) {
final r = i.isEven ? bigR : smallR;

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
class SequenceEffectExample extends FlameGame {
static const String description = '''
@ -14,7 +15,6 @@ class SequenceEffectExample extends FlameGame {
@override
Future<void> onLoad() async {
const tau = Transform2D.tau;
EffectController duration(double x) => EffectController(duration: x);
add(
Player()

View File

@ -35,6 +35,7 @@ export 'src/components/nine_tile_box_component.dart';
export 'src/components/parallax_component.dart';
export 'src/components/particle_system_component.dart';
export 'src/components/position_component.dart';
export 'src/components/spawn_component.dart';
export 'src/components/sprite_animation_component.dart';
export 'src/components/sprite_animation_group_component.dart';
export 'src/components/sprite_batch_component.dart';

View File

@ -0,0 +1,3 @@
export 'src/math/random_fallback.dart';
export 'src/math/solve_cubic.dart';
export 'src/math/solve_quadratic.dart';

View File

@ -0,0 +1,135 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/experimental.dart';
import 'package:flame/math.dart';
/// {@template spawn_component}
/// The [SpawnComponent] is a non-visual component which can spawn
/// [PositionComponent]s randomly within a set [area]. If [area] is not set it
/// will use the size of the nearest ancestor that provides a size.
/// [period] will set the static time interval for when it will spawn new
/// components.
/// If you want to use a non static time interval, use the
/// [SpawnComponent.periodRange] constructor.
/// {@endremplate}
class SpawnComponent extends Component {
/// {@macro spawn_component}
SpawnComponent({
required this.factory,
required double period,
this.area,
this.within = true,
Random? random,
super.key,
}) : _period = period,
_random = random ?? randomFallback;
/// Use this constructor if you want your components to spawn within an
/// interval time range.
/// [minPeriod] will be the minimum amount of time before the next component
/// spawns and [maxPeriod] will be the maximum amount of time before it
/// spawns.
SpawnComponent.periodRange({
required this.factory,
required double minPeriod,
required double maxPeriod,
this.area,
this.within = true,
Random? random,
super.key,
}) : _period = minPeriod +
(random ?? randomFallback).nextDouble() * (maxPeriod - minPeriod),
_random = random ?? randomFallback;
/// The function used to create new components to spawn.
///
/// [amount] is the amount of components that the [SpawnComponent] has spawned
/// so far.
PositionComponent Function(int amount) factory;
/// The area where the components should be spawned.
Shape? area;
/// Whether the random point should be within the [area] or along its edges.
bool within;
/// The timer that is used to control when components are spawned.
late final Timer timer;
/// The time between each component is spawned.
double get period => _period;
set period(double newPeriod) {
_period = newPeriod;
timer.limit = _period;
}
double _period;
/// The minimum amount of time that has to pass until the next component is
/// spawned.
double? minPeriod;
/// The maximum amount of time that has to pass until the next component is
/// spawned.
double? maxPeriod;
/// Whether it is spawning components within a random time frame or at a
/// static rate.
bool get hasRandomPeriod => minPeriod != null;
final Random _random;
/// The amount of spawned components.
int amount = 0;
@override
FutureOr<void> onLoad() async {
if (area == null) {
final parentPosition =
ancestors().whereType<PositionProvider>().firstOrNull?.position ??
Vector2.zero();
final parentSize =
ancestors().whereType<ReadOnlySizeProvider>().firstOrNull?.size ??
Vector2.zero();
assert(
!parentSize.isZero(),
'The SpawnComponent needs an ancestor with a size if area is not '
'provided.',
);
area = Rectangle.fromLTWH(
parentPosition.x,
parentPosition.y,
parentSize.x,
parentSize.y,
);
}
void updatePeriod() {
if (hasRandomPeriod) {
period = minPeriod! + _random.nextDouble() * (maxPeriod! - minPeriod!);
}
}
updatePeriod();
final timerComponent = TimerComponent(
period: _period,
repeat: true,
onTick: () {
final component = factory(amount);
component.position = area!.randomPoint(
random: _random,
within: within,
);
parent?.add(component);
updatePeriod();
amount++;
},
);
timer = timerComponent.timer;
add(timerComponent);
}
}

View File

@ -1,4 +1,5 @@
import 'dart:math' as math;
import 'package:flame/geometry.dart';
import 'package:flame/src/effects/controllers/duration_effect_controller.dart';
import 'package:flame/src/effects/controllers/infinite_effect_controller.dart';
import 'package:flame/src/effects/controllers/repeated_effect_controller.dart';
@ -17,7 +18,6 @@ class SineEffectController extends DurationEffectController {
@override
double get progress {
const tau = math.pi * 2;
return math.sin(tau * timer / duration);
}
}

View File

@ -2,9 +2,11 @@ import 'dart:math';
import 'dart:ui';
import 'package:flame/geometry.dart';
import 'package:flame/math.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/extensions/vector2.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:flame/src/math/random_fallback.dart';
/// The circle with a given [center] and a [radius].
///
@ -49,7 +51,11 @@ class Circle extends Shape {
@override
bool containsPoint(Vector2 point) {
return (point - _center).length2 <= _radius * _radius;
return (_tmpResult
..setFrom(point)
..sub(_center))
.length2 <=
_radius * _radius;
}
@override
@ -97,6 +103,17 @@ class Circle extends Shape {
..add(_center);
}
@override
Vector2 randomPoint({Random? random, bool within = true}) {
final randomGenerator = random ?? randomFallback;
final theta = randomGenerator.nextDouble() * tau;
final radius = within ? randomGenerator.nextDouble() * _radius : _radius;
final x = radius * cos(theta);
final y = radius * sin(theta);
return Vector2(_center.x + x, _center.y + y);
}
@override
String toString() => 'Circle([${_center.x}, ${_center.y}], $_radius)';

View File

@ -1,9 +1,12 @@
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:flame/components.dart';
import 'package:flame/math.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:flame/src/math/tmp_vector2.dart';
/// An arbitrary polygon with 3 or more vertices.
///
@ -241,4 +244,62 @@ class Polygon extends Shape {
@override
String toString() => 'Polygon($vertices)';
@override
Vector2 randomPoint({Random? random, bool within = true}) {
final randomGenerator = random ?? randomFallback;
if (within) {
final result = Vector2.zero();
final min = aabb.min;
final max = aabb.max;
while (true) {
final randomX = min.x + randomGenerator.nextDouble() * (max.x - min.x);
final randomY = min.y + randomGenerator.nextDouble() * (max.y - min.y);
result.setValues(randomX, randomY);
if (containsPoint(result)) {
return result;
}
}
} else {
return Polygon.randomPointAlongEdges(_vertices, random: randomGenerator);
}
}
/// Returns a random point on the [vertices].
static Vector2 randomPointAlongEdges(
List<Vector2> vertices, {
Random? random,
}) {
final randomGenerator = random ?? randomFallback;
final verticesLengths = <double>[];
var totalLength = 0.0;
for (final (i, startPoint) in vertices.indexed) {
final endPoint = vertices[(i + 1) % vertices.length];
final length = startPoint.distanceTo(endPoint);
verticesLengths.add(length);
totalLength += length;
}
final pointOnEdges = randomGenerator.nextDouble() * totalLength;
var vertexIndex = 0;
var currentEndPoint = 0.0;
late final double localEdgePoint;
while (vertexIndex < verticesLengths.length) {
final lastEndPoint = currentEndPoint;
currentEndPoint += verticesLengths[vertexIndex];
if (currentEndPoint >= pointOnEdges) {
localEdgePoint = pointOnEdges - lastEndPoint;
break;
}
vertexIndex++;
}
final startPoint = vertices[vertexIndex];
final endPoint = vertices[(vertexIndex + 1) % vertices.length];
tmpVector2
..setFrom(endPoint)
..sub(startPoint)
..scaleTo(localEdgePoint);
return startPoint + tmpVector2;
}
}

View File

@ -1,10 +1,13 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/extensions.dart';
import 'package:flame/geometry.dart';
import 'package:flame/src/experimental/geometry/shapes/polygon.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:flame/src/math/random_fallback.dart';
import 'package:flutter/cupertino.dart';
/// An axis-aligned rectangle.
///
@ -161,6 +164,19 @@ class Rectangle extends Shape {
return edges.expand((e) => e.intersections(line)).toSet();
}
@override
Vector2 randomPoint({Random? random, bool within = true}) {
final randomGenerator = random ?? randomFallback;
if (within) {
return Vector2(
left + randomGenerator.nextDouble() * width,
top + randomGenerator.nextDouble() * height,
);
} else {
return Polygon.randomPointAlongEdges(vertices, random: randomGenerator);
}
}
/// The 4 edges of this rectangle, returned in a clockwise fashion.
List<LineSegment> get edges => [topEdge, rightEdge, bottomEdge, leftEdge];

View File

@ -1,9 +1,10 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/geometry.dart';
import 'package:flame/math.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
/// An axis-aligned rectangle with rounded corners.
@ -197,7 +198,27 @@ class RoundedRectangle extends Shape {
@override
String toString() =>
'RoundedRectangle([$_left, $_top], [$_right, $_bottom], $_radius)';
}
@internal
const tau = Transform2D.tau; // 2π
@override
Vector2 randomPoint({Random? random, bool within = true}) {
assert(
within,
'It is not possible to get a point only along the edges of a '
'rounded rectangle.',
);
final randomGenerator = random ?? randomFallback;
final result = Vector2.zero();
final min = aabb.min;
final max = aabb.max;
while (true) {
final randomX = min.x + randomGenerator.nextDouble() * (max.x - min.x);
final randomY = min.y + randomGenerator.nextDouble() * (max.y - min.y);
result.setValues(randomX, randomY);
if (containsPoint(result)) {
return result;
}
}
}
}

View File

@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/src/experimental/geometry/shapes/circle.dart';
@ -93,4 +94,11 @@ abstract class Shape {
/// not get ownership of the returned object: they must treat it as an
/// immutable short-lived object.
Vector2 nearestPoint(Vector2 point);
/// Returns a random point within the shape if [within] is true (default) and
/// otherwise a point along the edges of the shape.
/// Do note that [within]=true also includes the edges.
///
/// If [isClosed] is false, the [within] value does not make a difference.
Vector2 randomPoint({Random? random, bool within = true});
}

View File

@ -1,3 +1,7 @@
import 'dart:math';
import 'package:flame/math.dart';
extension ListExtension<E> on List<E> {
/// Reverses the list in-place.
void reverse() {
@ -7,4 +11,11 @@ extension ListExtension<E> on List<E> {
this[j] = temp;
}
}
/// Returns a random element from the list.
E random([Random? random]) {
assert(isNotEmpty, "Can't get a random element from an empty list");
final randomGenerator = random ?? randomFallback;
return this[randomGenerator.nextInt(length)];
}
}

View File

@ -1,4 +1,4 @@
import 'dart:math' show min, max;
import 'dart:math' show Random, max, min;
import 'dart:math' as math;
import 'dart:ui';
@ -7,6 +7,7 @@ import 'package:flame/geometry.dart';
import 'package:flame/src/extensions/matrix4.dart';
import 'package:flame/src/extensions/offset.dart';
import 'package:flame/src/extensions/vector2.dart';
import 'package:flame/src/math/random_fallback.dart';
export 'dart:ui' show Rect;
@ -79,6 +80,15 @@ extension RectExtension on Rect {
);
}
/// Generates a random point within the bounds of this [Rect].
Vector2 randomPoint([Random? random]) {
final randomGenerator = random ?? randomFallback;
return Vector2(
left + randomGenerator.nextDouble() * width,
top + randomGenerator.nextDouble() * height,
);
}
/// Creates a [Rect] that represents the bounds of the list [pts].
static Rect getBounds(List<Vector2> pts) {
final xPoints = pts.map((e) => e.x);

View File

@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:flame/geometry.dart' as geometry;
import 'package:flame/src/game/notifying_vector2.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
@ -34,6 +35,8 @@ class Transform2D extends ChangeNotifier {
final NotifyingVector2 _position;
final NotifyingVector2 _scale;
final NotifyingVector2 _offset;
@Deprecated('Use tau from the package:flame/geometry.dart export instead, '
'this field will be removed in Flame v1.10.0')
static const tau = 2 * math.pi;
Transform2D()
@ -73,9 +76,10 @@ class Transform2D extends ChangeNotifier {
///
/// The [tolerance] parameter is in absolute units, not relative.
bool closeTo(Transform2D other, {double tolerance = 1e-10}) {
final deltaAngle = (angle - other.angle) % tau;
final deltaAngle = (angle - other.angle) % geometry.tau;
assert(deltaAngle >= 0);
return (deltaAngle <= tolerance || deltaAngle >= tau - tolerance) &&
return (deltaAngle <= tolerance ||
deltaAngle >= geometry.tau - tolerance) &&
(position.x - other.position.x).abs() <= tolerance &&
(position.y - other.position.y).abs() <= tolerance &&
(scale.x - other.scale.x).abs() <= tolerance &&
@ -110,9 +114,9 @@ class Transform2D extends ChangeNotifier {
}
/// Similar to [angle], but uses degrees instead of radians.
double get angleDegrees => _angle * (360 / tau);
double get angleDegrees => _angle * (360 / geometry.tau);
set angleDegrees(double a) {
_angle = a * (tau / 360);
_angle = a * (geometry.tau / 360);
_markAsModified();
}

View File

@ -4,7 +4,7 @@ import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/geometry.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/utils/solve_quadratic.dart';
import 'package:flame/src/math/solve_quadratic.dart';
import 'package:meta/meta.dart';
class CircleComponent extends ShapeComponent implements SizeProvider {

View File

@ -0,0 +1,5 @@
import 'dart:math';
/// When you don't care about what [Random] object you have and don't want to
/// create an unnecessary object you can use this pre-created object.
final Random randomFallback = Random();

View File

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flame/src/utils/solve_quadratic.dart';
import 'package:flame/geometry.dart';
import 'package:flame/src/math/solve_quadratic.dart';
/// Solves cubic equation `ax³ + bx² + cx + d == 0`.
///
@ -56,4 +57,3 @@ double _cubicRoot(double x) {
}
const discriminantEpsilon = 1e-15;
const tau = 2 * pi;

View File

@ -0,0 +1,7 @@
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
/// Use internally when you need a temporary [Vector2] object but don't want to
/// instantiate a new one due to performance.
@internal
final Vector2 tmpVector2 = Vector2.zero();

View File

@ -1,6 +1,6 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/geometry.dart';
import 'package:flame/src/rendering/decorator.dart';
import 'package:vector_math/vector_math_64.dart';
@ -46,7 +46,6 @@ class Rotate3DDecorator extends Decorator {
/// "back" side is shows if the component is rotated 180º degree around either
/// the X or Y axis.
bool get isFlipped {
const tau = 2 * pi;
final phaseX = (angleX / tau - 0.25) % 1.0;
final phaseY = (angleY / tau - 0.25) % 1.0;
return (phaseX > 0.5) ^ (phaseY > 0.5);

View File

@ -0,0 +1,6 @@
@Deprecated(
'Import math.dart instead, this file will be removed in a Flame v1.10.0',
)
export 'src/math/random_fallback.dart';
export 'src/math/solve_cubic.dart';
export 'src/math/solve_quadratic.dart';

View File

@ -5,6 +5,7 @@ import 'package:canvas_test/canvas_test.dart';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';
@ -1001,7 +1002,7 @@ void main() {
const h = 2.0;
final component = PositionComponent(size: Vector2(w, h));
for (var i = 0; i < 10; i++) {
final a = (i / 10) * Transform2D.tau / 4;
final a = (i / 10) * tau / 4;
component.angle = a;
expect(
component.toRect(),

View File

@ -0,0 +1,111 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';
void main() {
group('SpawnComponent', () {
testWithFlameGame('Spawns components within rectangle', (game) async {
final random = Random(0);
final shape = Rectangle.fromCenter(
center: Vector2(100, 200),
size: Vector2.all(200),
);
final spawn = SpawnComponent(
factory: (_) => PositionComponent(),
period: 1,
area: shape,
random: random,
);
await game.ensureAdd(spawn);
game.update(0.5);
expect(game.children.length, 1);
game.update(0.5);
game.update(0.0);
expect(game.children.length, 2);
game.update(1.0);
game.update(0.0);
expect(game.children.length, 3);
for (var i = 0; i < 1000; i++) {
game.update(random.nextDouble());
}
expect(
game.children
.query<PositionComponent>()
.every((c) => shape.containsPoint(c.position)),
isTrue,
);
});
testWithFlameGame('Spawns components within circle', (game) async {
final random = Random(0);
final shape = Circle(Vector2(100, 200), 100);
expect(shape.containsPoint(Vector2.all(200)), isTrue);
final spawn = SpawnComponent(
factory: (_) => PositionComponent(),
period: 1,
area: shape,
random: random,
);
await game.ensureAdd(spawn);
game.update(0.5);
expect(game.children.length, 1);
game.update(0.5);
game.update(0.0);
expect(game.children.length, 2);
game.update(1.0);
game.update(0.0);
expect(game.children.length, 3);
for (var i = 0; i < 1000; i++) {
game.update(random.nextDouble());
}
expect(
game.children
.query<PositionComponent>()
.every((c) => shape.containsPoint(c.position)),
isTrue,
);
});
testWithFlameGame('Spawns components within polygon', (game) async {
final random = Random(0);
final shape = Polygon(
[
Vector2(100, 100),
Vector2(200, 100),
Vector2(150, 200),
],
);
expect(shape.containsPoint(Vector2.all(150)), isTrue);
final spawn = SpawnComponent(
factory: (_) => PositionComponent(),
period: 1,
area: shape,
random: random,
);
await game.ensureAdd(spawn);
game.update(0.5);
expect(game.children.length, 1);
game.update(0.5);
game.update(0.0);
expect(game.children.length, 2);
game.update(1.0);
game.update(0.0);
expect(game.children.length, 3);
for (var i = 0; i < 1000; i++) {
game.update(random.nextDouble());
}
expect(
game.children
.query<PositionComponent>()
.every((c) => shape.containsPoint(c.position)),
isTrue,
);
});
});
}

View File

@ -2,7 +2,7 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/src/effects/measurable_effect.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
@ -91,7 +91,6 @@ void main() {
});
testWithFlameGame('speed on RotateEffect', (game) async {
const tau = Transform2D.tau;
final effect = RotateEffect.to(tau, EffectController(speed: 1));
final component = PositionComponent(position: Vector2(5, 8));
component.add(effect);

View File

@ -3,14 +3,13 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('MoveAlongPathEffect', () {
testWithFlameGame('relative path', (game) async {
const tau = Transform2D.tau;
const x0 = 32.5;
const y0 = 14.88;
final component = PositionComponent(position: Vector2(x0, y0));

View File

@ -0,0 +1,26 @@
import 'dart:math';
import 'package:flame/extensions.dart';
import 'package:test/test.dart';
void main() {
group('ListExtension', () {
test('reverse', () {
final list = [1, 3, 3, 7];
list.reverse();
expect(list, [7, 3, 3, 1]);
list.insert(1, 4);
list.reverse();
expect(list, [1, 3, 3, 4, 7]);
});
test('random', () {
final list = [1, 3, 3, 7];
final random = Random(0);
final element1 = list.random(random);
expect(element1, 7);
final element2 = list.random(random);
expect(element2, 3);
});
});
}

View File

@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:flame/geometry.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:test/test.dart';
import 'package:vector_math/vector_math_64.dart';
@ -90,7 +91,6 @@ void main() {
});
test('angle', () {
const tau = Transform2D.tau;
final t = Transform2D();
t.angle = tau / 6;
expect(t.angleDegrees, closeTo(60, 1e-10));

View File

@ -1,4 +1,4 @@
import 'package:flame/src/utils/solve_cubic.dart';
import 'package:flame/math.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';

View File

@ -1,6 +1,6 @@
import 'dart:math';
import 'package:flame/src/utils/solve_quadratic.dart';
import 'package:flame/math.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';