在Unity中使用异步线程创建Spine动画
有什么不明白的地方,扫描右方二维码加我微信交流。
提示:以下修改基于Unity2019.4.x,spine-unity3.6,如有版本不同,根据自己的理解自行修改,只要原理懂了,改起来非常容易。配合github的SpineTest示例食用更佳,点击获取。
问题的出现:这几天在优化游戏的启动速度(这里的启动速度说的从点击游戏Icon到用户可以开始操作),发现有一个比较复杂的Spine动画在加载和创建时比较耗时,故将其加载和实例化移到用户可以开始操作后再进行。这样游戏的启动时间大大减少了。Spine动画的加载使用了Addressable的异步加载,但在实例化时会阻塞主线程1-2s,导致UI卡住,用户此时也无法操作。这是无法容忍的(如果游戏设计不在乎这个时间,则放在用户可操作之前;或者游戏有loading界面,则可以放到loading的程中)。
分析:究竟是什么阻塞了主线程?通过在手机上进行测试(在Editor里测不出来,因为通过断点验证,Editor在非PlayMode时就已经把Spine动画数据解析好了,Spine的这种做法有点坑),查看Spine组件的相关代码,发现添加了Spine动画组件的预制体在实例化时会去解析Spine动画数据,于是我将预制体实例化和Spine动画组件的创建分开,发现耗时都在Spine动画组件的创建过程。
问题定位:官方的Spine动画创建是同步的,解析复杂Spine动画数据耗时导致主线程阻塞。
解决方案:
- Spine动画导出使用二进制格式(.skel),在官方文档中有解释,二进制格式解析要比json快,点击查看文档。亲测,此方案可使效率提升80%-95%。如果动画过于复杂,此种方案依然未达到要求,则可以尝试以下方案。
- Spine动画组件不直接绑定在预制体上,而是使用代码创建。同时我们需要写一个Spine动画组件的扩展,使Spine动画组件可以异步线程创建。
下面着重讲下方案2。
Unity和Object相关的API无法使用多线程,而在Spine动画Initialize过程中又有相关的API调用,所以在Initialize过程中需要代码可以在主线程和异步线程间自由切换。
- 首先我们写一个类,MainThread,此类实现在异步线程中把任务抛入主线程,当主线程将任务执行完毕时,再切回异步线程,代码见github。主要使用ManualResetEventSlim类的Wait和Set方法来实现异步线程和主线程同步;
- 然后我们写一个SkeletonGraphicExt扩展类,继承SkeletonGraphic类,并添加一个异步创建方法,InitializeAsync,将SkeletonGraphic类里的Initialize方法实现拷贝到InitializeAsync,代码见github;
- 最后,我们把InitializeAsync过程中使用到UnityAPI的部分代码使用MainThread抛到主线程处理。(主要的修改部份都在SkeletonDataAsset类里)
对以上修改如果有疑问,可以查看github中的示例,在项目中添加一个Spine动画资源,再在AddressableGroup中添加如下两个资源,就可把Scene_SpineAsync跑起来。
InitializeAsync方法使用
代码如下:
创建SkeletonGraphicExt组件,然后使用Addressable异步加载材质和动画数据,给SkeletonGraphicExt组件的skeletonDataAsset和material属性赋值,最后调用InitializeAsync即可完成Spine动画的手动创建。代码github地址。
var spineTestGameObject = transform.Find("spineTest").gameObject; spineTestGameObject.SetActive(false); var spineTestSkeletonExt = spineTestGameObject.AddComponent<SkeletonGraphicExt>(); var mat = await Addressables.LoadAssetAsync<Material>("spineTestMat").Task; var data = await Addressables.LoadAssetAsync<SkeletonDataAsset>("spineTestData").Task; spineTestSkeletonExt.skeletonDataAsset = data; spineTestSkeletonExt.material = mat; await spineTestSkeletonExt.InitializeAsync(); spineTestGameObject.SetActive(true); spineTestSkeletonExt.AnimationState.SetAnimation(0, "daiji1", true);//此处改为自己的动画名称就好