找回密码
 立即注册
查看: 546|回复: 3

Unity引擎中Spine加载的深度优化实战

[复制链接]
发表于 2023-3-29 17:14 | 显示全部楼层 |阅读模式
Spine骨骼动画是目前最流行的骨骼动画解决方案,在游戏制作实战中有大量的规模化应用。本篇主要记录了在《星之交响》游戏项目制作过程中遇到的Unity3D引擎中调用Spine动画时遇到的性能相关优化。
在Spine加载过程存在卡帧的情况,于是使用 profiler 对游戏初始化的过程进行帧逻辑分析,发现 spine 资源的加载耗时巨大,故对 spine 资源的加载进行了全面的优化,整体思路上包括下几个方向:

  • 预处理 > 动画资源用 binary(二进制)替换 json(文本)
  • 缓存 > 对加载过的资源增加缓存池
  • 复杂逻辑切片 > 对耗时开销最大的 Animations 数据解析进行按需加载
  • 内存操作切片 > 优化文件读取的方法
1】预处理:用 binary(二进制)替换 json(字符串)

首先在unity中使用profiler调试工具定位到启动时耗时最长的帧,图 1 中显示该帧画面 cpu 耗时 2104.72ms,超过了 2 秒,此时游戏中出现了明显的卡帧。



图1 profiler分析预加载卡帧



图2 PlayerLoop中的sc_camera_follow_player.Start()函数用时过长(431ms)


继续查看该帧的调用耗时表(图 5),可知主要的 PlayerLoop 消耗来自脚本 sc_camera_follow_player 的 Start()方法,该脚本是绑定在地图主场景中并伴随着场景加载时运行初始化功能,主要进行以下两类资源 的预加载:

  • 预加载场景中角色的 spine 动画资源
  • 预加载场景中会使用到的音效资源
这两类资源的运行除了主线程外主要关联 AsyncRead 和 PreloadManager 两个异步 timeline,仔细查看 profile 可以发现 spine character 资源是在 AsyncRead 中载入的,于是对其进行加载耗时跟踪,发现单角色 spine 动画资源载入就需要耗时 1 秒左右。



图 3 character.json 格式数据的加载耗时1004ms

Spine 默认采用了 json 文本方式进行了资源导出与载入,在这种加载方式中首先通过文本读取把整个文件以字符串string格式读进内存,然后进行 json 格式数据解析并转换为可直接调用的类SkeletonData实例中,解析完成后再通过 GC 把文本回收。细心的小伙伴们会发现,在这个过程中对文本文件的加载和string的处理是一个中间过程,如果可以跳过这个过程直接把数据以内存中的二进制进行读取和加载,可以节省大量的耗时间。
通过EsotericSoftwaret提供的官方文档查询得知 Spine 支持编译导出二进制,并通过类 SkeletonBinary 在 unity 中进行加载:
【api链接】 http://zh.esotericsoftware.com/spine-binary-format
在Spine中重新通过binary格式导出资源



图4 json格式文件



图5 binary格式文件


通过图 4 和图 5 可以看到两种格式的文件占用空间悬殊,因二进制 binary 格式不需要json行字符串的对齐和标准化格式数据,对同样的一个spine工程导出文件尺寸差距达到了55%。



图6 Binary(skel)格式数据的加载耗时

同时在载入的过程中因为无需进行数据转译,数据解析耗时从 1000ms 级别降到 150ms 级别,在加载效率上有本质的差距。

2】增加缓存池


通过 spine 官方工具把资源切换成二进制后,对单个 spine 文件的加载效率已经有了大幅的提高。但实际在 游戏中,一个场景内可能存在很多的不同角色,对大量角色的加载同样容易造成卡帧和阻塞。




图7 加载多个角色动画资源耗时(未缓存)

在实际项目开发中,采用同一套骨骼动作并且通过换装(Skin)蒙皮方案来制作不同的角色是一种非常常用的解决方案(注1),这决定了可以对资源进行更颗粒度更小的拆分并优化不同的加载策略。
【注1】通用骨骼方案:多个角色的骨骼动画共用一套骨骼,例如同一职业(例射手)的多个卡牌角色;通过不同的纹理蒙皮来制作出不同的形象,这样可以复用大量的基础动作,而把动画师的主要精力着重花在表现角色性格和技能特性的独有动作上。
通过查阅 spine-unity sdk 代码,可以拆解出 SkeletonData 的结构中 最重要的数据为:

  • 骨骼动画 animations相关数据
  • 蒙皮 skin 相关数据
而通用骨骼解决方案在 unity 对骨骼动画的调用生命周期中往往对 animations 的修改很少,但对蒙皮的换装修改调用非常频繁。基于这样的使用场景用例,对 spine-unity 的底层代码进行修改,在类 SkeletonDataAsset 的方法 ReadSkeletonData 中对同一场 景中需要反复使用的 SkeletonData 数据增加缓存池,避免对同一个动画资源文件进行反复的解析。



图8 增加SkeletonData缓存池


