先说Unity使用Spine动画,这是从其他地方抄来的:原文链接:

运行库导入

关于运行库的导入,有两种方式:

1. 从开源库导入

下载 Spine 的 runtime 库:EsotericSoftware/spine-runtimes ,只需要将其中的 spine-csharp 和 spine-unity 两个部分导入到 Unity 中即可。

2. 从官网插件导入

当然,也可以到 Spine 官网关于 Unity 插件的下载地址spine-unity-download 直接下载最新的插件包,例如:Spine-Unity 3.6 runtime unitypackage ,然后直接导入工程中。

第二种方式是比较常用而且方便的,因为在导入插件核心库的同时,还是导入一些 demo 例子,帮助我们快速上手使用插件的 API ,因此这里我也选择使用第二种方式导入插件。建议新建一个空工程来导入相关 demo 了解插件的使用,而正式工程只导入 spine-csharp 和 spine-unity 两个部分即可。

插件导入步骤

在 Unity 菜单中依次点击:Assets->Improt Package->Custom Package ;

选中 Spine-Unity 3.6 runtime unitypackage 并导入,去掉 Spine Examples 的勾选(假如需要查看 demo 则点击 All );

unity spine和批_spine

点击 Import 完成导入,导入完成后在工程 Assets 下会多出一个 Spine 目录,表示导入成功。

Spine 资源导入步骤

通常制作好的 Spine 动画导出时会有三个文件: .png 、.json 和 .atlas :

unity spine和批_unity_02

在导入 Unity 之前需要将 .atlas 后缀的文件改为 .atlas.txt 后缀(不修改可能会有问题);

将三个文件拖到 Unity 的 Project 面板中,假如运行库导入正常,此时会生成三个新的文件:_Atlas 、_Material 和 _SkeletinData ,并且在 Console 面板中会打印导入成功的日志:

unity spine和批_unity_03

三个文件的作用:

  • _Material资源包含一个着色器引用和.png纹理。
  • _Atlas资源包含一个材质引用和.atlas.txt 。
  • _SkeletonData资源包含一个json引用和_Atlas资源。

在 UGUI 中使用 Spine

在 Hierarchy 面板中,右键 Spine->SkeletonGraphic(UnityUI) :

unity spine和批_spine_04

然后将 Spine 资源导入时生成的 _SkeletinData 文件拖到动画 UI 对象的 Skeleton Data Asset 属性中,且可以编辑相应的动画控制属性:

unity spine和批_json_05

勾选 Starting Loop 然后运行场景即可看到当前 Starting Animation 所选中动画的效果,并能动态切换其他动画。

程序控制

很多时候,动画需要通过程序控制实现动态创建、切换和销毁,主要会用到几个组件:SkeletonAnimation 、SkeletonGraphic 和 SkeletonRenderer :

  • SkeletonAnimation 动画基本控制组件:
skeletonAnimation.timeScale = 1.5f;
skeletonAnimation.loop = true;
skeletonAnimation.AnimationName = "run";

这里用于设置动画的播放速度,是否循环,还有切换动画,如下脚本,将其挂在带 SkeletonAnimation 或继承自 SkeletonAnimation 的组件的物体上,用于播放一次动画:

using UnityEngine;

public class TestSpineAnimCtl : MonoBehaviour {
    public SkeletonAnimation sa;

    // Use this for initialization
    void Start () {
        sa = sa ?? gameObject.GetComponent<SkeletonAnimation>();
        PlayAnim("run2");
    }

    public void PlayAnim(string animName) {
        sa.state.SetAnimation(0, animName, false);
    }
}
  • 通过获取其 state 属性,其实是 Spine.AnimationState 对象。
  • SkeletonGraphic 用于 Unity 的 UI 中:
using UnityEngine;

public class TestSpineAnimCtl : MonoBehaviour {
    public SkeletonGraphic sgp;

    // Use this for initialization
    void Start () {
        sgp = sgp ?? gameObject.GetComponent<SkeletonGraphic>();
        PlayAnim("run2");
    }

    public void PlayAnim(string animName) {
        sgp.AnimationState.SetAnimation(0, animName, false);
    }
}

