feat: adding ParticleSystemComponent (#1489)

This commit is contained in:
Erick
2022-03-26 12:20:31 -03:00
committed by GitHub
parent a3b7de0acd
commit 6891eaaa24
8 changed files with 231 additions and 50 deletions

View File

@ -1,7 +1,7 @@
# Particles
Flame offers a basic, yet robust and extendable particle system. The core concept of this system is
the `Particle` class, which is very similar in its behavior to the `ParticleComponent`.
the `Particle` class, which is very similar in its behavior to the `ParticleSystemComponent`.
The most basic usage of a `Particle` with `FlameGame` would look as following:
@ -11,11 +11,11 @@ import 'package:flame/components.dart';
// ...
game.add(
// Wrapping a Particle with ParticleComponent
// Wrapping a Particle with ParticleSystemComponent
// which maps Component lifecycle hooks to Particle ones
// and embeds a trigger for removing the component.
ParticleComponent(
CircleParticle(),
ParticleSystemComponent(
particle: CircleParticle(),
),
);
```
@ -42,13 +42,13 @@ Vector2 randomVector2() => (Vector2.random(rnd) - Vector2.random(rnd)) * 200;
// Composition.
//
// Defining a particle effect as a set of nested behaviors from top to bottom, one within another:
// ParticleComponent
// ParticleSystemComponent
// > ComposedParticle
// > AcceleratedParticle
// > CircleParticle
game.add(
ParticleComponent(
Particle.generate(
ParticleSystemComponent(
particle: Particle.generate(
count: 10,
generator: (i) => AcceleratedParticle(
acceleration: randomVector2(),
@ -65,8 +65,8 @@ game.add(
// Expresses the same behavior as above, but with a more fluent API.
// Only Particles with SingleChildParticle mixin can be used as chainable behaviors.
game.add(
ParticleComponent(
Particle.generate(
ParticleSystemComponent(
particle: Particle.generate(
count: 10,
generator: (i) => pt.CircleParticle(paint: Paint()..color = Colors.red)
)
@ -78,8 +78,8 @@ game.add(
// All the behaviors are defined explicitly. Offers greater flexibility
// compared to built-in behaviors.
game.add(
ParticleComponent(
Particle.generate(
ParticleSystemComponent(
particle: Particle.generate(
count: 10,
generator: (i) {
Vector2 position = Vector2.zero();
@ -107,10 +107,10 @@ You can find more examples of how to use different built-in particles in various
## Lifecycle
A behavior common to all `Particle`s is that all of them accept a `lifespan` argument. This value is
used to make the `ParticleComponent` remove itself once its internal `Particle` has reached the end
of its life. Time within the `Particle` itself is tracked using the Flame `Timer` class. It can be
configured with a `double`, represented in seconds (with microsecond precision) by passing it into
the corresponding `Particle` constructor.
used to make the `ParticleSystemComponent` remove itself once its internal `Particle` has reached
the end of its life. Time within the `Particle` itself is tracked using the Flame `Timer` class. It
can be configured with a `double`, represented in seconds (with microsecond precision) by passing
it into the corresponding `Particle` constructor.
```dart
Particle(lifespan: .2); // will live for 200ms.
@ -134,7 +134,7 @@ as the `value` property of the `AnimationController` class in Flutter.
```dart
final particle = Particle(lifespan: 2.0);
game.add(ParticleComponent(particle));
game.add(ParticleSystemComponent(particle: particle));
// Will print values from 0 to 1 with step of .1: 0, 0.1, 0.2 ... 0.9, 1.0.
Timer.periodic(duration * .1, () => print(particle.progress));
@ -172,8 +172,8 @@ layer.
```dart
game.add(
ParticleComponent(
TranslatedParticle(
ParticleSystemComponent(
particle: TranslatedParticle(
// Will translate the child Particle effect to the center of game canvas.
offset: game.size / 2,
child: Particle(),
@ -189,8 +189,8 @@ Moves the child `Particle` between the `from` and `to` `Vector2`s during its lif
```dart
game.add(
ParticleComponent(
MovingParticle(
ParticleSystemComponent(
particle: MovingParticle(
// Will move from corner to corner of the game canvas.
from: Vector2.zero(),
to: game.size,
@ -216,8 +216,8 @@ final rnd = Random();
Vector2 randomVector2() => (Vector2.random(rnd) - Vector2.random(rnd)) * 100;
game.add(
ParticleComponent(
AcceleratedParticle(
ParticleSystemComponent(
particle: AcceleratedParticle(
// Will fire off in the center of game canvas
position: game.canvasSize/2,
// With random initial speed of Vector2(-100..100, 0..-100)
@ -241,8 +241,8 @@ desired positioning.
```dart
game.add(
ParticleComponent(
CircleParticle(
ParticleSystemComponent(
particle: CircleParticle(
radius: game.size.x / 2,
paint: Paint()..color = Colors.red.withOpacity(.5),
),
@ -256,8 +256,8 @@ Allows you to embed a `Sprite` into your particle effects.
```dart
game.add(
ParticleComponent(
SpriteParticle(
ParticleSystemComponent(
particle: SpriteParticle(
sprite: Sprite('sprite.png'),
size: Vector2(64, 64),
),
@ -281,8 +281,8 @@ await Flame.images.loadAll(const [
final image = await Flame.images.load('image.png');
game.add(
ParticleComponent(
ImageParticle(
ParticleSystemComponent(
particle: ImageParticle(
size: Vector2.all(24),
image: image,
);
@ -303,8 +303,8 @@ final spritesheet = SpriteSheet(
);
game.add(
ParticleComponent(
AnimationParticle(
ParticleSystemComponent(
particle: AnimationParticle(
animation: spritesheet.createAnimation(0, stepTime: 0.1),
);
),
@ -322,8 +322,8 @@ to the `game` directly, without the `Particle` in the middle.
final longLivingRect = RectComponent();
game.add(
ParticleComponent(
ComponentParticle(
ParticleSystemComponent(
particle: ComponentParticle(
component: longLivingRect
);
),
@ -362,8 +362,8 @@ flareAnimation.height = flareSize;
// Somewhere in game
game.add(
ParticleComponent(
FlareParticle(flare: flareAnimation),
ParticleSystemComponent(
particle: FlareParticle(flare: flareAnimation),
),
);
```
@ -380,9 +380,9 @@ on each frame to perform necessary computations and render something to the `Can
```dart
game.add(
ParticleComponent(
ParticleSystemComponent(
// Renders a circle which gradually changes its color and size during the particle lifespan.
ComputedParticle(
particle: ComputedParticle(
renderer: (canvas, particle) => canvas.drawCircle(
Offset.zero,
particle.progress * 10,

View File

@ -96,8 +96,8 @@ class ParticlesExample extends FlameGame with FPSCounter {
add(
// Bind all the particles to a [Component] update
// lifecycle from the [FlameGame].
ParticleComponent(
TranslatedParticle(
ParticleSystemComponent(
particle: TranslatedParticle(
lifespan: 1,
offset: cellCenter,
child: particle,

View File

@ -0,0 +1,59 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/particles.dart';
import 'package:flutter/material.dart';
class ParticlesInteractiveExample extends FlameGame with PanDetector {
static const description = 'An example which shows how '
'ParticleSystemComponent can be added in runtime '
'following an event, in this example, the mouse '
'dragging';
final random = Random();
final Tween<double> noise = Tween(begin: -1, end: 1);
final ColorTween colorTween;
final double zoom;
ParticlesInteractiveExample({
required Color from,
required Color to,
required this.zoom,
}) : colorTween = ColorTween(begin: from, end: to);
@override
Future<void> onLoad() async {
camera.followVector2(Vector2.zero());
camera.zoom = zoom;
}
@override
void onPanUpdate(DragUpdateInfo info) {
add(
ParticleSystemComponent(
position: info.eventPosition.game,
particle: Particle.generate(
count: 40,
generator: (i) {
return AcceleratedParticle(
lifespan: 2,
speed: Vector2(
noise.transform(random.nextDouble()),
noise.transform(random.nextDouble()),
) *
i.toDouble(),
child: CircleParticle(
radius: 2,
paint: Paint()
..color = colorTween.transform(random.nextDouble())!,
),
);
},
),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import '../../commons/commons.dart';
import 'flip_sprite_example.dart';
@ -7,6 +8,7 @@ import 'isometric_tile_map_example.dart';
import 'layers_example.dart';
import 'nine_tile_box_example.dart';
import 'particles_example.dart';
import 'particles_interactive_example.dart';
import 'text_example.dart';
void addRenderingStories(Dashbook dashbook) {
@ -46,5 +48,17 @@ void addRenderingStories(Dashbook dashbook) {
(_) => GameWidget(game: ParticlesExample()),
codeLink: baseLink('rendering/particles_example.dart'),
info: ParticlesExample.description,
)
..add(
'Particles (Interactive)',
(context) => GameWidget(
game: ParticlesInteractiveExample(
from: context.colorProperty('From color', Colors.pink),
to: context.colorProperty('To color', Colors.blue),
zoom: context.numberProperty('Zoom', 1),
),
),
codeLink: baseLink('rendering/particles_interactive_example.dart'),
info: ParticlesInteractiveExample.description,
);
}

View File

@ -18,6 +18,7 @@ export 'src/components/mixins/tappable.dart';
export 'src/components/nine_tile_box_component.dart';
export 'src/components/parallax_component.dart';
export 'src/components/particle_component.dart';
export 'src/components/particle_system_component.dart';
export 'src/components/position_component.dart';
export 'src/components/position_type.dart';
export 'src/components/sprite_animation_component.dart';

View File

@ -7,6 +7,7 @@ import 'component.dart';
/// to a [Component] tree. Could be added either to FlameGame
/// or an implementation of [Component].
/// Proxies [Component] lifecycle hooks to nested [Particle].
@Deprecated('Will be removed after v1.1, use ParticleSystemComponent instead')
class ParticleComponent extends Component {
Particle particle;

View File

@ -0,0 +1,53 @@
import 'dart:ui';
import '../../components.dart';
import '../../particles.dart';
/// {@template particle_system_component}
/// A [PositionComponent] that renders a [Particle] at the designated
/// position, scaled to have the designated size and rotated to the specified
/// angle.
/// {endtempalte}
class ParticleSystemComponent extends PositionComponent {
Particle? particle;
/// {@macro particle_system_component}
ParticleSystemComponent({
this.particle,
Vector2? position,
Vector2? size,
Vector2? scale,
double? angle,
Anchor? anchor,
int? priority,
}) : super(
position: position,
size: size,
scale: scale,
angle: angle,
anchor: anchor,
priority: priority,
);
/// Returns progress of the child [Particle].
///
/// Could be used by external code if needed.
double get progress => particle?.progress ?? 0;
/// Passes rendering chain down to the inset
/// [Particle] within this [Component].
@override
void render(Canvas canvas) {
super.render(canvas);
particle?.render(canvas);
}
/// Passes update chain to child [Particle].
@override
void update(double dt) {
particle?.update(dt);
if (particle?.shouldRemove ?? false) {
removeFromParent();
}
}
}

View File

@ -0,0 +1,53 @@
import 'package:canvas_test/canvas_test.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/particles.dart';
import 'package:flame_test/flame_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockParticle extends Mock implements Particle {}
void main() {
group('ParticleSystem', () {
test('returns the progress of its particle', () {
final particle = MockParticle();
when(() => particle.progress).thenReturn(0.2);
final progress = ParticleSystemComponent(particle: particle).progress;
expect(progress, equals(0.2));
});
test('returns the progress of its particle', () {
final particle = MockParticle();
final canvas = MockCanvas();
ParticleSystemComponent(particle: particle).render(canvas);
verify(() => particle.render(canvas)).called(1);
});
test('updates its particle', () {
final particle = MockParticle();
when(() => particle.shouldRemove).thenReturn(false);
ParticleSystemComponent(particle: particle).update(0.1);
verify(() => particle.update(0.1)).called(1);
});
testWithFlameGame(
'is removed when its particle is finished',
(FlameGame game) async {
final particle = MockParticle();
when(() => particle.shouldRemove).thenReturn(true);
final component = ParticleSystemComponent(particle: particle);
await game.ensureAdd(component);
game.update(1);
expect(component.shouldRemove, isTrue);
},
);
});
}