通过缓存池,短时间内加载多个角色spine 动画时只需要花费少量耗时读取 skin 数据即可,而直接引用或者是复制缓 存池中的必要数据可把后续加载降低到 20∼30ms 级别



图9 加载多个角色动画资源耗时(缓存后)




3】对解析耗时开销最大的 Animations 数据解析进行按需加载

在通过二进制和缓存池两步优化后,对游戏中的动画资源加载已经有了质的提升,但在实际运行环境中仍然存在问题。前边提到过在《星之交响》项目中,动画方案采用了通用骨骼+蒙皮换装方案,随着不断新增角色进入到通用骨骼中,spine 文件会变得越来越大,尤其是 Animations 会不断增加。在spine数据的加载过程中,Animations数据块的解析占据了整个解析过程中的绝大部分耗时。



图10 越来越多的角色技能动画



图11 角色spine资源解析总共169ms,其中156ms为解析animations

典型的卡牌游戏模型需要达到 4∼500 张角色卡、每个角色有 2∼3 个独有技动:游戏长期开发和长期运营后 spine 文件中可能包含上千个 Animation,这样动画资源的加载就会变成一个大难题。为了避免这 种情况的发生,我们当然可以考虑在资源制作和导出环节进行拆分,那不在本篇的讨论范畴。由于在场景中会登场的角色是有限的,所以可以根据登场的角色技能所需要的动画来进行选择性加载。随需加载有两 种策略可以选择:

  • 策略 1: 在首次调用/播放该动画时加载(更细颗粒度的切片);
  • 策略 2: 在进入场景前通过会登场的角色进行批量预加载;
《星之交响》是一款融合了策略/act 和节奏操作的游戏,而节奏操作玩法对于操作的实时反馈有很高的要求,若在场景中短时间内(比如在同一个节拍)有多个角色需要进行技能播放,采用策略 1 有可能会造成卡帧 (短时间内解析多个复杂动画数据)。另外,采取策略 1 方案为了减小解析延迟需要在内存中保留 spine 资源的原始数据,而大量的动画数据会对运行时内存造成负担。所以根据具体问题具体分析,在进入场景前根 据登场角色对动画进行筛选,并在类 SkeletonBinary 中新增 ReadSkeletonData 方法,把需要解析的动画名 作为参数传入并在加载时进行过滤,到这里对于 spine 加载的逻辑部分优化已经基本完成。

4】优化文件的读取方式


完成了逻辑部分优化以后,spine 加载在 cpu 的耗时方面已经降低到了一个比较理想的水准。我们开始关注 spine 内存加载方面的问题。首先,从 json(文本)调整为 binary(二进制),对于资源文件的内存占用已经有了不小的提高(文件占用空间); 其次就是对于 Animations 数据的过滤也使 SkeletonData 的内存使用更加节制。值得注意的是,在 spine-unity 官方提供的 SkeletonDataAssetInspector中,资源文件是通过TextAsset控件来进行配置和载入资源的。Unity原生提供的TextAsset 控件导入资源的局限性在于: 在调用资源时,TextAsset 会读取整个文件的所有内容进入 runtime 内存



图12 SkeletonDataAssetInspector中使用TextAsset配置资源



图13 binary读取内存占用4.1MB,与binary文件大小对应

前文中提到,采用了通用骨骼制作动画资源的方案中,随着 Animation 的增加,资源文件会变得越来 越大。读取整个资源文件会对运行时内存造成不必要的浪费,并且在需要加载多个spine文件时会极大增加 GC 操作的压力。查看 SkeletonDataAsset 代码,不难发现方法 SkeletonBinary.ReadSkeletonData 的调用传参是 MemoryStream,我们可以通过定制修改Unity-Spine的Editor Inspector代码,传入文件路径或者文件名,并编写完善的文件加载逻辑,同时修改 ReadSkeletonData 的调用参数为 FileStream类型,就可以对资源进行文件流读取,避免对整个文件同时载入后再做数据组织造成的内存浪费。

【总结】


经过上边的 4 点优化以后,同场景下的 Start()方法的在 PlayerLoop 中的计算耗时从 300ms 级降低到了 80ms 级,I/O 读取造成的阻塞也通过异步并行获得了的改善,内存占用从峰值 2.35GB 降低到 1.9GB,把场 景间的跳转降低到 2 秒左右,从技术角度改进了游戏体验。至此,对 unity 的 spine 资源调用告一段落,可 以胜任大型游戏项目尤其是有大量单位同场景下 spine 资源的调用,其中涉及到的一些优化策略可以根据实 际项目的具体问题灵活调整。



【注】本文所使用的开发环境为

  • spine 4.0.39
  • unity 2020.3 or later

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-3-29 17:22 | 显示全部楼层
写的不错,点赞评论支持一~~波[赞同][赞同]
发表于 2023-3-29 17:27 | 显示全部楼层
写的不错,点赞评论支持一~~波[赞同][赞同]]
发表于 2023-3-29 17:30 | 显示全部楼层
不错
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 21:40 , Processed in 0.112495 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表