# Particle Modifier

> Cocos Creator 2.4.x 粒子系统修饰器开发框架。用于创建、使用和调试自定义粒子修饰器，支持修改粒子位置、颜色、大小、旋转、速度等属性。当需要在 Cocos Creator 中实现复杂的粒子效果控制时使用此技能。

- Skill: `958877748/particle-modifier` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add 958877748/particle-modifier`
- Raw SKILL.md: https://api.skillmd.com/api/skills/958877748/particle-modifier/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: 958877748 (https://skillmd.com/u/958877748)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/958877748/particle-modifier

---


# 粒子修饰器系统开发指南

## 概述

本技能提供 Cocos Creator 2.4.x 粒子系统的自定义修饰器开发框架。通过编写修饰器类，可以精确控制每个粒子的行为，实现复杂的视觉效果。

## 核心文件结构

```
项目根目录/
├── 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   # 叶子旋转修饰器示例
```

## 快速开始

### 步骤 1：复制核心文件
将 `ParticleModifier.ts` 和 `ParticleModifierBase.ts` 复制到项目的任意目录（建议放在 `assets/scripts/` 下）

### 步骤 2：创建自定义修饰器
创建继承自 `ParticleModifierBase` 的新类，实现 `onParticleUpdate` 方法

### 步骤 3：在编辑器中配置
1. 在粒子系统节点上添加 `ParticleModifier` 组件
2. 添加自定义修饰器组件
3. 配置参数并运行

## 修饰器开发模板

### 基础模板

```typescript
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 接口

```typescript
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;             // 半径变化率
}
```

**注意**：IParticle 接口可以被扩展以存储自定义数据，但必须在 `onParticleEmit` 中初始化。

## 技术原理

### 粒子修饰器拦截机制

粒子修饰器系统通过以下方式拦截粒子更新：

1. **获取粒子系统引用**
   ```typescript
   const simulator = (system as any)._simulator;
   ```

2. **保存原始函数**
   ```typescript
   this._originalEmitParticle = simulator.emitParticle;
   this._originalStep = simulator.step;
   ```

3. **覆盖关键函数**
   ```typescript
   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 处理逻辑

引擎底层的 `updateParticleBuffer` 函数（`js/particle-simulator.js:257`）：
```javascript
let aspectRatio = particle.aspectRatio;
aspectRatio > 1 ? (height = width / aspectRatio) : (width = height * aspectRatio);
```

这就是长方形图片旋转跳变的根本原因！当 `aspectRatio` 从 >1 变到 <1 时，引擎会切换锚点。

## 常见修饰器模式

### 1. 位置修改

```typescript
onParticleUpdate(particle: IParticle, dt: number, system: cc.ParticleSystem): void {
    // X 轴摆动
    particle.pos.x += Math.sin(Date.now() / 1000) * 10 * dt;
}
```

### 2. 颜色渐变

```typescript
// 需要先定义扩展接口
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. 大小呼吸

