diff --git a/doc/flame/components.md b/doc/flame/components.md index 655339a278..06afa86c02 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -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 ``` +## 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) diff --git a/doc/flame/examples/lib/drag_events.dart b/doc/flame/examples/lib/drag_events.dart index 625745fd95..1ae5701c00 100644 --- a/doc/flame/examples/lib/drag_events.dart +++ b/doc/flame/examples/lib/drag_events.dart @@ -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; diff --git a/doc/flame/examples/lib/router.dart b/doc/flame/examples/lib/router.dart index fd91457b83..bfb854050b 100644 --- a/doc/flame/examples/lib/router.dart +++ b/doc/flame/examples/lib/router.dart @@ -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); } } diff --git a/doc/flame/examples/lib/value_route.dart b/doc/flame/examples/lib/value_route.dart index e34a618fc6..20af955d5a 100644 --- a/doc/flame/examples/lib/value_route.dart +++ b/doc/flame/examples/lib/value_route.dart @@ -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; diff --git a/examples/lib/stories/camera_and_viewport/camera_component_example.dart b/examples/lib/stories/camera_and_viewport/camera_component_example.dart index a107bc8044..76f4ae9215 100644 --- a/examples/lib/stories/camera_and_viewport/camera_component_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera_component_example.dart @@ -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; diff --git a/examples/lib/stories/components/components.dart b/examples/lib/stories/components/components.dart index ed0c31c28e..1b7aad0aed 100644 --- a/examples/lib/stories/components/components.dart +++ b/examples/lib/stories/components/components.dart @@ -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( diff --git a/examples/lib/stories/components/spawn_component_example.dart b/examples/lib/stories/components/spawn_component_example.dart new file mode 100644 index 0000000000..315496b045 --- /dev/null +++ b/examples/lib/stories/components/spawn_component_example.dart @@ -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, +} diff --git a/examples/lib/stories/effects/move_effect_example.dart b/examples/lib/stories/effects/move_effect_example.dart index 56396968e6..025b603211 100644 --- a/examples/lib/stories/effects/move_effect_example.dart +++ b/examples/lib/stories/effects/move_effect_example.dart @@ -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, diff --git a/examples/lib/stories/effects/rotate_effect_example.dart b/examples/lib/stories/effects/rotate_effect_example.dart index ce18df38bf..51d32dd6f6 100644 --- a/examples/lib/stories/effects/rotate_effect_example.dart +++ b/examples/lib/stories/effects/rotate_effect_example.dart @@ -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 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), diff --git a/examples/lib/stories/effects/scale_effect_example.dart b/examples/lib/stories/effects/scale_effect_example.dart index 866fed31c4..3f6921e9f1 100644 --- a/examples/lib/stories/effects/scale_effect_example.dart +++ b/examples/lib/stories/effects/scale_effect_example.dart @@ -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; diff --git a/examples/lib/stories/effects/sequence_effect_example.dart b/examples/lib/stories/effects/sequence_effect_example.dart index 5995837cd4..ea3f15e73b 100644 --- a/examples/lib/stories/effects/sequence_effect_example.dart +++ b/examples/lib/stories/effects/sequence_effect_example.dart @@ -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 onLoad() async { - const tau = Transform2D.tau; EffectController duration(double x) => EffectController(duration: x); add( Player() diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index ecd77d16a7..49855532ba 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -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'; diff --git a/packages/flame/lib/math.dart b/packages/flame/lib/math.dart new file mode 100644 index 0000000000..10309074e8 --- /dev/null +++ b/packages/flame/lib/math.dart @@ -0,0 +1,3 @@ +export 'src/math/random_fallback.dart'; +export 'src/math/solve_cubic.dart'; +export 'src/math/solve_quadratic.dart'; diff --git a/packages/flame/lib/src/components/spawn_component.dart b/packages/flame/lib/src/components/spawn_component.dart new file mode 100644 index 0000000000..4c72abaa65 --- /dev/null +++ b/packages/flame/lib/src/components/spawn_component.dart @@ -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 onLoad() async { + if (area == null) { + final parentPosition = + ancestors().whereType().firstOrNull?.position ?? + Vector2.zero(); + final parentSize = + ancestors().whereType().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); + } +} diff --git a/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart b/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart index ccd0a85982..4b69d7073e 100644 --- a/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart +++ b/packages/flame/lib/src/effects/controllers/sine_effect_controller.dart @@ -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); } } diff --git a/packages/flame/lib/src/experimental/geometry/shapes/circle.dart b/packages/flame/lib/src/experimental/geometry/shapes/circle.dart index 1a3e999133..9b7e859162 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/circle.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/circle.dart @@ -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)'; diff --git a/packages/flame/lib/src/experimental/geometry/shapes/polygon.dart b/packages/flame/lib/src/experimental/geometry/shapes/polygon.dart index 6d32764437..809788b960 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/polygon.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/polygon.dart @@ -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 vertices, { + Random? random, + }) { + final randomGenerator = random ?? randomFallback; + final verticesLengths = []; + 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; + } } diff --git a/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart b/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart index 41bb076dc7..b390ec5795 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart @@ -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 get edges => [topEdge, rightEdge, bottomEdge, leftEdge]; diff --git a/packages/flame/lib/src/experimental/geometry/shapes/rounded_rectangle.dart b/packages/flame/lib/src/experimental/geometry/shapes/rounded_rectangle.dart index 7af244fb5f..00d652756d 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/rounded_rectangle.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/rounded_rectangle.dart @@ -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; + } + } + } +} diff --git a/packages/flame/lib/src/experimental/geometry/shapes/shape.dart b/packages/flame/lib/src/experimental/geometry/shapes/shape.dart index abaca8fa69..e905e5c998 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/shape.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/shape.dart @@ -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}); } diff --git a/packages/flame/lib/src/extensions/list.dart b/packages/flame/lib/src/extensions/list.dart index 9ff1f802d2..2952ff8516 100644 --- a/packages/flame/lib/src/extensions/list.dart +++ b/packages/flame/lib/src/extensions/list.dart @@ -1,3 +1,7 @@ +import 'dart:math'; + +import 'package:flame/math.dart'; + extension ListExtension on List { /// Reverses the list in-place. void reverse() { @@ -7,4 +11,11 @@ extension ListExtension on List { 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)]; + } } diff --git a/packages/flame/lib/src/extensions/rect.dart b/packages/flame/lib/src/extensions/rect.dart index 462a409058..063a1d3d53 100644 --- a/packages/flame/lib/src/extensions/rect.dart +++ b/packages/flame/lib/src/extensions/rect.dart @@ -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 pts) { final xPoints = pts.map((e) => e.x); diff --git a/packages/flame/lib/src/game/transform2d.dart b/packages/flame/lib/src/game/transform2d.dart index b957d27a47..3d0f868d56 100644 --- a/packages/flame/lib/src/game/transform2d.dart +++ b/packages/flame/lib/src/game/transform2d.dart @@ -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(); } diff --git a/packages/flame/lib/src/geometry/circle_component.dart b/packages/flame/lib/src/geometry/circle_component.dart index 34d90b5175..1cfb56d76f 100644 --- a/packages/flame/lib/src/geometry/circle_component.dart +++ b/packages/flame/lib/src/geometry/circle_component.dart @@ -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 { diff --git a/packages/flame/lib/src/math/random_fallback.dart b/packages/flame/lib/src/math/random_fallback.dart new file mode 100644 index 0000000000..3666100eaf --- /dev/null +++ b/packages/flame/lib/src/math/random_fallback.dart @@ -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(); diff --git a/packages/flame/lib/src/utils/solve_cubic.dart b/packages/flame/lib/src/math/solve_cubic.dart similarity index 95% rename from packages/flame/lib/src/utils/solve_cubic.dart rename to packages/flame/lib/src/math/solve_cubic.dart index 6d40bdd970..9cc332f889 100644 --- a/packages/flame/lib/src/utils/solve_cubic.dart +++ b/packages/flame/lib/src/math/solve_cubic.dart @@ -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; diff --git a/packages/flame/lib/src/utils/solve_quadratic.dart b/packages/flame/lib/src/math/solve_quadratic.dart similarity index 100% rename from packages/flame/lib/src/utils/solve_quadratic.dart rename to packages/flame/lib/src/math/solve_quadratic.dart diff --git a/packages/flame/lib/src/math/tmp_vector2.dart b/packages/flame/lib/src/math/tmp_vector2.dart new file mode 100644 index 0000000000..d3d112550d --- /dev/null +++ b/packages/flame/lib/src/math/tmp_vector2.dart @@ -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(); diff --git a/packages/flame/lib/src/rendering/rotate3d_decorator.dart b/packages/flame/lib/src/rendering/rotate3d_decorator.dart index bba660fa9c..83e0030aa7 100644 --- a/packages/flame/lib/src/rendering/rotate3d_decorator.dart +++ b/packages/flame/lib/src/rendering/rotate3d_decorator.dart @@ -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); diff --git a/packages/flame/lib/util.dart b/packages/flame/lib/util.dart new file mode 100644 index 0000000000..717d52013d --- /dev/null +++ b/packages/flame/lib/util.dart @@ -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'; diff --git a/packages/flame/test/components/position_component_test.dart b/packages/flame/test/components/position_component_test.dart index a9324b7b7d..38d3e072c4 100644 --- a/packages/flame/test/components/position_component_test.dart +++ b/packages/flame/test/components/position_component_test.dart @@ -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(), diff --git a/packages/flame/test/components/spawn_component_test.dart b/packages/flame/test/components/spawn_component_test.dart new file mode 100644 index 0000000000..3f44f98696 --- /dev/null +++ b/packages/flame/test/components/spawn_component_test.dart @@ -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() + .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() + .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() + .every((c) => shape.containsPoint(c.position)), + isTrue, + ); + }); + }); +} diff --git a/packages/flame/test/effects/controllers/speed_effect_controller_test.dart b/packages/flame/test/effects/controllers/speed_effect_controller_test.dart index 100562634b..55b9d3c35a 100644 --- a/packages/flame/test/effects/controllers/speed_effect_controller_test.dart +++ b/packages/flame/test/effects/controllers/speed_effect_controller_test.dart @@ -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); diff --git a/packages/flame/test/effects/move_along_path_effect_test.dart b/packages/flame/test/effects/move_along_path_effect_test.dart index c46b5a0612..9927f23e02 100644 --- a/packages/flame/test/effects/move_along_path_effect_test.dart +++ b/packages/flame/test/effects/move_along_path_effect_test.dart @@ -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)); diff --git a/packages/flame/test/extensions/list_test.dart b/packages/flame/test/extensions/list_test.dart new file mode 100644 index 0000000000..310d7e6ed4 --- /dev/null +++ b/packages/flame/test/extensions/list_test.dart @@ -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); + }); + }); +} diff --git a/packages/flame/test/game/transform2d_test.dart b/packages/flame/test/game/transform2d_test.dart index d68ab15a82..bfc5e5f660 100644 --- a/packages/flame/test/game/transform2d_test.dart +++ b/packages/flame/test/game/transform2d_test.dart @@ -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)); diff --git a/packages/flame/test/utils/recycled_queue_test.dart b/packages/flame/test/math/recycled_queue_test.dart similarity index 100% rename from packages/flame/test/utils/recycled_queue_test.dart rename to packages/flame/test/math/recycled_queue_test.dart diff --git a/packages/flame/test/utils/solve_cubic_test.dart b/packages/flame/test/math/solve_cubic_test.dart similarity index 98% rename from packages/flame/test/utils/solve_cubic_test.dart rename to packages/flame/test/math/solve_cubic_test.dart index b4a72e5bda..47e7802fc4 100644 --- a/packages/flame/test/utils/solve_cubic_test.dart +++ b/packages/flame/test/math/solve_cubic_test.dart @@ -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'; diff --git a/packages/flame/test/utils/solve_quadratic_test.dart b/packages/flame/test/math/solve_quadratic_test.dart similarity index 97% rename from packages/flame/test/utils/solve_quadratic_test.dart rename to packages/flame/test/math/solve_quadratic_test.dart index 15f607b9f2..9e39569b56 100644 --- a/packages/flame/test/utils/solve_quadratic_test.dart +++ b/packages/flame/test/math/solve_quadratic_test.dart @@ -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';