Flutter Flame教程10 -- Particles粒子效果

4,633 阅读6分钟

Particle -- 粒子


Flame提供了基本的,同时也很强大的,可扩展的粒子系统。这个系统的核心概念是Particle类,在行为上和ParticleComponent很相似。

ParticleBaseGame的最基本使用如下:

import 'package:flame/components/particle_component.dart';

game.add(
    ParticleComponent(
        particle: CircleParticle()
    );
);

Particle和自定义的Game实现一起使用时,请确保在每次游戏循环帧时,Particleupdaterender生命周期钩子被调用。

实现想要的粒子效果的主要方式:

  • 现有行为的组合
  • 使用行为链(只是一个语法糖)
  • 使用ComputedParticle

通过从上到下的效果,合成的工作方式与Flutter小部件的工作方式类似。通过定义自底向上的行为,链式允许更流畅的表达相同的组合树。计算粒子又将行为的实现完全委派给您的代码。在需要的地方,任何方式都可以和现存的行为组合使用。

下面你可以看到显示使用上面定义的三种方式从(0,0)加速到随机方向的一个圆圈的效果示例。

Random rnd = Random();
Function randomOffset = () => Offset(
    rnd.nextDouble() * 200 - 100,
    rnd.nextDouble() * 200 - 100,
);

game.add(
    ParticleComponent(
        particle: Particle.generate(
            count: 10,
            generator: (i) => AcceleratedParticle(
                acceleration: randomOffset(),
                child: CircleParticle(
                    paint: Paint()..color = Colors.red
                )
            )
        )
    )
);

game.add(
    Particle
        .generate(
            count: 10,
            generator: (i) => CircleParticle(paint: Paint()..color = Colors.red)
                .accelerating(randomOffset())
        )
        .asComponent()
);

game.add(
    Particle
        .gnerate(
            count: 10,
            generator: (i) {
                var position = Offset.zero;
                var speed = Offset.zero;
                final acceleration = randomOffset();
                final paint = Paint()..color = Colors.red;
                
                return ComputedParticle(
                    renderer: (canvas, _) {
                        speed += acceleration;
                        position += speed;
                        canvas.drawCircle(position, 10, paint);
                    }
                );
            }
        )
)

你可以在这里找到更多以各种组合使用不同的内置粒子的示例。

Lifecycle 生命周期


所有Particle的普遍行为是它们都接受lifespan参数。一旦其内部粒子达到其使用寿命时,该值用于使ParticleComponent自我销毁。Particle内部的时间使用Flame的Timer。可以将其配置为double,通过将其传递到相应的Particle构造器来表示秒(精确到微妙)。

Particle(lifespan: .2);
Particle(lifespan: 4);

通过使用setLifespan方法,也可以重置Particle寿命,接收double类型的秒。

final particle = Particle(lifespan: 2);
particle.setLifespan(2);

在存活时间内,Particle追踪它存活的时间,并暴露progess进度,这是一个浮点型单位,值从0跨度到1.它的值与Flutter的AnimationController的value值类似。

final duration = const Duration(seconds: 2);
final particle = Particle(lifespan: duration.inMicroseconds / Duration.microsecondsPerSecond);

game.add(ParticleComponent(particle: particle));

Timer.periodic(duration * .1, () => print(particle.progress));

如果支持任何一个内建行为,生命周期会被传递给定Particle的所有后代。

内置粒子


Flame附带了一些内置的粒子行为:

  • TranslatedParticle,通过给定的Offset平移child
  • MovingParticle,在两个预定义的Offset移动它的child,支持Curve
  • AcceleratedParticle,允许基本的基于物理的效果,比如重力或者速度衰减
  • CircleParticle,渲染所有形状和大小的圆形
  • SpriteParticle,在粒子效果中渲染Flame精灵
  • ImageParticle,在粒子效果中渲染dart:ui的Image
  • ComponentParticle,在粒子效果中渲染Flame的Component
  • FlareParticle,在粒子效果中渲染Flare动画

一起使用这些行为的更多粒子可以在这里找到。所有的可用实现都可以在Flame源码的particles文件夹中找到。

Translated Particle


只需平移粒子到渲染画布的指定便宜即可。不改变或修改位置。如果需要改变位置,考虑使用MovingParticle或者AcceleratedParticle。通过平移画布也可以达到相同的效果。

game.add(
    ParticleComponent(
        particle: TranslatedParticle(
            offset: game.size.center(Offset.zero),
            child: Particle(),
        )
    )
);

Moving Particle 移动粒子


在粒子的生命周期中,从from移动到to。通过CurvedParticle支持Curve

game.add(
    ParticleComponent(
        particle: MovingParticle(
            from: game.size.topLeft(Offset.zero),
            to: game.size.bottomRight(Offset.zero),
            child: Particle(),
        )
    )
);

Accelerated Particle 加速粒子


