主题切换
粒子系统实现
预计阅读 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 Creator | ParticleSystem2D | 需自定义 Effect |
微信小游戏环境中,V8 引擎的 CPU 粒子性能通常足以支撑大多数 2D 游戏需求。只有在需要数万粒子同时渲染(如大规模战争场景的烟雾)时,才考虑 GPU 粒子方案。
Cocos Creator 3D 粒子 (ParticleSystem)
前面介绍的是 ParticleSystem2D(2D 粒子),适合 2D 游戏。Cocos Creator 3.x 同时提供了 ParticleSystem(3D 粒子),支持更丰富的效果和 GPU 加速。
2D vs 3D 粒子对比
| 特性 | ParticleSystem2D | ParticleSystem |
|---|---|---|
| 渲染管线 | 2D Canvas/Sprite | 3D 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,000 | 10,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;
}微信小游戏 GPU 粒子注意事项
- WebGL 1.0 限制:微信小游戏运行环境基于 WebGL 1.0(部分新设备支持 WebGL 2.0),不支持 Compute Shader。GPU 粒子需要用顶点着色器技巧实现。
- 浮点纹理:高级 GPU 粒子技术(如将粒子状态编码为纹理)需要
OES_texture_float扩展,并非所有设备都支持。 - 建议:在微信小游戏中,轻量 2D 粒子(Cocos Creator 2D 粒子/自定义 Canvas 粒子)在大多数场景下足够使用。GPU 粒子主要适用于视觉效果要求很高的 3D 游戏或特殊表现需求。
📝 课后练习
练习 1:实现拖尾效果(Trail)
题目: 使用粒子系统为移动角色实现拖尾效果——角色移动时身后留下逐渐淡出的彩色粒子轨迹。
要求:
- 拖尾粒子跟随角色的历史位置
- 粒子颜色从角色当前位置的颜色渐变到透明
- 粒子大小从大到小
- 使用对象池避免 GC
参考答案: 记录角色最近 N 帧的位置(环形缓冲区),每帧在缓冲区中均匀采样 M 个位置发射粒子。发射速率应根据角色移动速度动态调整(静止时不发射)。
练习 2:模拟下雨效果
题目: 实现一个全屏下雨粒子效果,要求:
- 雨滴从上向下掉落(受重力加速)
- 落地时产生小型溅射粒子
- 雨滴横向有少量随机偏移
- 支持调节降雨密度(小雨/中雨/大雨)
参考答案: 使用两个发射器——主发射器在屏幕上方发射雨滴(窄长矩形,模拟雨水形状),子发射器在雨滴落地位置触发溅射粒子。雨滴使用 GRAVITY 模式,gravity.y 为负值加速下落。