Skip to content

粒子系统实现

预计阅读 25 分钟
版本信息
最后更新:2026-07-09 · Cocos Creator 3.8.8 LTS · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0)

粒子系统(Particle System)是游戏中最常用的视觉效果工具之一——火焰、烟雾、爆炸、飘雪、下雨、拖尾特效都离不开它。在 Web 前端中你可能用过 CSS 动画或 Canvas 手动绘制这些效果,但在游戏引擎中有一套更高效、更强大的粒子系统。本章从底层原理出发,介绍 Cocos Creator 的内置粒子系统以及原生 Canvas 2D 的粒子实现。


粒子系统基本原理

一个粒子系统由**发射器(Emitter)和若干粒子(Particle)**组成。每个粒子在生命周期内持续更新自身属性:

粒子生命周期:
  出生 → 初始化(位置/速度/颜色/大小/生命值)
  每帧更新 → 位置 += 速度 × dt → 生命值减少 → 颜色/大小渐变
  死亡 → 生命值归零 → 回收到对象池

核心属性

属性说明示例
发射速率每秒发射粒子数火焰 ~50/s, 烟雾 ~10/s
生命周期粒子存活时间(秒)火花 0.5-1s, 烟雾 2-5s
初始速度粒子出生时的运动速度爆炸 200-500 px/s
重力/加速度粒子所受外力影响火焰上升(负Y重力)、雪花下落
初始大小/颜色出生时的视觉属性火花从亮白渐变到红→透明
大小/颜色渐变生命期内属性变化曲线烟雾从小变大、从浓到淡
发射形状粒子出生的空间区域点/圆形/矩形/锥形
纹理粒子的渲染贴图圆形光斑/星形/自定义
前端类比
  • 粒子系统 ≈ 100 个带 @keyframes 动画的 <div>,每个从随机位置向随机方向移动,淡出后回收
  • 发射器 ≈ 一个定时器 setInterval 不断创建新的 <div> 元素
  • 粒子属性渐变 ≈ CSS transitionanimation 的关键帧
  • 对象池 ≈ 虚拟滚动列表中的 DOM 节点复用

关键区别:粒子系统在 GPU 上并行处理成千上万个粒子,而非逐个操作 DOM 节点。


Cocos Creator 粒子系统

ParticleSystem2D 组件

Cocos Creator 3.8.x 提供 ParticleSystem2D 组件(基于 CCParticleSystem),通过编辑器可视化配置粒子效果,或通过代码动态控制。

基础用法

typescript
import { _decorator, Component, ParticleSystem2D, Node, Vec2, tween } from 'cc'
const { ccclass, property } = _decorator

@ccclass('ExplosionEffect')
export class ExplosionEffect extends Component {
  @property(ParticleSystem2D)
  particle: ParticleSystem2D | null = null

  /** 在指定位置播放一次爆炸效果 */
  playExplosionAt(worldPos: Vec2) {
    if (!this.particle) return

    // 移动节点到目标位置
    this.node.setWorldPosition(worldPos.x, worldPos.y, 0)

    // 重置粒子系统并播放
    this.particle.stopSystem() // 先停止当前播放
    this.particle.resetSystem() // 重置所有粒子状态
    this.particle.play() // 播放

    // 爆炸效果播完后自动隐藏节点(回收到对象池)
    this.scheduleOnce(() => {
      this.node.active = false
    }, this.particle.duration + 0.1)
  }
}

常用配置参数