通过其 AnimationState 组件来控制动画,其实也是获取一个 Spine.AnimationState 对象。

SkeletonRenderer 还没用到,暂不记录。

Spine.AnimationState 主要需要了解其事件和回调:Spine事件 & AnimationState回调
 

效率优化

上面提到从 Spine 导出的文件有三种:.png 、.json 和 .atlas ,但使用 .json 格式读取动画数据是比较慢且运行效率较低的方式。Spine 新版本其实还支持更快的数据导出方式,即 Binary format ,那就是二进制的数据导出,采用这种方式导出的格式是:.png 、.skel 和 .atlas 。

unity spine和批_json_06

导入前需要将 .skel 后缀改成 .skel.bytes ,将 .atlas 后缀改成 .atlas.txt ,然后再拖入 Unity 中,不然 .skel 文件不会对应生成 Unity 可识别的 .asset 格式的数据文件。
 

插件自带材质

由于打包的时候资源需要打包成 Assetbundle ,有几个插件自带的材质球需要注意一下的:

  • spine-unity\Modules\SkeletonGraphic\Shaders 中的 SkeletonGraphicDefault.mat 和 SkeletonGraphicTintBlack.mat ;
  • spine-unity\Shaders\Utility 中也有 HiddenPass.mat ;
  • spine-unity\Modules\SlotBlendModes 中有 SkeletonPMAMultiply.mat 和 SkeletonPMAScreen.mat

需要动态加载这些材质球的话,需要将这些材质球与其他材质球资源一起打包,否则在手机包会出现材质球丢失。
 

手机包贴图丢失问题

当然,除了材质丢失,还有可能出现贴图丢失的问题,情况如下:

unity spine和批_unity_07

只有几个白色的方块,而正常的应该如下:

unity spine和批_unity spine和批_08

1. 问题分析

现在这就是贴图丢失了,看了 《unity 在代码中创建spine动画组件》 才发现项目中用到了spine 动画,使用 Assetbundle 打包后,在手机上运行会出现丢材质的情况。如果不进行打包,直接放到 Resources 目录下是可以正常加载的,但是,这样包就会很大,而且也不能进行热更新。进过测试,发现在代码中创建 spine 组件是可以解决这个问题。

其实,最根本的问题是 :xxx_Altas.asset 和 xxx_SkeletonData.asset 这两个资源打包到 Assetbundle 之后就取不出来了(偶尔能取出),从而导致了 SkeletonGraphic 的 skeletonDataAsset 属性值变为 null 。

所以,要解决此问题可以干脆用代码动态创建两个对象替换这两个文件的作用即可:
 

// 动态创建一个 spine 动画数据
    public static SkeletonDataAsset BuildSkeletonDataAsset(string skeletonPath, GameObject go) {
        SkeletonDataAsset sda;
        sda = ScriptableObject.CreateInstance<SkeletonDataAsset>();
        sda.fromAnimation = new string[0];
        sda.toAnimation = new string[0];
        sda.duration = new float[0];
        sda.scale = 0.01f;
        sda.defaultMix = 0.2f;
        AtlasAsset[] arrAtlasData = new AtlasAsset[1];
        for (int i = 0; i < arrAtlasData.Length; i++) {
            AtlasAsset atlasdata = ScriptableObject.CreateInstance<AtlasAsset>();
            atlasdata.atlasFile = ResourceManager.Instance.LoadAsset(skeletonPath + ".atlas.txt", typeof(TextAsset), go) as TextAsset;
            atlasdata.materials = new Material[1];
            atlasdata.materials[0] = ResourceManager.Instance.LoadAsset(skeletonPath + "_Material.mat", typeof(Material), go) as Material;
            arrAtlasData[i] = atlasdata;
        }
        sda.atlasAssets = arrAtlasData;
        sda.skeletonJSON = ResourceManager.Instance.LoadAsset(skeletonPath + ".skel.bytes", typeof(TextAsset), go) as TextAsset;
        return sda;
    }