```typescript
// 基于粒子生命周期计算呼吸效果
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 旋转（关键）

**⚠️ 重要：aspectRatio 跳变问题**

长方形图片旋转时会出现视觉跳变，因为引擎底层硬切换锚点：
- `aspectRatio > 1`：宽度固定为 `particle.size`
- `aspectRatio < 1`：高度固定为 `particle.size`

**解决方案：长方形图片（AR > 1）只使用 X 轴旋转**

```typescript
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;
        }
    }
}
```

**使用建议：**
- 长方形图片（AR > 1）：使用 `rotationAxis = 'X'`
- 正方形图片（AR ≈ 1）：可以使用任意轴
- 配合粒子 Z 轴旋转：只做 X 轴 3D 翻转 + 粒子 `rotation` 属性

## 高级功能

### 访问顶点缓冲区

**⚠️ 警告**：此功能需要深入了解引擎顶点格式，仅用于高级场景

```typescript
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
}
```

### 多修饰器协同

在编辑器中添加多个修饰器，按 `priority` 顺序执行（数值越小越先执行）：

```
ParticleSystem
├── ParticleModifier (胶水组件，自动收集并排序子修饰器)
├── GravityModifier (priority: 0)       # 先应用重力
├── LeafRotationModifier (priority: 1) # 再应用 3D 旋转
└── ColorFadeModifier (priority: 2)    # 最后应用颜色渐变
```

**执行顺序影响**：
- 如果先应用颜色渐变，再应用 3D 旋转，旋转不会影响颜色
- 优先级决定了修饰器的应用顺序，可能影响最终效果

## 调试技巧

### 查看粒子系统状态

**⚠️ 性能警告**：以下调试方法会产生大量日志，仅在开发环境使用，生产环境务必移除

```typescript
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);
    }
}
```

### 监控 aspectRatio

```typescript
if (particle.aspectRatio > 1.1 || particle.aspectRatio < 0.9) {
    console.log('aspectRatio 异常:', particle.aspectRatio);
}
```

### 性能分析

```typescript
const startTime = Date.now();
// ... 修饰器逻辑
const endTime = Date.now();
if (endTime - startTime > 1) {
    console.warn('执行时间过长:', endTime - startTime, 'ms');
}
```

## 质量校验清单

在创建修饰器时，请确保满足以下标准：

- [ ] **性能检查**：避免在 `onParticleUpdate` 中创建新对象
- [ ] **初始化检查**：粒子自定义数据在 `onParticleEmit` 中初始化
- [ ] **生命周期检查**：不修改粒子数组，只修改单个粒子属性
- [ ] **兼容性检查**：确认粒子系统模式（Gravity 或 Radius）
- [ ] **aspectRatio 检查**：长方形图片使用 X 轴旋转避免跳变
- [ ] **priority 设置**：合理设置优先级控制执行顺序

## 常见问题与解决方案

### 问题 1：修饰器未生效

**检查清单：**
- [ ] 是否添加了 `ParticleModifier` 组件？
- [ ] 修饰器是否已添加到粒子系统节点？
- [ ] `priority` 是否正确（按顺序执行）？

### 问题 2：性能问题

**优化建议：**
- 减少粒子数量（建议 < 1000）
- 简化修饰器逻辑
- 避免复杂的数学运算

### 问题 3：长方形图片旋转跳变

**解决方案：**
- 使用 `rotationAxis = 'X'`
- 公式：`aspectRatio = originalAspectRatio / cos`

### 问题 4：粒子间交互需求

**实现方法：**
- 在修饰器中维护粒子列表
- 在 `onParticleUpdate` 中遍历计算

### 问题 5：自定义数据丢失

**解决方案：**
- 必须在 `onParticleEmit` 中初始化自定义数据
- 使用 TypeScript 接口扩展 IParticle

## 参考资源

### 示例文件（位于本技能包的 `examples/` 目录）
- `examples/ColorFadeModifier.ts` - 颜色渐变
- `examples/GravityModifier.ts` - 重力修改
- `examples/LeafRotationModifier.ts` - 3D 旋转

**注意**：这些示例文件位于粒子修饰器技能包中，可参考实现方式

### 引擎源码（位于本技能包的 `js/` 目录，供理解原理使用）

#### `js/particle-simulator.js` - 粒子模拟器
- **Particle 类定义**：粒子对象结构，包含位置、颜色、大小、旋转、生命周期等属性
- **emitParticle 函数**：发射新粒子，初始化粒子属性
- **step 函数**：每帧更新所有粒子，包括发射、更新、回收
- **updateParticleBuffer 函数**：更新顶点缓冲区，包含 **aspectRatio 处理的关键逻辑**（根据 aspectRatio 切换宽高锚点）
- **对象池**：粒子对象池管理，避免频繁创建销毁

#### `js/CCParticleSystem.js` - 粒子系统组件
- **属性定义**：粒子系统所有可配置属性（totalParticles、duration、emissionRate、life、startColor、endColor、gravity、speed 等）
- **_simulator 初始化**：创建粒子模拟器实例
- **lateUpdate 函数**：每帧调用模拟器的 step 方法
- **emitterMode**：GRAVITY（重力模式）和 RADIUS（半径模式）
- **positionType**：FREE（自由模式）、RELATIVE（相对模式）、GROUPED（整组模式）

#### `js/particle-system-assembler.js` - 渲染汇编器
- **getBuffer 函数**：获取顶点缓冲区（QuadBuffer）
- **fillBuffers 函数**：提交渲染数据到 GPU
- **顶点格式**：vfmtPosUvColor（位置 + UV + 颜色）
- **每粒子顶点数**：4 个顶点，6 个索引（2 个三角形）

#### `js/CCParticleAsset.js` - 粒子资源
- **spriteFrame 属性**：粒子贴图资源
- **支持格式**：plist 格式的粒子配置文件

**注意**：这些是 Cocos Creator 引擎源码，位于技能包中供理解原理使用

## 最佳实践

1. **优先使用 X 轴旋转**：长方形图片避免跳变
2. **避免对象创建**：在 `onParticleUpdate` 中不创建新对象
3. **合理控制粒子数**：建议 < 1000 个粒子
4. **使用 priority 控制顺序**：修饰器按 `priority` 从小到大执行
5. **在 onParticleEmit 初始化**：粒子自定义数据在发射时初始化
6. **扩展 IParticle 接口**：使用 TypeScript 接口扩展自定义数据类型