typescript
// 通过代码动态配置粒子效果
function configureFireEffect(particle: ParticleSystem2D) {
  // 发射器模式:GRAVITY(受重力影响)或 RADIUS(径向扩散)
  particle.emitterMode = ParticleSystem2D.EmitterMode.GRAVITY

  // 发射速率
  particle.totalParticles = 200
  particle.emissionRate = 50 // 每秒发射 50 个
  particle.life = 0.8 // 每个粒子存活 0.8 秒
  particle.lifeVar = 0.3 // 生命值随机浮动 ±0.3

  // 速度
  particle.speed = 200 // 初始速度
  particle.speedVar = 50 // 速度随机浮动
  particle.sourceAngle = 90 // 发射角度(90 = 向上)
  particle.angleVar = 20 // 角度随机浮动

  // 外观
  particle.startSize = 32 // 出生大小
  particle.endSize = 8 // 死亡时缩小到 8
  particle.startColor = { r: 255, g: 200, b: 50, a: 255 } // 亮黄
  particle.endColor = { r: 255, g: 50, b: 0, a: 0 } // 红→透明

  // 发射形状(相对于节点中心)
  particle.posVar = { x: 10, y: 5 } // 出生位置随机偏移
  particle.positionType = ParticleSystem2D.PositionType.FREE // 粒子脱离发射器独立运动
}

// 烟雾效果 — 慢速、大颗粒、长时间淡出
function configureSmokeEffect(particle: ParticleSystem2D) {
  particle.emitterMode = ParticleSystem2D.EmitterMode.GRAVITY
  particle.emissionRate = 10
  particle.life = 3.0
  particle.lifeVar = 1.0
  particle.speed = 30
  particle.speedVar = 10
  particle.sourceAngle = 90
  particle.angleVar = 30
  particle.startSize = 20
  particle.endSize = 80
  particle.startColor = { r: 150, g: 150, b: 150, a: 128 }
  particle.endColor = { r: 180, g: 180, b: 180, a: 0 }
  particle.gravity = { x: 0, y: 10 } // 烟雾轻微上升
}

粒子性能优化

策略说明效果
限制总粒子数totalParticles 设置合理上限(≤ 500)避免 GPU 填充率瓶颈
使用小纹理粒子贴图 ≤ 64×64 px减少纹理采样开销
降低发射速率远处效果可使用更低速率减少活跃粒子数
视口裁剪屏幕外粒子系统暂停大幅减少 GPU 开销
对象池复用爆炸等一次性效果使用对象池避免频繁创建/销毁
Atlas 图集打包多个粒子纹理放入同一图集减少 Draw Call

原生 Canvas 2D 粒子实现

对于不使用引擎的原生 Canvas 项目,手动实现一个轻量粒子系统可以帮助你深入理解其原理。

typescript
// particle/Particle.ts — 单个粒子
interface ParticleConfig {
  x: number
  y: number
  vx: number
  vy: number
  life: number
  maxLife: number
  size: number
  endSize: number
  color: string
  endColor: string
  alpha: number
  endAlpha: number
}

class Particle {
  x: number
  y: number
  vx: number
  vy: number
  life: number
  maxLife: number
  size: number
  endSize: number
  startSize: number // 出生大小,用于生命周期插值
  color: string
  endColor: string
  alpha: number
  endAlpha: number
  startAlpha: number // 出生透明度,用于生命周期插值
  active: boolean = true

  constructor(config: ParticleConfig) {
    Object.assign(this, config)
    // 记录初始值,避免基于当前已被修改的属性再次插值导致指数漂移
    this.startSize = this.size
    this.startAlpha = this.alpha
  }

  /** 每帧更新,返回 false 表示粒子已死亡 */
  update(dt: number): boolean {
    this.life -= dt
    if (this.life <= 0) {
      this.active = false
      return false
    }

    // 位置更新
    this.x += this.vx * dt
    this.y += this.vy * dt

    // 属性线性插值(生命进度 0→1)
    const progress = 1 - this.life / this.maxLife
    this.size = this.startSize + (this.endSize - this.startSize) * progress
    this.alpha = this.startAlpha + (this.endAlpha - this.startAlpha) * progress

    return true
  }

  /** 渲染到 Canvas */
  render(ctx: CanvasRenderingContext2D): void {
    ctx.save()
    ctx.globalAlpha = Math.max(0, this.alpha)
    ctx.fillStyle = this.color
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.size / 2, 0, Math.PI * 2)
    ctx.fill()
    ctx.restore()
  }
}

// particle/ParticleEmitter.ts — 发射器(含对象池)
class ParticleEmitter {
  private particles: Particle[] = []
  private pool: Particle[] = [] // 对象池 — 复用死亡粒子实例
  private emitTimer: number = 0

