9.3 粒子与骨骼动画
前两节把《方块冲刺》的「动」分成了两条路:Animation 剪辑管角色姿态,tween 管单属性动效。但还有两类东西它们都做不了。
第一类是爆发式特效:金币被拾取时炸开一圈金色小点、角色加速时身后拖出残影、通关时满屏彩带。这些特效的粒子数量从几十到几百,每个粒子有自己的位置、速度、颜色、生命周期——用关键帧描述它们不现实,用 tween 一个一个补更不现实。
第二类是骨骼动画:角色要换皮肤、要复用同一套骨架做不同动作、要由美术在 Spine 里精细调曲线。这类需求下,逐帧图集(序列帧)太占内存,也改不动。
这一节把这两类交给引擎的专门系统:ParticleSystem2D 和 sp.Skeleton。
9.3.1 粒子与骨骼:两种完全不同的需求
先把概念分清楚,选型才不会错:
| 维度 | 粒子系统 | 骨骼动画 |
|---|---|---|
| 描述对象 | 大量无固定形态的小点 | 有骨架层级结构的角色 |
| 数据来源 | 发射器参数(速度、角度、颜色曲线) | 美术用 Spine / DragonBones 导出 |
| 典型用途 | 爆炸、拖尾、雨雪、星光 | 角色待机/跑/跳/受击 |
| 能否换皮 | 否 | 是(换 skin) |
| 内存 | 低(同一张贴图 + 参数) | 中(骨架 + 图集) |
| 运行开销 | 与粒子数成正比 | 与骨骼数、图集 Draw Call 相关 |
一句话选型:要「一堆点」用粒子,要「一具骨架」用骨骼。两者都不属于 9.1 的 Animation 剪辑,因为剪辑的本质是「给已有节点打属性关键帧」,而粒子和骨骼的内部状态由引擎自己管。
9.3.2 ParticleSystem2D:2D 粒子系统
Cocos 3.8 有两个粒子组件:3D 的 ParticleSystem 和 2D 的 ParticleSystem2D。《方块冲刺》是纯 2D 项目,用 ParticleSystem2D——它挂在 UI 节点上,和 Sprite 同一套坐标系,不受 3D 相机影响。
它的核心参数分四组:
| 组 | 参数 | 含义 |
|---|---|---|
| 发射 | duration / emissionRate | 持续多久、每秒发多少个 |
| 生命周期 | life / lifeVar | 单个粒子活多久、随机浮动范围 |
| 运动 | angle / angleVar / speed / speedVar | 初速度的方向与大小 |
| 外观 | startColor / endColor / startSize / endSize | 颜色与尺寸的起止值 |
带 Var 后缀的都是随机浮动量:angle = 90, angleVar = 30 表示每个粒子的初始角度在 60 到 120 度之间随机。这是粒子看起来「自然」的关键——如果所有粒子参数完全一致,它们会像一束激光而不是一团火花。
9.3.3 编辑器里配一个金币拾取粒子
这一步属于 L3——需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。目标是做 assets/prefabs/coin-burst.prefab。
- 层级管理器右键 → 创建 → 2D 对象 →
ParticleSystem2D,命名为CoinBurst。 - 在属性检查器把
Sprite Frame设为textures/particle-star.png(一张 16×16 的白色小星)。 - 设
Duration为0.1(一次性爆发),Emission Rate为0(不持续发射,靠代码触发)。 - 展开
Start Color,把 RGBA 设为255, 220, 60, 255(金黄色);End Color设为255, 255, 255, 0(淡出到透明)。 Life设0.5,Life Var设0.2。Angle设90,Angle Var设360;Speed设180,Speed Var设60。- 勾选
Play On Awake取消,勾选Auto Remove On Finish。 - 把
CoinBurst拖进assets/prefabs/,得到coin-burst.prefab。
操作完成后,属性面板上应显示:Total Particles 约 100,Duration 为 0.1,Emission Rate 为 0,Start Color 是金黄色,Auto Remove On Finish 勾选。把预制体拖进场景预览,你会看到点一次「播放」,节点位置炸开一圈金色小点后自动消失。
Auto Remove On Finish 很关键:粒子播完自动 destroy 节点,省得你在代码里手动回收。
9.3.4 用代码控制粒子:reset / stop
一次性粒子靠 resetSystem() 触发。下面这个组件可以过 tsc --noEmit:
import { _decorator, Component, Node, ParticleSystem2D, Prefab, instantiate } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('CoinBurstSpawner')
export class CoinBurstSpawner extends Component {
@property(Prefab)
private burstPrefab: Prefab | null = null;
/** 在指定世界坐标炸一次金币特效 */
playAt(x: number, y: number): void {
if (!this.burstPrefab) {
return;
}
const fx = instantiate(this.burstPrefab);
fx.setPosition(x, y, 0);
this.node.addChild(fx);
const ps = fx.getComponent(ParticleSystem2D);
// 手动触发一次发射;节点会因 Auto Remove On Finish 自行销毁
ps?.resetSystem();
}
}
要点:
instantiate(prefab)复制一份预制体实例,每次拾取都新建一个,互不干扰。fx.getComponent(ParticleSystem2D)拿到粒子组件后调resetSystem()——它会清空已有粒子并重新开始发射。- 如果不想自动销毁,也可以保留节点、只调
stopSystem()停止发射。
resetSystem 与 stopSystem 的区别值得记牢:stopSystem 只停止产生新粒子,已经在飞的粒子会继续飞完;resetSystem 是立刻清空并重来。做「连续拾取」时,后者才不会让上一波的残留粒子混进来。
9.3.5 粒子文件在编辑器里打开会看到什么
粒子预制体保存为 .prefab,而它的粒子参数存在节点的一个 ParticleSystem2D 组件里。需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现,片段如下:
{
"__type__": "cc.ParticleSystem2D",
"_name": "",
"_duration": 0.1,
"_emissionRate": 0,
"_life": 0.5,
"_lifeVar": 0.2,
"_angle": 90,
"_angleVar": 360,
"_speed": 180,
"_speedVar": 60,
"_startColor": { "__type__": "cc.Color", "r": 255, "g": 220, "b": 60, "a": 255 },
"_endColor": { "__type__": "cc.Color", "r": 255, "g": 255, "b": 255, "a": 0 },
"_autoRemoveOnFinish": true
}
打开编辑器后你会看到:资源管理器 assets/prefabs/ 下是 coin-burst.prefab;双击它,层级管理器只有 CoinBurst 一个根节点,属性检查器里是这个 ParticleSystem2D 组件,各项数值和上面对得上。注意颜色是 cc.Color 对象,四个通道 r/g/b/a 各占一个字段——改颜色时改的是这四个数,不是十六进制字符串。
老版本(Cocos 2.x)的粒子用
.plist文件存参数。3.8 已经把它内联进.prefab的组件里,assets/下不会再出现.plist。如果从网上抄到「编辑.plist」的教程,那是对 2.x 说的,别照搬。
9.3.6 Spine 骨骼动画:sp.Skeleton
Spine 是目前 2D 骨骼动画的事实标准。美术在 Spine 编辑器里画好骨架、K 好动作,导出三样东西:.json(或 .skel)描述数据、.atlas 描述图集、一张或多张 .png 图集贴图。导入 Cocos 后,用 sp.Skeleton 组件播放。
编辑器侧(需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现):
- 把美术导出的
hero.json、hero.atlas、hero.png一起拖进assets/textures/hero/。Cocos 会自动把它们识别成一套 Spine 资源。 - 层级管理器选中角色节点 → 添加组件 → 搜
sp.Skeleton。 - 把
hero拖到组件的Skeleton Data字段。 Default Skin留空(用默认皮肤),Animation字段可以先选idle。Premultiply Alpha按美术导出时的设置勾选,通常默认即可。
代码侧:
import { _decorator, Component, sp } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('HeroSpine')
export class HeroSpine extends Component {
@property(sp.Skeleton)
private skeleton: sp.Skeleton | null = null;
playRun(): void {
if (!this.skeleton) {
return;
}
// trackIndex 0 是主轨道;第三个参数 true 表示循环
this.skeleton.setAnimation(0, 'run', true);
}
playHurt(): void {
if (!this.skeleton) {
return;
}
// 受击不循环,播完停在最后一帧
this.skeleton.setAnimation(0, 'hurt', false);
}
}
setAnimation(trackIndex, name, loop) 的三个参数:
trackIndex:轨道号。0 是主轨道;要做「跑动的同时播放一层武器挥砍」这种叠加,才用 1、2 号轨道。name:Spine 里 K 好的动作名。loop:是否循环。跑动循环,受击不循环。
还有一个 addAnimation(trackIndex, name, loop, delay)——它把动作排进队列,等当前动作播完再接上。适合「受击后自动接回跑动」这种连贯过渡。
9.3.7 DragonBones 与 SkeletalAnimation
Cocos 也内置了 DragonBones 支持,对应的组件是 SkeletalAnimation。它继承自 Animation,所以 9.1 里那套 play / crossFade / AnimationState 的用法可以复用到骨骼上——这是它相对 sp.Skeleton 的一个便利:动画状态管理是同一套 API。
| 项 | Spine (sp.Skeleton) | DragonBones (SkeletalAnimation) |
|---|---|---|
| 数据格式 | .json/.skel + .atlas | _ske.json + _tex.json + _tex.png |
| 播放 API | setAnimation / addAnimation | play / crossFade(继承自 Animation) |
| 换皮 | setSkin | setSkin |
| 社区生态 | 更广,工具链更成熟 | 更轻量,免费版限制少 |
选哪个?《方块冲刺》这种小项目,看美术习惯——团队里谁熟哪个就用哪个,引擎侧两边都支持。如果美术没偏好,默认选 Spine,教程和第三方资源更多。
还有一条工程上的提醒:骨骼资源不要和粒子混在一个节点上。sp.Skeleton 是 UIRenderer,粒子的 spriteFrame 也是渲染组件,两者叠在同一节点会互相覆盖渲染属性。正确做法是骨骼挂角色根节点、粒子挂一个子节点,各管各的。
9.3.8 选型表与性能
把这一章三套表现系统合起来看:
| 需求 | 用哪个 | 关键 API |
|---|---|---|
| 角色多属性姿态、需反复播 | Animation 剪辑 | crossFade / play |
| 单属性一次性动效 | tween | to / by / sequence |
| 大量小点的爆发特效 | ParticleSystem2D | resetSystem / stopSystem |
| 换皮、精调曲线的角色 | sp.Skeleton / SkeletalAnimation | setAnimation |
性能上给两条量级参考(参考值,需在你的设备上复测):
- 一个
ParticleSystem2D的粒子数控制在 200 以内,中低端手机上通常还能稳住 60 帧;超过这个量级,优先减粒子而不是减其他东西。 - Spine 角色的开销主要来自图集拆分的 Draw Call,不是骨骼数。同一角色的所有插槽尽量放进一张图集,能显著减少 Draw Call。
粒子还有一个容易忽略的坑:同屏多个粒子节点会各自产生 Draw Call。金币特效一次炸一个没问题,但一屏十几个金币同时被磁铁吸走、每个都炸一次,Draw Call 会瞬间飙升。正确做法是复用一个粒子节点,或者干脆用一张序列帧图集做「假粒子」。
9.3.9 没有美术资源时:序列帧兜底
粒子图、Spine 骨架都依赖美术产出。原型阶段常常拿不到,这时候别卡住——序列帧是成本最低的兜底:让美术(或自己用工具)把一段爆炸导出成 6~8 张 PNG,拼成一张图集,用 Animation 剪辑按帧切。
做法:把图集拖进项目,用编辑器自带的图集切图功能按等分切成 8 帧,得到一个 SpriteFrame 数组;再建一条 spriteFrame 轨道的 Animation 剪辑,每 0.05 秒切一帧。这套流程和第 9.1 节的剪辑完全一致,只是轨道属性从 position 换成 spriteFrame。
序列帧的代价是内存:8 帧 128×128 的 RGBA 图集约 0.5 MB,而同等视觉的粒子可能只要 20 KB。所以它是原型的拐杖,不是终态——上线前应替换成粒子或骨骼。
9.3.10 工程视角的收口
这一节做完,工程里多了:
assets/prefabs/coin-burst.prefab:金币拾取粒子,Auto Remove On Finish开启;assets/scripts/game/coin-burst-spawner.ts:按坐标生成粒子;assets/textures/hero/:Spine 三件套(若角色改用骨骼);assets/scripts/game/hero-spine.ts:骨骼动画播放封装。
到这里,第 9 章的三块表现系统全部就位:Animation、tween、粒子与骨骼。它们各自有明确的适用边界,玩家看到的「角色在跑、金币在转、拾取时炸开」是三套系统协同的结果。
但动效只解决了一半的「反馈」——另一半是声音。金币炸开时如果没有「叮」的一声,玩家仍然觉得空。第 10 章会讲音频:怎么用 AudioSource 和音效池把三端音频管起来,怎么处理浏览器和微信小游戏的自动播放限制,以及怎么做震动和顿帧这种「打击感」的最后一层。
阅读导航:上一节:9.2 Tween 与程序动画 · 下一节:10.1 音频管理器与音效池 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。