有什么不明白的地方,扫描右方二维码加我微信交流。
       

提示:以下修改基于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动画数据耗时导致主线程阻塞。

解决方案:

  1. Spine动画导出使用二进制格式(.skel),在官方文档中有解释,二进制格式解析要比json快,点击查看文档。亲测,此方案可使效率提升80%-95%。如果动画过于复杂,此种方案依然未达到要求,则可以尝试以下方案。
  2. 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 = Addressables.LoadAssetAsync<Material>("spineTestMat");
var data = Addressables.LoadAssetAsync<SkeletonDataAsset>("spineTestData");

while (!mat.IsDone || !data.IsDone)
{
    await Task.Delay(1);
}

if (data.Status == AsyncOperationStatus.Succeeded)
{
    spineTestSkeletonExt.skeletonDataAsset = data.Result;
}
        
if (mat.Status == AsyncOperationStatus.Succeeded)
{
    spineTestSkeletonExt.material = mat.Result;
}

await spineTestSkeletonExt.InitializeAsync();

spineTestGameObject.SetActive(true);
spineTestSkeletonExt.AnimationState.SetAnimation(0, "daiji1", true);//此处改为自己的动画名称就好

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注