  constructor(
    public x: number = 0,
    public y: number = 0,
    public rate: number = 30, // 每秒发射
    public active: boolean = true
  ) {}

  /** 发射一个新粒子 */
  private emit(): Particle {
    // 优先从对象池获取
    let particle = this.pool.pop()
    if (particle) {
      // 复用旧实例,重置属性
      particle.x = this.x + (Math.random() - 0.5) * 20
      particle.y = this.y + (Math.random() - 0.5) * 20
      particle.vx = (Math.random() - 0.5) * 100
      particle.vy = -Math.random() * 200 - 50 // 向上飞出
      particle.life = particle.maxLife = 0.5 + Math.random() * 0.5
      particle.size = 8
      particle.startSize = 8
      particle.endSize = 2
      particle.color = '#ffcc00'
      particle.endColor = '#ff3300'
      particle.alpha = 1
      particle.startAlpha = 1
      particle.endAlpha = 0
      particle.active = true
    } else {
      // 对象池为空,创建新实例
      particle = new Particle({
        x: this.x + (Math.random() - 0.5) * 20,
        y: this.y + (Math.random() - 0.5) * 20,
        vx: (Math.random() - 0.5) * 100,
        vy: -Math.random() * 200 - 50,
        life: 0.5 + Math.random() * 0.5,
        maxLife: 1.0,
        size: 8,
        endSize: 2,
        color: '#ffcc00',
        endColor: '#ff3300',
        alpha: 1,
        endAlpha: 0,
      })
    }
    this.particles.push(particle)
    return particle
  }

  /** 每帧更新 */
  update(dt: number): void {
    if (!this.active) return

    // 按发射速率创建新粒子
    this.emitTimer += dt
    const emitCount = Math.floor(this.emitTimer * this.rate)
    this.emitTimer -= emitCount / this.rate
    for (let i = 0; i < emitCount; i++) {
      this.emit()
    }

    // 更新活跃粒子,回收死亡粒子
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const p = this.particles[i]
      if (!p.update(dt)) {
        this.particles.splice(i, 1)
        this.pool.push(p) // 回收到对象池
      }
    }
  }

  /** 渲染所有活跃粒子 */
  render(ctx: CanvasRenderingContext2D): void {
    for (const p of this.particles) {
      p.render(ctx)
    }
  }
}
对象池的价值

上述实现中最关键的优化是对象池——不 new Particle() 每次创建,而是回收死亡粒子复用。高频发射的粒子系统(如火焰 50/s),每秒创建/销毁 50 个对象会导致 GC 频繁触发,在低端设备上造成明显卡顿。对象池将 GC 压力降为 0。


GPU 粒子 vs CPU 粒子

粒子系统按计算位置可分为两种实现:

维度CPU 粒子GPU 粒子
计算位置CPU(JS 逐粒子计算)GPU(顶点着色器中计算)
粒子数上限~1000(受 JS 性能限制)~100 万+(受显存限制)
灵活度高(可自定义任意行为)中等(受着色器限制)
碰撞检测✅ 支持(可逐个粒子检测)❌ 不支持
适用场景中小规模、需要交互的效果大规模纯视觉效果
Cocos CreatorParticleSystem2D需自定义 Effect

微信小游戏环境中,V8 引擎的 CPU 粒子性能通常足以支撑大多数 2D 游戏需求。只有在需要数万粒子同时渲染(如大规模战争场景的烟雾)时,才考虑 GPU 粒子方案。


Cocos Creator 3D 粒子 (ParticleSystem)

前面介绍的是 ParticleSystem2D(2D 粒子),适合 2D 游戏。Cocos Creator 3.x 同时提供了 ParticleSystem(3D 粒子),支持更丰富的效果和 GPU 加速。

2D vs 3D 粒子对比