步骤其实很简单,用代码动态创建一个 SkeletonDataAsset 对象,然后赋值给 SkeletonGraphic 中的 skeletonDataAsset 再调整相应的动画参数即可:

using Spine.Unity;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 用于解决 Spine 动画在手机 ab 包资源管理模式下贴图丢失问题
/// </summary>
public class SkeletonGraphicABReseter : MonoBehaviour {
    private SkeletonGraphic skg;
    public string spinePath = "";
    public string animName = "";
    public bool loop = false;
    public float timeScale = 1.0f;

    // Use this for initialization
    void Start () {
#if UNITY_EDITOR

#else
        skg = gameObject.GetComponent<SkeletonGraphic>();
        if(skg == null) {
            return;
        }
        ResetSkData();
#endif
    }

    void ResetSkData() {
        if(skg.SkeletonDataAsset == null && spinePath.Length > 0) {
            Debugger.Log("------------ 重置动画");
            SkeletonDataAsset sda = Utils.BuildSkeletonDataAsset(spinePath, gameObject);

            skg.skeletonDataAsset = sda;
            // 重置动画
            skg.Initialize(true);
            skg.AnimationState.SetAnimation(0, animName, loop);
            skg.AnimationState.TimeScale = timeScale;
        }
    }
}

在使用 SkeletonGraphic 的地方挂上此组件即可解决移动端材质丢失的问题。

需求是FairyGUI需要替换Spine动画,直接上代码:

//item.GetChild("Loader3D_hero").asLoader3D.url = "ui://o3ujglcdfj403a";
        int width = Mathf.FloorToInt(item.GetChild("Loader3D_hero").asLoader3D.width);
        int height = Mathf.FloorToInt(item.GetChild("Loader3D_hero").asLoader3D.height);
        Vector2 vector2 = item.GetChild("Loader3D_hero").asLoader3D.pivot;
        SkeletonDataAsset skeletonDataAsset = SpinModelManager.Instance.GetSkeletonDataAsset("Spine/skeleton", new string[] { "Spine/skeleton_Atlas" }, "aaa");
        item.GetChild("Loader3D_hero").asLoader3D.SetSpine(skeletonDataAsset, width, height, vector2);

注:下面的这种直接赋值URL的是spine在fairyGUI中导出,setSpine()是资源在外面的。

item.GetChild("Loader3D_hero").asLoader3D.url = "ui://o3ujglcdfj403a";

具体实现方法:

/// <summary>
    /// 得到Spin动画的SkeletonDataAsset
    /// </summary>
    /// <param name="jsonPath">json的路径如:skeleton.json</param>
    /// <param name="atlasPaths">图集的路径数组,如:skeleton_Atlas.asset</param>
    /// <param name="name">骨骼的名字</param>
    /// <param name="atlasNum">图集的数量</param>
    /// <param name="scale">spin的大小</param>
    /// <returns></returns>
    public SkeletonDataAsset GetSkeletonDataAsset(string jsonPath,string[] atlasPaths,string name,int atlasNum = 1,float scale = 0.5f)
    {
        SkeletonDataAsset skeletonDataAsset = new SkeletonDataAsset();
        skeletonDataAsset.name = name;
        skeletonDataAsset.fromAnimation = new string[0];
        skeletonDataAsset.toAnimation = new string[0];
        skeletonDataAsset.duration = new float[0];
        skeletonDataAsset.scale = scale;
        //skeletonDataAsset.defaultMix = 0.2f;

        AtlasAssetBase[] atlasAssets = new AtlasAssetBase[atlasNum];
        for (int i = 0; i < atlasPaths.Length; i++)
        {
            atlasAssets[i] = Resources.Load<AtlasAssetBase>(atlasPaths[i]);
        }
        //atlasAssets[0] = Resources.Load<AtlasAssetBase>("Spine/skeleton_Atlas");
        skeletonDataAsset.atlasAssets = atlasAssets;
        //skeletonDataAsset.skeletonJSON = Resources.Load<TextAsset>("Spine/skeleton");
        skeletonDataAsset.skeletonJSON = Resources.Load<TextAsset>(jsonPath);

        return skeletonDataAsset;
    }