主题切换
Spine / DragonBones 骨骼动画
预计阅读 30 分钟
版本信息
最后更新:2026-07-09 · Spine Runtime 4.3+ · DragonBones 5.6+ · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0)
帧动画(Sprite Sheet / 序列帧)是最直观的动画方式——逐帧切换图片。但当成千上万帧的动画需要加载和播放时,帧动画的内存占用和包体大小会迅速失控。骨骼动画(Skeletal Animation) 通过分离骨骼数据与纹理图集,在保持流畅动画的同时将资源体积压缩到帧动画的十分之一甚至更低。本章讲解 Spine 和 DragonBones 两大骨骼动画工具在微信小游戏中的集成与优化。
骨骼动画 vs 帧动画
| 维度 | 帧动画 | 骨骼动画 |
|---|---|---|
| 原理 | 逐帧切换完整图片 | 骨骼驱动顶点变形 + 纹理采样 |
| 内存占用 | 帧数 × 单帧尺寸 | 1 张纹理图集 + 骨骼数据(JSON/Skel) |
| 动画流畅度 | 依赖帧率(通常 12-30fps) | 插值计算,任意帧率平滑 |
| 换装/换肤 | 需要额外图片资源 | 更换插槽附件即可 |
| 动画混合 | 困难 | 原生支持(如跑步→瞄准的过渡) |
| 制作工具 | PS / Aseprite | Spine / DragonBones / Live2D |
| 文件体积 | 较大(多张大图) | 极小(JSON + 1 张图集) |
前端类比
- 帧动画 ≈ CSS Sprite 动画 +
steps()缓动(如 Twitter 点赞动画) - 骨骼动画 ≈ CSS
@keyframes+transform(只移动/旋转元素,不替换图片本身) - 动画混合 ≈ CSS
transition在两个关键帧状态间平滑插值
Spine 运行时集成
Spine 简介
Spine 是业界最主流的 2D 骨骼动画工具,广泛应用于手游、独立游戏和微信小游戏。它提供专业的编辑器(付费)和开源运行时库(Runtime),支持几乎所有主流引擎。
版本说明:Spine 4.3+ 是当前推荐版本。Spine 4.3 引入 Pose 系统、API 重命名等破坏性变更,4.2.x 导出的数据格式与 4.3 不完全兼容,需使用对应版本的 Runtime。
Cocos Creator 中的 Spine 集成
Cocos Creator 3.8.x 内置了 Spine 支持,无需额外安装插件。
步骤 1:导入 Spine 资源
将 Spine 导出的三个文件放入 Cocos Creator 的 assets 目录:
character.skel(或.json)— 骨骼数据character.atlas— 图集描述character.png— 纹理图集
Cocos Creator 会自动识别并生成 .sp.SkeletonData 资源。
步骤 2:在场景中使用
typescript
import { _decorator, Component, sp, Node } from 'cc'
const { ccclass, property } = _decorator
@ccclass('SpineCharacter')
export class SpineCharacter extends Component {
@property(sp.Skeleton)
skeleton: sp.Skeleton | null = null
// 动画名称常量(推荐枚举管理,避免字符串拼写错误)
private readonly ANIM = {
IDLE: 'idle',
RUN: 'run',
ATTACK: 'attack',
DEATH: 'death',
} as const
start() {
if (!this.skeleton) return
// 设置默认动画(循环播放)
this.skeleton.setAnimation(0, this.ANIM.IDLE, true)
}
/** 播放一次攻击动画,完成后回到 idle */
playAttack() {
if (!this.skeleton) return
// setAnimation 会立即切换动画
// addAnimation 会在当前动画播完后追加
this.skeleton.setAnimation(0, this.ANIM.ATTACK, false)
this.skeleton.addAnimation(0, this.ANIM.IDLE, true)
}
/** 使用动画混合实现平滑过渡 */
playRun() {
if (!this.skeleton) return
// trackTime = 0.15 表示 0.15 秒内从当前姿态过渡到 run 动画
this.skeleton.setAnimation(0, this.ANIM.RUN, true)
}
/** 动态换装:切换插槽附件 */
changeWeapon(weaponName: string) {
if (!this.skeleton) return
// 找到武器插槽,替换附件为指定武器
this.skeleton.setAttachment('weapon', weaponName)
}
/** 设置皮肤(整体换装) */
changeSkin(skinName: string) {
if (!this.skeleton) return
this.skeleton.setSkin(skinName)
}
}性能优化关键点
1. 共享纹理图集
多个 Spine 角色如果使用相同的纹理图集,应确保它们引用同一份 SkeletonData,避免重复加载纹理:
typescript
// resources 目录下的 SkeletonData 可通过 resources.load 动态加载
import { resources, sp } from 'cc'
async function loadSharedSkeleton(): Promise<sp.SkeletonData> {
return new Promise((resolve, reject) => {
resources.load('spine/character', sp.SkeletonData, (err, asset) => {
if (err) reject(err)
else resolve(asset)
})
})
}2. 控制同时播放的骨骼数量
每帧更新骨骼矩阵计算有一定 CPU 开销。屏幕上同时播放的骨骼动画建议控制在 20-30 个以内(真机测试为准)。超出部分可降级为静态图片或帧动画。
3. 视口裁剪
屏幕外的骨骼动画可以暂停更新:
typescript
update() {
if (!this.skeleton) return
// 检查节点是否在屏幕可视范围内
const worldPos = this.node.worldPosition
const visible = worldPos.x > -500 && worldPos.x < 1500
&& worldPos.y > -500 && worldPos.y < 1500
if (visible) {
this.skeleton.paused = false
} else {
this.skeleton.paused = true
}
}4. 子分包加载
Spine 资源通常体积不小(纹理图集可达数 MB),建议放入分包中按需加载:
json
// game.json
{
"subpackages": [
{
"name": "spine-assets",
"root": "subpackages/spine/"
}
]
}实战:Flappy Bird 小鸟换装
下面以 Flappy Bird 的小鸟为例,演示如何在 Cocos Creator 中使用 Spine 实现角色动画与换装。
1. 资源准备
在 Spine 编辑器中制作小鸟动画,导出为:
bird.skel(或bird.json)— 骨骼数据bird.atlas— 图集描述bird.png— 纹理图集
确保动画包含以下关键动画:
fly— 循环飞行flap— 扇动翅膀一次die— 死亡
并制作至少两套皮肤:
default— 默认黄色小鸟blue— 蓝色小鸟
2. Cocos Creator 中搭建
- 将
bird.skel、bird.atlas、bird.png拖入assets/spine/目录。 - 在 Bird 节点上添加
sp.Skeleton组件,绑定bird_SkeletonData。 - 创建
BirdSpineController.ts脚本并挂载到 Bird 节点。
typescript
import { _decorator, Component, sp, Vec3 } from 'cc'
const { ccclass, property } = _decorator
@ccclass('BirdSpineController')
export class BirdSpineController extends Component {
@property(sp.Skeleton)
skeleton: sp.Skeleton | null = null
// 动画名常量
private readonly ANIM = {
FLY: 'fly',
FLAP: 'flap',
DIE: 'die',
} as const
start() {
if (!this.skeleton) return
// 默认循环飞行
this.skeleton.setAnimation(0, this.ANIM.FLY, true)
}
/** 跳跃时播放一次扇翅膀动画,然后回到 fly */
playFlap() {
if (!this.skeleton) return
this.skeleton.setAnimation(0, this.ANIM.FLAP, false)
this.skeleton.addAnimation(0, this.ANIM.FLY, true)
}
/** 死亡动画 */
playDie() {
if (!this.skeleton) return
this.skeleton.setAnimation(0, this.ANIM.DIE, false)
}
/** 切换皮肤 */
changeSkin(skinName: 'default' | 'blue') {
if (!this.skeleton) return
this.skeleton.setSkin(skinName)
// 切换皮肤后需要刷新骨骼才能立即生效
this.skeleton.setSlotsToSetupPose()
}
/** 给小鸟戴帽子(替换 head 插槽附件) */
wearHat(hatAttachment: string | null) {
if (!this.skeleton) return
this.skeleton.setAttachment('head', hatAttachment)
}
}3. 与 GameManager 集成
在 Flappy Bird 的 GameManager 中,当玩家点击跳跃时调用 playFlap(),碰撞时调用 playDie():
typescript
// 伪代码:在 InputManager 或 GameManager 中
const birdSpine = this.birdNode.getComponent(BirdSpineController)
// 触摸跳跃
wx.onTouchStart(() => {
this.bird.jump()
birdSpine?.playFlap()
})
// 碰撞死亡
this.bird.onCollide = () => {
birdSpine?.playDie()
this.gameOver()
}4. 换装商店示例
如果游戏有皮肤商店,可以在用户购买后通过 changeSkin 切换:
typescript
// 购买蓝色皮肤后切换
birdSpine.changeSkin('blue')
// 佩戴帽子装饰(假设 Spine 中有名为 "hat" 的附件)
birdSpine.wearHat('cap_red')Spine 4.3 Pose 系统注意
Spine 4.3 引入 Pose 系统,setSkin 后建议调用 setSlotsToSetupPose() 确保新皮肤立即生效。如果在动画播放中切换皮肤,可能需要额外调用 setToSetupPose() 重置 track。
DragonBones 运行时集成
DragonBones(龙骨)是白鹭时代(Egret)开源的骨骼动画系统,曾与 Spine 并列为国内 2D 游戏两大动画方案。虽然官方维护已放缓,但因其完全免费且生态成熟(大量已导出资源可直接使用),在小游戏开发中仍有广泛应用。
Cocos Creator 中的 DragonBones 集成
Cocos Creator 3.8.x 通过 dragonBones 模块支持龙骨动画。
typescript
import { _decorator, Component, dragonBones, Node } from 'cc'
const { ccclass, property } = _decorator
@ccclass('DragonBonesCharacter')
export class DragonBonesCharacter extends Component {
@property(dragonBones.ArmatureDisplay)
armature: dragonBones.ArmatureDisplay | null = null
start() {
if (!this.armature) return
// 播放动画(与 Spine API 类似但略有不同)
this.armature.playAnimation('walk', 1) // 第二个参数是播放次数,-1 表示循环
}
/** 监听动画事件 */
onEnable() {
if (!this.armature) return
// 龙骨支持在动画中嵌入事件标记
this.armature.on(
dragonBones.EventObject.COMPLETE,
() => {
console.log('动画播放完毕')
},
this
)
this.armature.on(
dragonBones.EventObject.FRAME_EVENT,
(event: any) => {
// 处理动画编辑器中设置的帧事件(如"挥刀命中"帧)
console.log('帧事件:', event.name)
},
this
)
}
}Spine vs DragonBones 选择建议
| 维度 | Spine | DragonBones |
|---|---|---|
| 编辑器 | 付费($69-$299/人) | 免费开源 |
| 运行时 | 官方维护,多引擎适配 | 社区维护,Cocos Creator 内置 |
| 社区生态 | 全球最大,资源市场丰富 | 国内生态为主 |
| 动画混合 | 优秀 | 良好 |
| 新项目建议 | ✅ 预算允许首选 | ✅ 零预算或已有龙骨资源 |
| 微信小游戏 | 支持良好 | 支持良好 |
选择建议
- 新项目有预算:选 Spine — 编辑器体验好,资源市场丰富,美术外包接受度高
- 零预算或已有龙骨资源:选 DragonBones — 免费且 Cocos Creator 内置支持
- 两种都支持:如果你的项目可能复用不同来源的动画资源,可以在架构上抽象一层
AnimationPlayer接口,底层同时适配 Spine 和 DragonBones
动画系统架构设计
对于中大型项目,建议将动画播放逻辑封装在统一接口后,避免骨骼动画库的直接耦合:
typescript
// animation/types.ts — 动画系统抽象接口
interface AnimationPlayer {
/** 播放动画,返回动画 ID 用于后续控制 */
play(name: string, loop?: boolean): number
/** 动画混合过渡 */
crossFade(name: string, duration: number): void
/** 停止指定动画 */
stop(trackIndex?: number): void
/** 设置播放速度 */
setTimeScale(scale: number): void
/** 暂停/恢复 */
setPaused(paused: boolean): void
/** 销毁释放资源 */
destroy(): void
}
// animation/SpinePlayer.ts — Spine 实现
// 注意:Spine Runtime 4.3 引入了破坏性变更(Pose 系统、API 重命名等),
// 以下代码基于 Runtime 4.2,使用 4.3 时需参考迁移指南
class SpinePlayer implements AnimationPlayer {
private skeleton: sp.Skeleton
constructor(skeleton: sp.Skeleton) {
this.skeleton = skeleton
}
play(name: string, loop = true): number {
// Spine 4.2: TrackEntry.trackTime 可用于获取动画 ID
// Spine 4.3: TrackEntry API 有变更,需改用其他属性
const entry = this.skeleton.setAnimation(0, name, loop)
return entry?.trackIndex ?? 0 // 使用 trackIndex 而非 trackTime 作为动画 ID
}
crossFade(name: string, duration: number): void {
// 真正的交叉淡入淡出:先设置混合时长,再添加动画到轨道
const entry = this.skeleton.setAnimation(0, name, true)
if (entry) {
entry.mixDuration = duration // 设置过渡混合时长
}
}
stop(trackIndex = 0): void {
this.skeleton.clearTrack(trackIndex)
}
setTimeScale(scale: number): void {
this.skeleton.timeScale = scale
}
setPaused(paused: boolean): void {
this.skeleton.paused = paused
}
destroy(): void {
this.skeleton.destroy()
}
}
// animation/DragonBonesPlayer.ts — DragonBones 实现
class DragonBonesPlayer implements AnimationPlayer {
private armature: dragonBones.ArmatureDisplay
constructor(armature: dragonBones.ArmatureDisplay) {
this.armature = armature
}
play(name: string, loop = true): number {
this.armature.playAnimation(name, loop ? -1 : 1)
return 0
}
crossFade(name: string, duration: number): void {
// DragonBones fadeIn 支持混合过渡时间
this.armature.fadeIn(name, duration)
}
stop(): void {
this.armature.stopAnimation()
}
setTimeScale(scale: number): void {
this.armature.timeScale = scale
}
setPaused(paused: boolean): void {
// DragonBones 无直接 pause API,通过 timeScale=0 模拟
this.armature.timeScale = paused ? 0 : 1
}
destroy(): void {
this.armature.destroy()
}
}架构设计要点
- 接口统一:通过
AnimationPlayer接口隔离底层实现,方便未来切换动画库 - 对象池复用:对于频繁创建/销毁的动画角色(如子弹、特效),使用对象池预创建骨骼组件
- 分级加载:核心角色的骨骼动画打包进主包,非核心角色放入分包按需加载
- LOD 策略:远景角色可降级为静态 Sprite 帧,只对近景角色使用完整骨骼动画
内存与包体优化
纹理图集压缩
骨骼动画的核心资源是单张纹理图集(通常 1024×1024 或 2048×2048),优化图集就能大幅减少内存:
| 格式 | iOS 支持 | Android 支持 | 压缩率 | 质量 |
|---|---|---|---|---|
| PNG | ✅ | ✅ | 无 | 无损 |
| ASTC 6×6 | ✅ A9+ | ✅ Mali/Adreno | ~95% | 近无损 |
| ETC2 RGB | ❌ | ✅ GLES 3.0+ | ~75% | 良好 |
| PVRTC 4bpp | ✅ 全系 | ❌ | ~87.5% | 一般 |
微信小游戏支持
wx.getSystemInfoSync()查询平台后动态选择纹理格式。
动画数据压缩
Spine 导出的 JSON 格式骨骼数据可能达到数百 KB,建议:
- 使用二进制格式(.skel) — 比 JSON 小约 40-60%
- 压缩后网络下发 — 服务端 gzip/brotli 压缩后传输
- 剔除未使用的动画 — 导出时只勾选实际使用的动画轨道
📝 课后练习
练习 1:实现一个带状态动画的角色
题目: 用 Spine 或 DragonBones 实现一个玩家角色,要求:
- 站立时播放
idle动画(循环) - 移动时播放
run动画(循环),与 idle 间有 0.2 秒混合过渡 - 点击屏幕播放
attack动画(单次),完成后回到之前的动画 - 被击中时在任何动画上叠加
hit动画(track 1),播放一次后自动清除
参考答案: 使用两个 track:track 0 管理移动/待机状态动画,track 1 管理受击反馈。攻击动画通过 setAnimation + addAnimation 链式调用实现"攻击→待机"的自动过渡。
练习 2:骨骼动画性能基准测试
题目: 在真机上测试不同数量的 Spine 角色同时播放时的帧率,找出你的目标机型的合理上限。
测试参数:
- 角色数:5 / 10 / 20 / 30 / 50
- 动画类型:idle(循环)/ attack(单次触发)
- 记录指标:FPS、Draw Call、JS 帧时间
参考答案: 通常 iPhone 12 及以上可流畅运行 30 个骨骼角色(≥55fps),中端 Android 可能在 20 个左右开始掉帧。实际结果取决于骨骼复杂度(Bone 数量)、纹理图集大小和游戏其他逻辑开销。