Overview
This skill provides a custom modifier development framework for the Cocos Creator 2.4.x particle system. By writing modifier classes, you can precisely control the behavior of each particle and achieve complex visual effects.
Core File Structure
项目根目录/
├── ParticleModifier.ts # 粒子修饰器胶水组件(拦截粒子系统更新)
├── ParticleModifierBase.ts # 修饰器基类(提供钩子方法)
├── SKILL.md # 本技能文档
├── js/ # 引擎核心源码(Cocos Creator 2.4.13)
│ ├── CCParticleAsset.js # 粒子资源类
│ ├── CCParticleSystem.js # 粒子系统组件(主组件)
│ ├── particle-simulator.js # 粒子模拟器(核心逻辑)
│ └── particle-system-assembler.js # 渲染汇编器
└── examples/
├── ColorFadeModifier.ts # 颜色渐变修饰器示例
├── GravityModifier.ts # 重力修饰器示例
└── LeafRotationModifier.ts # 叶子旋转修饰器示例
Quick Start
Step 1: Copy the core files
Copy ParticleModifier.ts and ParticleModifierBase.ts to any directory in the project (recommended to place them under assets/scripts/ )
Step 2: Create a custom modifier
Create a new class that inherits from ParticleModifierBase and implements the onParticleUpdate method
Step 3: Configure in the editor
- Add the
ParticleModifiercomponent to the particle system node - Add the custom modifier component
- Configure the parameters and run
Modifier Development Template
Basic Template
import { IParticle } from "./ParticleModifier";
import ParticleModifierBase from "./ParticleModifierBase";
const { ccclass, property } = cc._decorator;
@ccclass
export class MyCustomModifier extends ParticleModifierBase {
@property({ tooltip: '执行优先级(数值越小越先执行)' })
priority: number = 0;
@property({ tooltip: '属性说明' })
myProperty: number = 1.0;
onParticleEmit(particle: IParticle, system: cc.ParticleSystem): void {
// 粒子发射时调用(可选)
// 初始化粒子自定义数据
}
onParticleUpdate(particle: IParticle, dt: number, system: cc.ParticleSystem): void {
// 粒子每帧更新时调用(必需)
// 更新粒子状态
}
}
IParticle Interface
interface IParticle {
// 位置
pos: cc.Vec2; // 当前位置(相对于发射器)
startPos: cc.Vec2; // 起始位置
drawPos: cc.Vec2; // 绘制位置
// 颜色
color: cc.Color; // 当前颜色
deltaColor: { r, g, b, a }; // 颜色变化率
preciseColor: { r, g, b, a }; // 精确颜色(浮点)
// 大小
size: number; // 当前大小
deltaSize: number; // 大小变化率
aspectRatio: number; // 宽高比
// 旋转
rotation: number; // 当前旋转角度(度)
deltaRotation: number; // 旋转变化率(度/秒)
// 生命周期
timeToLive: number; // 剩余生命周期(秒)
// Mode A: 重力模式
dir: cc.Vec2; // 速度向量
radialAccel: number; // 径向加速度
tangentialAccel: number; // 切向加速度
// Mode B: 半径模式
angle: number; // 当前角度(弧度)
degreesPerSecond: number; // 角速度
radius: number; // 当前半径
deltaRadius: number; // 半径变化率
}
Note: The IParticle interface can be extended to store custom data, but it must be initialized in onParticleEmit .
Technical Principles
Particle Modifier Interception Mechanism
The particle modifier system intercepts particle updates in the following ways:
-
Obtain the particle system reference
const simulator = (system as any)._simulator; -
Save the original functions
this._originalEmitParticle = simulator.emitParticle; this._originalStep = simulator.step; -
Override key functions
simulator.emitParticle = (pos: cc.Vec2) => { this._originalEmitParticle.call(simulator, pos); this._onParticleEmit(particle); }; simulator.step = (dt: number) => { this._originalStep.call(simulator, dt); this._onParticleUpdate(particle, dt); };
aspectRatio Handling Logic
The engine's underlying updateParticleBuffer function (js/particle-simulator.js:257):
let aspectRatio = particle.aspectRatio;
aspectRatio > 1 ? (height = width / aspectRatio) : (width = height * aspectRatio);
This is the root cause of the rotation jump for rectangular images! When aspectRatio changes from >1 to <1, the engine switches the anchor point.
Common Modifier Patterns
1. Position Modification
onParticleUpdate(particle: IParticle, dt: number, system: cc.ParticleSystem): void {
// X 轴摆动
particle.pos.x += Math.sin(Date.now() / 1000) * 10 * dt;
}
2. Color Gradient
// 需要先定义扩展接口
interface IColorFadeOptions extends IParticle {
maxTimeToLive: number; // 记录初始生命周期
}
onParticleEmit(particle: IColorFadeOptions, system: cc.ParticleSystem): void {
particle.maxTimeToLive = particle.timeToLive;
}
onParticleUpdate(particle: IColorFadeOptions, dt: number, system: cc.ParticleSystem): void {
const lifeRatio = 1 - (particle.timeToLive / particle.maxTimeToLive);
particle.color.r = Math.floor(255 * lifeRatio);
particle.color.g = Math.floor(255 * (1 - lifeRatio));
}
3. Size Breathing
// 基于粒子生命周期计算呼吸效果
interface IBreathingSizeOptions extends IParticle {
maxTimeToLive: number; // 记录初始生命周期
baseSize: number; // 记录初始大小
}
onParticleEmit(particle: IBreathingSizeOptions, system: cc.ParticleSystem): void {
particle.maxTimeToLive = particle.timeToLive;
particle.baseSize = particle.size;
}
onParticleUpdate(particle: IBreathingSizeOptions, dt: number, system: cc.ParticleSystem): void {
const lifeRatio = 1 - (particle.timeToLive / particle.maxTimeToLive);
// 基于生命周期的呼吸效果,每个粒子有独立相位
particle.size = particle.baseSize + Math.sin(lifeRatio * Math.PI * 4) * 20;
}
4. 3D Rotation (Key)
⚠️ Important: aspectRatio jump issue
Rectangular images will show a visual jump when rotating because the engine's underlying layer hard-switches the anchor point:
aspectRatio > 1: width is fixed toparticle.sizeaspectRatio < 1: height is fixed toparticle.size
Solution: Rectangular images (AR > 1) should only use X-axis rotation
interface ILeafRotationOptions extends IParticle {
maxTimeToLive: number; // 自定义:记录初始生命周期
originalAspectRatio: number; // 自定义:记录初始宽高比
rotationAngle: number; // 自定义:当前旋转角度
rotationSpeed: number; // 自定义:旋转速度
randomOffset: number; // 自定义:随机偏移
}
@ccclass
export class LeafRotationModifier extends ParticleModifierBase {
@property({ tooltip: '最小旋转速度(圈/秒)' })
minRotationSpeed: number = 0.5;
@property({ tooltip: '最大旋转速度(圈/秒)' })
maxRotationSpeed: number = 2.0;
@property({ tooltip: '旋转轴(X=上下翻转,Y=左右翻转)' })
rotationAxis: 'X' | 'Y' = 'X';
onParticleEmit(particle: ILeafRotationOptions, system: cc.ParticleSystem): void {
// ⚠️ 重要:必须在 onParticleEmit 中初始化自定义数据
particle.maxTimeToLive = particle.timeToLive;
particle.originalAspectRatio = particle.aspectRatio || 1.0;
particle.rotationAngle = 0;
const randomSpeed = this.minRotationSpeed + Math.random() * (this.maxRotationSpeed - this.minRotationSpeed);
particle.rotationSpeed = Math.PI * 2 * randomSpeed;
particle.randomOffset = Math.random() * Math.PI * 2;
}
onParticleUpdate(particle: ILeafRotationOptions, dt: number, system: cc.ParticleSystem): void {
particle.rotationAngle += particle.rotationSpeed * dt;
const currentAngle = particle.rotationAngle + particle.randomOffset;
const cosValue = Math.abs(Math.cos(currentAngle));
const safeCos = Math.max(0.01, cosValue);
if (this.rotationAxis === 'X') {
// X 轴旋转:宽度不变,高度缩放
particle.aspectRatio = particle.originalAspectRatio / safeCos;
} else {
// Y 轴旋转:⚠️ 长方形图片会跳变
particle.aspectRatio = particle.originalAspectRatio * safeCos;
}
}
}
Usage Recommendations:
- Rectangular images (AR > 1): use
rotationAxis = 'X' - Square images (AR ≈ 1): any axis can be used
- With particle Z-axis rotation: only do X-axis 3D flipping + particle
rotationproperty
Advanced Features
Accessing the Vertex Buffer
⚠️ Warning: This feature requires an in-depth understanding of the engine vertex format and is only for advanced scenarios
onParticleUpdate(particle: IParticle, dt: number, system: cc.ParticleSystem): void {
const simulator = (system as any)._simulator;
const buffer = system._assembler.getBuffer();
const vbuf = buffer._vData;
const particleIndex = simulator.particles.indexOf(particle);
// vfmtPosUvColor 格式:每顶点 8 floats (pos:2 + uv:2 + color:4),每粒子 4 顶点 = 32 floats
const FLOAT_PER_PARTICLE = 32;
const offset = particleIndex * FLOAT_PER_PARTICLE;
// 修改第一个顶点的 UV 坐标(offset + 8, offset + 9)
// 第二个顶点:offset + 16, offset + 17
// 第三个顶点:offset + 24, offset + 25
// 第四个顶点:offset + 32, offset + 33
vbuf[offset + 8] = 0.0;
vbuf[offset + 9] = 0.0;
// step 函数会自动上传数据,无需手动调用 uploadData
}
Multi-Modifier Coordination
Add multiple modifiers in the editor; they execute in priority order (the smaller the value, the earlier the execution):
ParticleSystem
├── ParticleModifier (胶水组件,自动收集并排序子修饰器)
├── GravityModifier (priority: 0) # 先应用重力
├── LeafRotationModifier (priority: 1) # 再应用 3D 旋转
└── ColorFadeModifier (priority: 2) # 最后应用颜色渐变
Execution Order Impact:
- If the color gradient is applied first and then 3D rotation, the rotation will not affect the color
- Priority determines the order in which modifiers are applied and may affect the final result
Debugging Tips
Viewing Particle System State
⚠️ Performance Warning: The following debugging methods generate a large amount of logs; use them only in the development environment and be sure to remove them in the production environment.
onParticleUpdate(particle: IParticle, dt: number, system: cc.ParticleSystem): void {
// 仅在每 100 帧输出一次,避免日志爆炸
if (Date.now() % 100 < 16) { // 约 6fps
const simulator = (system as any)._simulator;
console.log('粒子数量:', simulator.particles.length);
console.log('激活状态:', simulator.active);
}
}
Monitor aspectRatio
if (particle.aspectRatio > 1.1 || particle.aspectRatio < 0.9) {
console.log('aspectRatio 异常:', particle.aspectRatio);
}
Performance Analysis
const startTime = Date.now();
// ... 修饰器逻辑
const endTime = Date.now();
if (endTime - startTime > 1) {
console.warn('执行时间过长:', endTime - startTime, 'ms');
}
Quality Checklist
When creating a modifier, make sure the following standards are met:
- Performance check: avoid creating new objects in
onParticleUpdate. - Initialization check: particle custom data is initialized in
onParticleEmit. - Lifecycle check: do not modify the particle array; only modify individual particle properties.
- Compatibility check: confirm the particle system mode (Gravity or Radius).
- aspectRatio check: for rectangular images, use X-axis rotation to avoid jumps.
- priority setting: set the priority appropriately to control execution order.
Common Issues and Solutions
Issue 1: Modifier not taking effect
Checklist:
- Has
ParticleModifierbeen added? - Has the modifier been added to the particle system node?
-
priorityIs it correct (executed in order)?
Issue 2: Performance problems
Optimization suggestions:
- Reduce the number of particles (recommended < 1000)
- Simplify modifier logic
- Avoid complex mathematical operations
Issue 3: Rectangular image rotation jumps
Solution:
- Use
rotationAxis = 'X' - Formula:
aspectRatio = originalAspectRatio / cos
Issue 4: Requirement for interaction between particles
Implementation method:
- Maintain a particle list in the modifier
- In
onParticleUpdate, traverse and calculate
Issue 5: Custom data loss
Solution:
- Custom data must be initialized in
onParticleEmit. - Use a TypeScript interface to extend IParticle.
Reference Resources
Example files (located in this skill package's examples/ directory)
examples/ColorFadeModifier.ts- Color gradientexamples/GravityModifier.ts- Gravity modificationexamples/LeafRotationModifier.ts- 3D rotation
Note: These example files are located in the particle modifier skill package; you can refer to the implementation methods.
Engine source code (located in this skill package's js/ directory, for understanding the principles)
js/particle-simulator.js - Particle simulator
- Particle class definition: Particle object structure, including properties such as position, color, size, rotation, and lifecycle.
- emitParticle function: Emits a new particle and initializes particle properties.
- step function: Updates all particles every frame, including emission, update, and recycling.
- updateParticleBuffer function: Updates the vertex buffer, including the key logic for aspectRatio handling(switches the width/height anchor based on aspectRatio).
- Object pool: Particle object pool management to avoid frequent creation and destruction.
js/CCParticleSystem.js - Particle system component
- Property definitions: All configurable properties of the particle system (totalParticles, duration, emissionRate, life, startColor, endColor, gravity, speed, etc.).
- _simulator initialization: Create a particle simulator instance
- lateUpdate function: Call the simulator's step method every frame
- emitterMode: GRAVITY (gravity mode) and RADIUS (radius mode)
- positionType: FREE (free mode), RELATIVE (relative mode), GROUPED (grouped mode)
js/particle-system-assembler.js - Render assembler
- getBuffer function: Get the vertex buffer (QuadBuffer)
- fillBuffers function: Submit render data to the GPU
- Vertex format: vfmtPosUvColor (position + UV + color)
- Vertices per particle: 4 vertices, 6 indices (2 triangles)
js/CCParticleAsset.js - Particle asset
- spriteFrame property: Particle texture asset
- Supported format: plist format particle configuration file
Note: These are Cocos Creator engine source code, located in the skill package for understanding the principles
Best practices
- Prefer X-axis rotation: Rectangular images avoid jumping
- Avoid object creation: In
onParticleUpdatedo not create new objects - Reasonably control the particle count: Recommended < 1000 particles
- Use priority to control order: Modifiers execute in
priorityfrom smallest to largest - Initialize in onParticleEmit: Particle custom data is initialized at emission time
- Extend the IParticle interface: Use TypeScript interfaces to extend custom data types