Skip to content

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 / AsepriteSpine / 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 中搭建

  1. bird.skelbird.atlasbird.png 拖入 assets/spine/ 目录。
  2. 在 Bird 节点上添加 sp.Skeleton 组件,绑定 bird_SkeletonData
  3. 创建 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 选择建议

维度SpineDragonBones
编辑器付费($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,建议:

  1. 使用二进制格式(.skel) — 比 JSON 小约 40-60%
  2. 压缩后网络下发 — 服务端 gzip/brotli 压缩后传输
  3. 剔除未使用的动画 — 导出时只勾选实际使用的动画轨道

📝 课后练习

练习 1:实现一个带状态动画的角色

题目: 用 Spine 或 DragonBones 实现一个玩家角色,要求:

  1. 站立时播放 idle 动画(循环)
  2. 移动时播放 run 动画(循环),与 idle 间有 0.2 秒混合过渡
  3. 点击屏幕播放 attack 动画(单次),完成后回到之前的动画
  4. 被击中时在任何动画上叠加 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 数量)、纹理图集大小和游戏其他逻辑开销。


📚 相关阅读

用心学习,持续实践