mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 08:27:36 +08:00
feat: adding ParticleSystemComponent (#1489)
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user