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 transition 或 animation 的关键帧
  • 对象池 ≈ 虚拟滚动列表中的 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 为负值加速下落。


📚 相关阅读 ​

用心学习,持续实践