运行环境
Cocos Creator 3.5.2 web/native
需求
在 2D/3D 游戏中,动画换装是一种常见的需求, 而 2D 游戏中,Spine 是一个强大且应用广泛的骨骼动画工具,那么 Spine 换装则是很多开发者避不开的话题了。按照羽毛的理解,根据需求和实现方式,可以将换装区分为:
-
整体换装 -
局部换装
整体换装以及局部换装有不同的实现方案,其中局部换装可分为:
-
Spine 皮肤附件替换换肤 -
外部贴图换肤 -
socket 挂点换肤
具体选择哪种方案更为合适,需要根据项目需求,从程序以及美术维护便利性、性能瓶颈等方面综合考虑。
首先,羽毛将在本文基于 Cocos Creator 3.5.2,介绍自己对各种方案的利弊思考、选择以及实现方式与避坑过程。
其次,在实现过程中,还需要考虑 web 平台以及原生平台的差异,进行适配。羽毛也将向大家分享在不同平台(web、小游戏、原生)实现适配过程中遇到的困难以及解决方式。
原理
Spine 基本概念
Spine 是 2D 骨骼动画的一种实现方案,类似的方案方案还用 DragonBone。Spine 简单概况来说,就是通过设计骨骼运动关键帧运动信息、蒙皮信息,在运行时关键帧之间的数据将由 Spine 自动计算完成,由骨骼(bone)驱动插槽(slot),插槽驱动附件(attachment,附件的一项重要信息则是贴图)进行移动、旋转、显示、形变等表现。
相比帧动画,骨骼动画能够在运行时根据数据对象进行动画插值,一般来说,具有体积更小、更少美术资源要求、更好的动画流畅效果、动画混合、可程序控制骨骼等优点。
换装,本质上,就是换插槽上的附件贴图。贴图来源可以是 Spine 导出文件中带有的贴图,也可以是 Cocos Creator 中的 texture。文中的整体换装以及皮肤附件换装即是使用自带贴图进行换装。而外部贴图顾名思义就是使用 Cocos Creator 中的 texture 进行换装。
至于挂点,简单理解则是选用一根骨骼作为挂点 A,将挂点 A 作为欲挂 Cocos 中节点(Node)B 的父节点,B 将随着 A 的移动、旋转等(暂时已知有这些属性,更多的属性不确定,可查询文档,文档未说明的可阅读引擎源码及测试验证)进行变化。挂点常用于武器的更换,或一些不便于使用外部贴图却可以使用挂点完成的局部换装需求。
Spine 简单示意图
Spine 渲染流程图
分析与实现
整体换装
整体换装
-
-
优点
相对来说实现比较简单,无需修改引擎,三端统一。在 Spine 编辑软件中设置好皮肤,运行时基于运行库调用 api 即可,运行库的集成 Cocos Creator 也已帮我们完成了,支持版本如下,需要自行升级的同学也可以到 Spine 官方仓库下载进行替换适配。
-
-
-
缺点
无法满足局部换肤的需求,如果存在 m 个需要局部换肤的部位,同时每个部位有 n 种皮肤,通过命名组合的方式切换皮肤以实现局部换肤,则需要 mxn 个完整皮肤才能完整覆盖所有的皮肤,当 m、n 稍微有点大,日常的维护将会非常的麻烦,成本较大,同时随着 m 与 n 的增大,图集的大小也将不受控制,耗费内存,降低加载速度。
因此,整体换肤比较适合只需要整体换装或者只有两三个部位两三个皮肤进行组合的情况。
-
实现:
-
在 Spine 动画编辑软件中创建皮肤并选择生效 -
并且在需要动态换肤的部件上设置皮肤占位符,在占位符下放入生效皮肤所需要显示的贴图。这部分内容应该是动画制作的美术同学需要更多的关注,但开发同学也需要大概了解一下机制。动作工程内容制作这块更多的内容请参考 Spine 官方说明:Spine用户指南-皮肤[1] -
运行时程序控制,调用 setSkin(skin_name:string) 即可
-
@property({
type: sp.Skeleton
})
role: sp.Skeleton;
cur_skin_name = "full-skins/girl-spring-dress"
start() {
this.role.setSkin(this.cur_skin_name);
}
onSetFullSkin(event: TouchEvent, data: string) {
if (data != "") {
this.role.setSkin(data);
this.cur_skin_name = data;
}
}
同时,整体换装的实现也可参考官方文档的介绍,也写得非常详细了,这里给写文档的同学点个赞!Spine Skeleton 组件参考[2]
局部换装
局部换装——附件换装
Spine 附件换装则是指在 Spine 工程内针对某一部位插槽 SlotA 创建皮肤(记为 SkinPart)并记录皮肤占位符,运行时通过查询局部皮肤 SkinPart 中的附件(记为 attachmentPart),使用局部皮肤对应位置的附件 attachmentPart 替换全身皮肤中 SlotA 下的附件(attachmentFull)。
-
优点:
1.可以实现皮肤组合,相对全局换装可以实现局部换装组合。
2.换装效果在工程中所见即在运行中所得,视觉效果更多的交由美术处理,效果更加可控。3.相对外部贴图的局部换装方式,部件的图集可参与 Spine 的图集合图,减少 DrawCall。
4.无需修改引擎,native 与 web 端表现统一,在小游戏端也不会因为修改引擎而无法使用分离引擎功能,从而导致加长了游戏加载的时间成本。
至于现有的外部贴图换装的 DrawCall 局限性原因将在后续内容介绍。
-
缺点 -
当前不使用的皮肤贴图造成不必要的内存浪费,当 Spine 数量上升时问题尤其明显。 -
皮肤的贴图与 Cocos Creator 场景中的节点 sprite 无法共用,在需要共用的场景下,造成一份内存浪费。同时实例化速度也会变慢。 -
当部件与皮肤数量上升后,Spine 工程逐渐变得臃肿,难于管理且不利于多人协作。
-
-
实现
1、在 Spine 工程中创建全身皮肤、对每一个需要换肤的部件出 n 个局部皮肤 SkinParts,导出 Spine。2、运行时查询某一局部皮肤 SkinPart 中的附件(记为 attachmentPart),使用替换全身皮肤中 SlotA 下的附件(attachmentFull)。
/**
* @param skinName 要替换的部件皮肤名称
* @param slotName 要替换的部件的插槽名称
* @param targetAttaName Spine中皮肤占位符的名字
*/
changeSlot(skinName: string, slotName: string, targetAttaName: string) {
//查找局部皮肤
let skeletonData = this.role.skeletonData.getRuntimeData();
let targetSkin: sp.spine.Skin = skeletonData.findSkin(skinName);
//查找局部皮肤下的插槽与附件
let targetSkinSlotIndex = skeletonData.findSlotIndex(slotName);
let atta = targetSkin.getAttachment(targetSkinSlotIndex, targetAttaName);
//查找全身皮肤下的插槽
let curSlot = this.role.findSlot(slotName);
//替换全身皮肤插槽的附件
curSlot && curSlot.setAttachment(atta);
}