特性ParticleSystem2DParticleSystem
渲染管线2D Canvas/Sprite3D Mesh Renderer
模块化配置属性平铺(life, angle, speed 等)模块系统(Main, Emission, Shape, ColorOverLifetime 等)
受光照影响❌ 不受场景光照✅ 可响应场景光照
深度排序❌ 同层级按添加顺序✅ 基于 Z 深度自动排序
3D 旋转❌ 仅平面旋转✅ 支持三维旋转
性能轻量(适合简单 2D 游戏)功能丰富但开销更大
适用场景传统 2D 游戏特效2.5D/3D 游戏、混合 2D+3D 项目

3D 粒子模块配置

typescript
// particle/Explosion3D.ts — Cocos Creator 3.x
import { _decorator, Component, ParticleSystem, Vec3, Color, AnimationCurve } from 'cc'
const { ccclass, property } = _decorator

@ccclass('Explosion3D')
export class Explosion3D extends Component {
  private ps: ParticleSystem | null = null

  start() {
    this.ps = this.getComponent(ParticleSystem)
    if (this.ps) {
      this.configureExplosion()
    }
  }

  /** 配置爆炸效果 */
  private configureExplosion(): void {
    if (!this.ps) return

    // === Main 模块(基础属性)===
    this.ps.duration = 0.5 // 单次效果持续 0.5 秒
    this.ps.loop = false // 不循环
    this.ps.startLifetime = 1.0 // 单个粒子生命 1 秒
    this.ps.startSpeed = 50 // 初始速度
    this.ps.startSize = 1.0 // 初始大小
    this.ps.startColor = new Color(255, 200, 50, 255) // 橙黄色
    this.ps.simulationSpace = 0 // 0 = Local, 1 = World

    // === Emission 模块(发射频率)===
    this.ps.rateOverTime = 0 // 每秒发射 0(一次性爆发模式)
    // 单次爆发
    this.ps.emitterMode = 0
    // 注:Burst 模式在 ParticleSystem 中通过 burstCount 等属性配置
    // 也可以通过代码在 start 时 emit(count)

    // === Shape 模块(发射形状)===
    this.ps.shapeModule = {
      ...this.ps.shapeModule,
      shapeType: 0, // 球体 (0 = Sphere)
      radius: 1.0, // 球体半径
    }

    // === Color over Lifetime 模块 ===
    // 粒子从橙黄渐变到红色,最后透明
    // 通过 AnimationCurve 或 Gradient 配置

    // === Size over Lifetime 模块 ===
    // 粒子从小变大再消失
    // 通过曲线控制:0s→size=1, 0.5s→size=2, 1s→size=0

    // === Renderer 模块 ===
    this.ps.renderer = {
      ...this.ps.renderer,
      material: undefined, // 自动使用默认粒子材质
      renderMode: 0, // Billboard(始终面向摄像机)
    }
  }

  /** 播放爆炸效果 */
  play(): void {
    if (!this.ps) return
    this.ps.clear()
    this.ps.play()
  }

  /** 停止并隐藏 */
  stop(): void {
    if (!this.ps) return
    this.ps.stop()
    this.ps.clear()
  }
}

何时使用 3D 粒子

场景推荐粒子类型原因
2D 平台游戏(Flappy Bird 等)2D 粒子足够用,性能更好
2D 游戏需要光照效果3D 粒子灯光照射下的粒子效果
2.5D 等距视角游戏3D 粒子深度排序自然正确
3D 游戏3D 粒子唯一选择
混合 2D UI + 3D 粒子3D 粒子UI 层用 Sprite,特效层用 3D 粒子
编辑器 vs 代码

Cocos Creator 编辑器提供了可视化的粒子属性面板,推荐先在编辑器中调整好效果参数,然后在代码中只做 play/stop 控制。手动代码配置粒子的可读性远不如编辑器面板。


GPU 粒子 (GPU Instancing)

传统 CPU 粒子在 CPU 端计算每个粒子的位置、速度、颜色等属性,然后逐个提交给 GPU 渲染——当粒子数超过 500-1000 个时,CPU 计算和 Draw Call 成为瓶颈。

GPU 粒子的核心原理

GPU 粒子将粒子状态计算完全放在 GPU 端(通过 Compute Shader 或顶点着色器),仅需一次 Draw Call 即可提交全部粒子,大幅提升并行度。

