mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-28 23:46:52 +08:00
Update min Dart constraint to 3.8, which will enable us to use the fancier collection literals. This requires bumping the min Flutter version as well: <img width="1892" height="1122" alt="image" src="https://github.com/user-attachments/assets/7c7b07fc-4d96-4987-824d-9a7133ecfb85" />
566 lines
16 KiB
Dart
566 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:flame/components.dart' hide Timer;
|
|
import 'package:flame/game.dart';
|
|
import 'package:flame/particles.dart';
|
|
import 'package:flame/sprite.dart';
|
|
import 'package:flame/timer.dart' as flame_timer;
|
|
import 'package:flutter/material.dart' hide Image;
|
|
|
|
class ParticlesExample extends FlameGame {
|
|
static const String description = '''
|
|
In this example we show how to render a lot of different particles.
|
|
''';
|
|
|
|
/// Defines dimensions of the sample
|
|
/// grid to be displayed on the screen,
|
|
/// 5x5 in this particular case
|
|
static const gridSize = 5.0;
|
|
static const steps = 5;
|
|
|
|
/// Miscellaneous values used
|
|
/// by examples below
|
|
final Random rnd = Random();
|
|
Timer? spawnTimer;
|
|
final StepTween steppedTween = StepTween(begin: 0, end: 5);
|
|
final trafficLight = TrafficLightComponent();
|
|
|
|
/// Defines the lifespan of all the particles in these examples
|
|
final sceneDuration = const Duration(seconds: 1);
|
|
|
|
Vector2 get cellSize => size / gridSize;
|
|
Vector2 get halfCellSize => cellSize / 2;
|
|
|
|
@override
|
|
Future<void> onLoad() async {
|
|
await images.load('zap.png');
|
|
await images.load('boom.png');
|
|
}
|
|
|
|
@override
|
|
void onMount() {
|
|
spawnParticles();
|
|
// Spawn new particles every second
|
|
spawnTimer = Timer.periodic(sceneDuration, (_) {
|
|
spawnParticles();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void onRemove() {
|
|
super.onRemove();
|
|
spawnTimer?.cancel();
|
|
}
|
|
|
|
/// Showcases various different uses of [Particle]
|
|
/// and its derivatives
|
|
void spawnParticles() {
|
|
// Contains sample particles, in order by complexity
|
|
// and amount of used features. Jump to source for more explanation on each
|
|
final particles = <Particle>[
|
|
circle(),
|
|
smallWhiteCircle(),
|
|
movingParticle(),
|
|
randomMovingParticle(),
|
|
alignedMovingParticles(),
|
|
easedMovingParticle(),
|
|
intervalMovingParticle(),
|
|
computedParticle(),
|
|
chainingBehaviors(),
|
|
steppedComputedParticle(),
|
|
reuseParticles(),
|
|
imageParticle(),
|
|
reuseImageParticle(),
|
|
rotatingImage(),
|
|
acceleratedParticles(),
|
|
paintParticle(),
|
|
spriteParticle(),
|
|
animationParticle(),
|
|
fireworkParticle(),
|
|
componentParticle(),
|
|
];
|
|
|
|
// Place all the [Particle] instances
|
|
// defined above in a grid on the screen
|
|
// as per defined grid parameters
|
|
do {
|
|
final particle = particles.removeLast();
|
|
final col = particles.length % gridSize;
|
|
final row = (particles.length ~/ gridSize).toDouble();
|
|
final cellCenter = (cellSize..multiply(Vector2(col, row))) + halfCellSize;
|
|
|
|
add(
|
|
// Bind all the particles to a [Component] update
|
|
// lifecycle from the [FlameGame].
|
|
ParticleSystemComponent(
|
|
particle: TranslatedParticle(
|
|
lifespan: 1,
|
|
offset: cellCenter,
|
|
child: particle,
|
|
),
|
|
),
|
|
);
|
|
} while (particles.isNotEmpty);
|
|
}
|
|
|
|
/// Simple static circle, doesn't move or
|
|
/// change any of its attributes
|
|
Particle circle() {
|
|
return CircleParticle(
|
|
paint: Paint()..color = Colors.white10,
|
|
);
|
|
}
|
|
|
|
/// This one will is a bit smaller,
|
|
/// and a bit less transparent
|
|
Particle smallWhiteCircle() {
|
|
return CircleParticle(
|
|
radius: 5.0,
|
|
paint: Paint()..color = Colors.white,
|
|
);
|
|
}
|
|
|
|
/// Particle which is moving from
|
|
/// one predefined position to another one
|
|
Particle movingParticle() {
|
|
return MovingParticle(
|
|
/// This parameter is optional, will default to [Vector2.zero]
|
|
from: Vector2(-20, -20),
|
|
to: Vector2(20, 20),
|
|
child: CircleParticle(paint: Paint()..color = Colors.amber),
|
|
);
|
|
}
|
|
|
|
/// [Particle] which is moving to a random direction
|
|
/// within each cell each time created
|
|
Particle randomMovingParticle() {
|
|
return MovingParticle(
|
|
to: randomCellVector2(),
|
|
child: CircleParticle(
|
|
radius: 5 + rnd.nextDouble() * 5,
|
|
paint: Paint()..color = Colors.red,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Generates 5 particles, each moving
|
|
/// symmetrically within grid cell
|
|
Particle alignedMovingParticles() {
|
|
return Particle.generate(
|
|
count: 5,
|
|
generator: (i) {
|
|
final currentColumn = (cellSize.x / 5) * i - halfCellSize.x;
|
|
return MovingParticle(
|
|
from: Vector2(currentColumn, -halfCellSize.y),
|
|
to: Vector2(currentColumn, halfCellSize.y),
|
|
child: CircleParticle(
|
|
radius: 2.0,
|
|
paint: Paint()..color = Colors.blue,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Burst of 5 particles each moving
|
|
/// to a random direction within the cell
|
|
Particle randomMovingParticles() {
|
|
return Particle.generate(
|
|
count: 5,
|
|
generator: (i) => MovingParticle(
|
|
to: randomCellVector2()..scale(0.5),
|
|
child: CircleParticle(
|
|
radius: 5 + rnd.nextDouble() * 5,
|
|
paint: Paint()..color = Colors.deepOrange,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Same example as above, but with easing, utilizing [CurvedParticle]
|
|
/// extension.
|
|
Particle easedMovingParticle() {
|
|
return Particle.generate(
|
|
count: 5,
|
|
generator: (i) => MovingParticle(
|
|
curve: Curves.easeOutQuad,
|
|
to: randomCellVector2()..scale(0.5),
|
|
child: CircleParticle(
|
|
radius: 5 + rnd.nextDouble() * 5,
|
|
paint: Paint()..color = Colors.deepPurple,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Same example as above, but using awesome [Interval]
|
|
/// curve, which "schedules" transition to happen between
|
|
/// certain values of progress. In this example, circles will
|
|
/// move from their initial to their final position
|
|
/// when progress is changing from 0.2 to 0.6 respectively.
|
|
Particle intervalMovingParticle() {
|
|
return Particle.generate(
|
|
count: 5,
|
|
generator: (i) => MovingParticle(
|
|
curve: const Interval(0.2, 0.6, curve: Curves.easeInOutCubic),
|
|
to: randomCellVector2()..scale(0.5),
|
|
child: CircleParticle(
|
|
radius: 5 + rnd.nextDouble() * 5,
|
|
paint: Paint()..color = Colors.greenAccent,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// A [ComputedParticle] completely delegates all the rendering
|
|
/// to an external function, hence It's very flexible, as you can implement
|
|
/// any currently missing behavior with it.
|
|
/// Also, it allows to optimize complex behaviors by avoiding nesting too
|
|
/// many [Particle] together and having all the computations in place.
|
|
Particle computedParticle() {
|
|
return ComputedParticle(
|
|
renderer: (canvas, particle) => canvas.drawCircle(
|
|
Offset.zero,
|
|
particle.progress * halfCellSize.x,
|
|
Paint()
|
|
..color = Color.lerp(
|
|
Colors.red,
|
|
Colors.blue,
|
|
particle.progress,
|
|
)!,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Using [ComputedParticle] to use custom tweening
|
|
/// In reality, you would like to keep as much of renderer state
|
|
/// defined outside and reused between each call
|
|
Particle steppedComputedParticle() {
|
|
return ComputedParticle(
|
|
lifespan: 2,
|
|
renderer: (canvas, particle) {
|
|
const steps = 5;
|
|
final steppedProgress =
|
|
steppedTween.transform(particle.progress) / steps;
|
|
|
|
canvas.drawCircle(
|
|
Offset.zero,
|
|
(1 - steppedProgress) * halfCellSize.x,
|
|
Paint()
|
|
..color = Color.lerp(
|
|
Colors.red,
|
|
Colors.blue,
|
|
steppedProgress,
|
|
)!,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Particle which is used in example below
|
|
Particle? reusableParticle;
|
|
|
|
/// A burst of white circles which actually using a single circle
|
|
/// as a form of optimization. Look for reusing parts of particle effects
|
|
/// whenever possible, as there are limits which are relatively easy to reach.
|
|
Particle reuseParticles() {
|
|
reusableParticle ??= circle();
|
|
|
|
return Particle.generate(
|
|
generator: (i) => MovingParticle(
|
|
curve: Interval(rnd.nextDouble() * 0.1, rnd.nextDouble() * 0.8 + 0.1),
|
|
to: randomCellVector2()..scale(0.5),
|
|
child: reusableParticle!,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Simple static image particle which doesn't do much.
|
|
/// Images are great examples of where assets should
|
|
/// be reused across particles. See example below for more details.
|
|
Particle imageParticle() {
|
|
return ImageParticle(
|
|
size: Vector2.all(24),
|
|
image: images.fromCache('zap.png'),
|
|
);
|
|
}
|
|
|
|
/// Particle which is used in example below
|
|
Particle? reusableImageParticle;
|
|
|
|
/// A single [imageParticle] is drawn 9 times
|
|
/// in a grid within grid cell. Looks as 9 particles
|
|
/// to user, saves us 8 particle objects.
|
|
Particle reuseImageParticle() {
|
|
const count = 9;
|
|
const perLine = 3;
|
|
const imageSize = 24.0;
|
|
final colWidth = cellSize.x / perLine;
|
|
final rowHeight = cellSize.y / perLine;
|
|
|
|
reusableImageParticle ??= imageParticle();
|
|
|
|
return Particle.generate(
|
|
count: count,
|
|
generator: (i) => TranslatedParticle(
|
|
offset: Vector2(
|
|
(i % perLine) * colWidth - halfCellSize.x + imageSize,
|
|
(i ~/ perLine) * rowHeight - halfCellSize.y + imageSize,
|
|
),
|
|
child: reusableImageParticle!,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// [RotatingParticle] is a simple container which rotates
|
|
/// a child particle passed to it.
|
|
/// As you can see, we're reusing [imageParticle] from example above.
|
|
/// Such a composability is one of the main implementation features.
|
|
Particle rotatingImage({double initialAngle = 0}) {
|
|
return RotatingParticle(from: initialAngle, child: imageParticle());
|
|
}
|
|
|
|
/// [AcceleratedParticle] is a very basic acceleration physics container,
|
|
/// which could help implementing such behaviors as gravity, or adding
|
|
/// some non-linearity to something like [MovingParticle]
|
|
Particle acceleratedParticles() {
|
|
return Particle.generate(
|
|
generator: (i) => AcceleratedParticle(
|
|
speed:
|
|
Vector2(
|
|
rnd.nextDouble() * 600 - 300,
|
|
-rnd.nextDouble() * 600,
|
|
) *
|
|
0.2,
|
|
acceleration: Vector2(0, 200),
|
|
child: rotatingImage(initialAngle: rnd.nextDouble() * pi),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// [PaintParticle] allows to perform basic composite operations
|
|
/// by specifying custom [Paint].
|
|
/// Be aware that it's very easy to get *really* bad performance
|
|
/// misusing composites.
|
|
Particle paintParticle() {
|
|
final colors = [
|
|
const Color(0xffff0000),
|
|
const Color(0xff00ff00),
|
|
const Color(0xff0000ff),
|
|
];
|
|
final positions = [
|
|
Vector2(-10, 10),
|
|
Vector2(10, 10),
|
|
Vector2(0, -14),
|
|
];
|
|
|
|
return Particle.generate(
|
|
count: 3,
|
|
generator: (i) => PaintParticle(
|
|
paint: Paint()..blendMode = BlendMode.difference,
|
|
child: MovingParticle(
|
|
curve: SineCurve(),
|
|
from: positions[i],
|
|
to: i == 0 ? positions.last : positions[i - 1],
|
|
child: CircleParticle(
|
|
radius: 20.0,
|
|
paint: Paint()..color = colors[i],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// [SpriteParticle] allows easily embed
|
|
/// Flame's [Sprite] into the effect.
|
|
Particle spriteParticle() {
|
|
return SpriteParticle(
|
|
sprite: Sprite(images.fromCache('zap.png')),
|
|
size: cellSize * 0.5,
|
|
);
|
|
}
|
|
|
|
/// An [SpriteAnimationParticle] takes a Flame [SpriteAnimation]
|
|
/// and plays it during the particle lifespan.
|
|
Particle animationParticle() {
|
|
return SpriteAnimationParticle(
|
|
animation: getBoomAnimation(),
|
|
size: Vector2(128, 128),
|
|
);
|
|
}
|
|
|
|
/// [ComponentParticle] proxies particle lifecycle hooks
|
|
/// to its child [Component]. In example below, [Component] is
|
|
/// reused between particle effects and has internal behavior
|
|
/// which is independent from the parent [Particle].
|
|
Particle componentParticle() {
|
|
return MovingParticle(
|
|
from: -halfCellSize * 0.2,
|
|
to: halfCellSize * 0.2,
|
|
curve: SineCurve(),
|
|
child: ComponentParticle(component: trafficLight),
|
|
);
|
|
}
|
|
|
|
/// Not very realistic firework, yet it highlights
|
|
/// use of [ComputedParticle] within other particles,
|
|
/// mixing predefined and fully custom behavior.
|
|
Particle fireworkParticle() {
|
|
// A palette to paint over the "sky"
|
|
final paints = [
|
|
Colors.amber,
|
|
Colors.amberAccent,
|
|
Colors.red,
|
|
Colors.redAccent,
|
|
Colors.yellow,
|
|
Colors.yellowAccent,
|
|
// Adds a nice "lense" tint
|
|
// to overall effect
|
|
Colors.blue,
|
|
].map((color) => Paint()..color = color).toList();
|
|
|
|
return Particle.generate(
|
|
generator: (i) {
|
|
final initialSpeed = randomCellVector2();
|
|
final deceleration = initialSpeed * -1;
|
|
final gravity = Vector2(0, 40);
|
|
|
|
return AcceleratedParticle(
|
|
speed: initialSpeed,
|
|
acceleration: deceleration + gravity,
|
|
child: ComputedParticle(
|
|
renderer: (canvas, particle) {
|
|
final paint = randomElement(paints);
|
|
// Override the color to dynamically update opacity
|
|
paint.color = paint.color.withValues(
|
|
alpha: 1 - particle.progress,
|
|
);
|
|
|
|
canvas.drawCircle(
|
|
Offset.zero,
|
|
// Closer to the end of lifespan particles
|
|
// will turn into larger glaring circles
|
|
rnd.nextDouble() * particle.progress > 0.6
|
|
? rnd.nextDouble() * (50 * particle.progress)
|
|
: 2 + (3 * particle.progress),
|
|
paint,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// [Particle] base class exposes a number
|
|
/// of convenience wrappers to make positioning.
|
|
///
|
|
/// Just remember that the less chaining and nesting - the
|
|
/// better for performance!
|
|
Particle chainingBehaviors() {
|
|
final paint = Paint()..color = randomMaterialColor();
|
|
final rect = ComputedParticle(
|
|
renderer: (canvas, _) => canvas.drawRect(
|
|
Rect.fromCenter(center: Offset.zero, width: 10, height: 10),
|
|
paint,
|
|
),
|
|
);
|
|
|
|
return ComposedParticle(
|
|
children: [
|
|
rect
|
|
.rotating(to: pi / 2)
|
|
.moving(to: -cellSize)
|
|
.scaled(2)
|
|
.accelerated(acceleration: halfCellSize * 5)
|
|
.translated(halfCellSize),
|
|
rect
|
|
.rotating(to: -pi)
|
|
.moving(to: Vector2(1, -1)..multiply(cellSize))
|
|
.scaled(2)
|
|
.translated(Vector2(1, -1)..multiply(halfCellSize))
|
|
.accelerated(acceleration: Vector2(-5, 5)..multiply(halfCellSize)),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Returns random [Vector2] within a virtual grid cell
|
|
Vector2 randomCellVector2() {
|
|
return (Vector2.random() - Vector2.random())..multiply(cellSize);
|
|
}
|
|
|
|
/// Returns random [Color] from primary swatches
|
|
/// of material palette
|
|
Color randomMaterialColor() {
|
|
return Colors.primaries[rnd.nextInt(Colors.primaries.length)];
|
|
}
|
|
|
|
/// Returns a random element from a given list
|
|
T randomElement<T>(List<T> list) {
|
|
return list[rnd.nextInt(list.length)];
|
|
}
|
|
|
|
/// Sample "explosion" animation for [SpriteAnimationParticle] example
|
|
SpriteAnimation getBoomAnimation() {
|
|
const columns = 8;
|
|
const rows = 8;
|
|
const frames = columns * rows;
|
|
final spriteImage = images.fromCache('boom.png');
|
|
final spriteSheet = SpriteSheet.fromColumnsAndRows(
|
|
image: spriteImage,
|
|
columns: columns,
|
|
rows: rows,
|
|
);
|
|
final sprites = List<Sprite>.generate(frames, spriteSheet.getSpriteById);
|
|
return SpriteAnimation.spriteList(sprites, stepTime: 0.1);
|
|
}
|
|
}
|
|
|
|
Future<FlameGame> loadGame() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
return ParticlesExample();
|
|
}
|
|
|
|
/// A curve which maps sinus output (-1..1,0..pi)
|
|
/// to an oscillating (0..1..0,0..1), essentially "ease-in-out and back"
|
|
class SineCurve extends Curve {
|
|
@override
|
|
double transformInternal(double t) {
|
|
return (sin(pi * (t * 2 - 1 / 2)) + 1) / 2;
|
|
}
|
|
}
|
|
|
|
/// Sample for [ComponentParticle], changes its colors
|
|
/// each 2s of registered lifetime.
|
|
class TrafficLightComponent extends Component {
|
|
final Rect rect = Rect.fromCenter(center: Offset.zero, height: 32, width: 32);
|
|
final flame_timer.Timer colorChangeTimer = flame_timer.Timer(2, repeat: true);
|
|
final colors = <Color>[
|
|
Colors.green,
|
|
Colors.orange,
|
|
Colors.red,
|
|
];
|
|
final Paint _paint = Paint();
|
|
|
|
@override
|
|
void onMount() {
|
|
colorChangeTimer.start();
|
|
}
|
|
|
|
@override
|
|
void render(Canvas canvas) {
|
|
canvas.drawRect(rect, _paint..color = currentColor);
|
|
}
|
|
|
|
@override
|
|
void update(double dt) {
|
|
colorChangeTimer.update(dt);
|
|
}
|
|
|
|
Color get currentColor {
|
|
return colors[(colorChangeTimer.progress * colors.length).toInt()];
|
|
}
|
|
}
|