mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: SpawnComponent (#2709)
This PR introduces the `SpawnComponent`, which randomly spawns components within a set area.
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(
|
||||
|
||||
60
examples/lib/stories/components/spawn_component_example.dart
Normal file
60
examples/lib/stories/components/spawn_component_example.dart
Normal 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,
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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';
|
||||
|
||||
3
packages/flame/lib/math.dart
Normal file
3
packages/flame/lib/math.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'src/math/random_fallback.dart';
|
||||
export 'src/math/solve_cubic.dart';
|
||||
export 'src/math/solve_quadratic.dart';
|
||||
135
packages/flame/lib/src/components/spawn_component.dart
Normal file
135
packages/flame/lib/src/components/spawn_component.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)';
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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});
|
||||
}
|
||||
|
||||
@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
5
packages/flame/lib/src/math/random_fallback.dart
Normal file
5
packages/flame/lib/src/math/random_fallback.dart
Normal 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();
|
||||
@ -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;
|
||||
7
packages/flame/lib/src/math/tmp_vector2.dart
Normal file
7
packages/flame/lib/src/math/tmp_vector2.dart
Normal 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();
|
||||
@ -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);
|
||||
|
||||
6
packages/flame/lib/util.dart
Normal file
6
packages/flame/lib/util.dart
Normal 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';
|
||||
@ -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(),
|
||||
|
||||
111
packages/flame/test/components/spawn_component_test.dart
Normal file
111
packages/flame/test/components/spawn_component_test.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
26
packages/flame/test/extensions/list_test.dart
Normal file
26
packages/flame/test/extensions/list_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user