SuperAPI AI Directory

particle-modifier

Design & creation

View repository

Updated 10-15 11:09

About this skill

Particle Modifier System Development Guide

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

  1. Add the ParticleModifier component to the particle system node
  2. Add the custom modifier component
  3. 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:

  1. Obtain the particle system reference

    const simulator = (system as any)._simulator;
    
  2. Save the original functions

    this._originalEmitParticle = simulator.emitParticle;
    this._originalStep = simulator.step;
    
  3. 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 to particle.size
  • aspectRatio < 1: height is fixed to particle.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 rotation property

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 ParticleModifier been added?
  • Has the modifier been added to the particle system node?
  • priority Is 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 gradient
  • examples/GravityModifier.ts - Gravity modification
  • examples/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

  1. Prefer X-axis rotation: Rectangular images avoid jumping
  2. Avoid object creation: In onParticleUpdate do not create new objects
  3. Reasonably control the particle count: Recommended < 1000 particles
  4. Use priority to control order: Modifiers execute in priority from smallest to largest
  5. Initialize in onParticleEmit: Particle custom data is initialized at emission time
  6. Extend the IParticle interface: Use TypeScript interfaces to extend custom data types