一个允许你指定它的初始位置,速度和加速度的基本物理粒子,update周期完成剩余工作。三个指定的都是Offset,你可以认为是向量。对于基于物理的爆发尤其有效,但不局限于此。Offset的单位是每秒逻辑像素。所以Offset(0, 100)的速度会在游戏时间的每秒移动子粒子100个设备像素。

final rnd = Random();
game.add(
    ParticleComponent(
        particle AcceleratedParticle(
            position: game.size.center(Offset.zero),
            speed: Offset(rnd.nextDouble() *200 - 100, -rnd.nextDouble() * 100),
            acceleration: Offset(0, 100),
            child:Particle(),
        )
    )
);

Circle Particle 原型粒子


传递给画布的0值偏移的画笔上渲染圆形的粒子。为了达到想要的位置,组合使用TranslatedParticle,MovingParticle或者AcceleratedParticle

game.add(
    ParticleComponent(
        particle: CircleParticle(
            radius: game.size.width / 2,
            paint: Paint()..color = Colors.red.withOpacity(.5),
        )
    )
);

Sprite Particle 精灵粒子


允许你在粒子效果中插入Flame的精灵。使用SpriteSheet效果的图形时很有用

game.add(
    ParticleComponent(
        particle: SpriteParticle(
            sprite: Sprite('sprite.png'),
            size: Position(64, 64),
        )
    )
);

Image Particle 图片粒子


在粒子树种渲染给定的dart:ui图片。

await Flame.images.loadAll(const [
    'image.png'
]);

game.add(
    ParticleComponent(
        particle: ImageParticle(
            size: const Size.square(24),
            image: Flame.images.loadedFiles['image.png'],
        )
    )
);

Animation Particle 动画粒子


嵌入Flame动画的粒子。默认情况下,对齐“动画” stepTime,以便在“粒子”生命周期中完全播放。可以使用alignAnimationTime参数覆盖此行为.

final spritesheet = SpriteSheet(
    imageName: 'spritesheet.png',
    textureWidth: 16,
    textureHeight: 16,
    columns: 10,
    rows: 2
);

game.add(
  ParticleComponent(
    particle: AnimationParticle(
        animation: spritesheet.createAnimation(0, stepTime: 0.1),
    )
  )  
);

Component Particle 组件粒子


Particle允许你将Flame Component嵌入到粒子效果中。Component可以有自己的update生命周期,并且在不同的效果树种被重用。如果您唯一需要的是向某些Component的实例添加一些动态效果,请考虑将其直接添加到Game中,而中间不包含Particle

var longLivingRect = RectComponent();

game.add(
    ParticleComponent(
        particle: ComponentParticle(
            component: longLivingRect
        )
    )
);

class RectComponent extends Component {
    void render(Canvas c) {
        c.drawRect(
            Rect.fromCenter(center: Offset.zero, width: 100, height: 100),
            Paint()..color = Colors.red
        );
    }
    
    void update(double dt) {
        
    }
}

Flare Particle


FlareAnimation的容器,向它的子代传递updaterender钩子。

const flareSize = 32.0;
final flareAnimation = await FlareAnimation.load('assets/sparkle.flr');
flareAnimation.updateAnimation('Shine');
flareAnimation.width = flareSize;
flareAnimation.height = flareSize;

game.add(
    ParticleComponent(
        particle: FlareParticle(flare: flareAnimation),
    )
);

Computed Particle 计算的粒子


Particle可以在如下情况帮助你:

  • 默认的行为不够
  • 复杂效果优化
  • 简化自定义

当创建的时候,提供的ParticleRenderDelegate代理所有的渲染,在每一帧被调用,并且执行必要的计算,渲染到画布上。 创建后,将所有渲染委托给提供的ParticleRenderDeletegate,在每一帧上调用该类,以执行必要的计算并将某些内容渲染到Canvas上。

game.add(
    ParticleComponent(
        particle: ComputedParticle(
            renderer: (canvas, particle) => canvas.drawCircle(
                Offset.zero,
                particle.progress * 10,
                Paint()
                    ..color = Color.lerp(
                        Colors.red,
                        Colors.blue,
                        particle.progress,
                    )
            )
        )
    )
)

Nesting behavior 内嵌行为


Flame的粒子实现遵循与Flutter 小组件相同的极端合成模式。封装每个粒子的小部分行为,然后嵌入到一起来达到想要的视觉效果来实现的。

允许粒子内嵌彼此的两个实体是:SingleChildParticle mixin 和ComposedParticle类。

SingleChildParticle可以帮助你创建自定义行为的粒子。 比如,在每帧中随机放置子组件的位置:

var rnd = Random();
class GlitchParticle extends Particle with SingleChildParticle {
    @override
    Particle child;
    
    GlitchParticle({
        @required this.child,
        double lifespan,
    }) : super(lifespan: lifespan);
    
    @override
    render(Canvas canvas) {
        canvas.save();
        canvas.translate(rnd.nextDouble(0 * 100, rnd.nextDouble() *100);
        
        super.render();
        
        canvas.restore();
    }
}

ComposedParticle 可以单独使用,也可以使用在已存的粒子树种。