CPU 粒子流程:
  CPU: 更新粒子位置 → 构建顶点数据 → 提交 Draw Call(每个粒子系统至少 1 次)
  GPU: 渲染

GPU 粒子流程:
  CPU: 初始化粒子数据 → 提交一次 Draw Call
  GPU: 更新位置 + 渲染(全部在 GPU 端完成)

性能对比

指标CPU 粒子GPU 粒子
最大粒子数500-1,00010,000-100,000+
每帧 CPU 开销O(N) 每个粒子O(1) 仅一次提交
灵活性高(任意逻辑可编码)中(受 Shader 限制)
与场景交互易(碰撞、光线)难(需额外 Pass)
微信小游戏支持✅ 原生支持⚠️ 需引擎/Shader 支持

Cocos Creator 中的 GPU 粒子

Cocos Creator 3.x 的 ParticleSystem 组件默认可以使用 GPU 加速渲染模式:

typescript
// 在 3D 粒子的 Renderer 模块中启用 GPU 渲染
this.ps.renderer = {
  ...this.ps.renderer,
  // 使用 GPU Instancing 渲染(如果材质支持)
  renderMode: 0, // 0 = Billboard(GPU 友好)
  enableCulling: true, // 启用视锥剔除
}

自建 GPU 粒子(WebGL Shader)

如果使用原生 Canvas/WebGL 而非引擎,也可以通过自定义 Shader 实现 GPU 粒子:

glsl
// vertex.glsl — GPU 粒子的顶点着色器(片段)
// 通过顶点属性传入粒子的初始位置和速度
// 利用 uniform float uTime 计算当前位置

attribute vec3 a_position;      // 初始位置
attribute vec3 a_velocity;      // 速度
attribute float a_startTime;    // 粒子生成时间
attribute float a_lifetime;     // 粒子生命周期
attribute float a_size;         // 粒子大小

uniform mat4 u_mvpMatrix;       // 变换矩阵
uniform float u_time;           // 当前游戏时间
uniform float u_gravity;        // 重力

varying float v_alpha;          // 传给片元着色器的透明度

void main() {
  float age = u_time - a_startTime;

  // 生命周期检查
  if (age > a_lifetime || age < 0.0) {
    gl_Position = vec4(0.0, 0.0, -999.0, 1.0); // 移到屏幕外
    v_alpha = 0.0;
    return;
  }

  // 计算当前位置(运动学方程)
  vec3 pos = a_position
    + a_velocity * age
    + vec3(0.0, -u_gravity * age * age * 0.5, 0.0);

  gl_Position = u_mvpMatrix * vec4(pos, 1.0);
  gl_PointSize = a_size * (1.0 - age / a_lifetime); // 粒子逐渐缩小

  // 透明度:开始时最大,结束时透明
  v_alpha = 1.0 - age / a_lifetime;
}

📝 课后练习

练习 1:实现拖尾效果(Trail)

题目: 使用粒子系统为移动角色实现拖尾效果——角色移动时身后留下逐渐淡出的彩色粒子轨迹。

要求:

  1. 拖尾粒子跟随角色的历史位置
  2. 粒子颜色从角色当前位置的颜色渐变到透明
  3. 粒子大小从大到小
  4. 使用对象池避免 GC

参考答案: 记录角色最近 N 帧的位置(环形缓冲区),每帧在缓冲区中均匀采样 M 个位置发射粒子。发射速率应根据角色移动速度动态调整(静止时不发射)。

练习 2:模拟下雨效果

题目: 实现一个全屏下雨粒子效果,要求:

  1. 雨滴从上向下掉落(受重力加速)
  2. 落地时产生小型溅射粒子
  3. 雨滴横向有少量随机偏移
  4. 支持调节降雨密度(小雨/中雨/大雨)

参考答案: 使用两个发射器——主发射器在屏幕上方发射雨滴(窄长矩形,模拟雨水形状),子发射器在雨滴落地位置触发溅射粒子。雨滴使用 GRAVITY 模式,gravity.y 为负值加速下落。


📚 相关阅读

用心学习,持续实践