mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 16:36:57 +08:00
Update cicd.yml file on flame_3d to match main. Since we finally updated main, we should require no difference whatsoever on this file anymore. This will require downgrading all the color changes, which is fine. This should never have been a part of flame_3d to begin with. Files were "untouched" by checking out the exact version as they are in main right now (will need to rebase flame_3d to main later). I had to add a couple more files because the files on main had dependencies on changes that are not yet rebased on flame_3d. These extra diffs should disappear when I do the final rebase.
563 lines
16 KiB
Dart
563 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.withOpacity(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()];
|
|
}
|
|
}
|