Unity使用spine动画
在 Unity 中,常常使用 Spine 来制作一些动画,引擎本身并不能直接播放 Spine 动画,需要额外导入一个 RunTime 插件库才能支持。
官网插件导入
当然,也可以到 Spine 官网关于 Unity 插件的下载地址spine-unity-download 直接下载最新的插件包,导入插件核心库的同时,还是导入一些 demo 例子,帮助我们快速上手使用插件的 API ,因此这里我也选择使用第二种方式导入插件。建议新建一个空工程来导入相关 demo 了解插件的使用,而正式工程只导入 spine-csharp 和 spine-unity 两个部分即可。
插件导入步骤
- 在 Unity 菜单中依次点击:Assets->Improt Package->Custom Package ;
- 选中 Spine-Unity 3.6 runtime unitypackage 并导入,去掉 Spine Examples 的勾选(假如需要查看 demo 则点击 All );
- 点击 Import 完成导入,导入完成后在工程 Assets 下会多出一个 Spine 目录,表示导入成功。
Spine 资源导入步骤
通常制作好的 Spine 动画导出时会有三个文件: .png 、.json 和 .atlas :
- 在导入 Unity 之前需要将 .atlas 后缀的文件改为 .atlas.txt 后缀(不修改可能会有问题);
- 将三个文件拖到 Unity 的 Project 面板中,假如运行库导入正常,此时会生成三个新的文件:_Atlas 、_Material 和 _SkeletinData ,并且在 Console 面板中会打印导入成功的日志:
三个文件的作用:
- _Material资源包含一个着色器引用和.png纹理。
- _Atlas资源包含一个材质引用和.atlas.txt 。
- _SkeletonData资源包含一个json引用和_Atlas资源。
在 UGUI 中使用 Spine
在 Hierarchy 面板中,右键 Spine->SkeletonGraphic(UnityUI) :
然后将 Spine 资源导入时生成的 _SkeletinData 文件拖到动画 UI 对象的 Skeleton Data Asset 属性中,且可以编辑相应的动画控制属性:
勾选 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 initializationvoid Start () {sa = sa ?? gameObject.GetComponent<SkeletonAnimation>();PlayAnim("run2");}public void PlayAnim(string animName) {sa.state.SetAnimation(0, animName, false);}}
通过获取其 state 属性,其实是 Spine.AnimationState 对象。
切换动画的bug
由于spine在切换动画的时候自动补偿,用于动画的平稳过度。但是会导致残影等bug,这时候需要在SetAnimation前调用
skeletonAnimation.skeleton.SetToSetupPose ();
spineAnimationState.ClearTracks ();
来消除前一个动画的影响。
左右方向动画
不比在Spine编辑对话框中设置动画的朝向(向左、向右)。SkeletonAnimation的Skeleton对象有FlipX(还有FlipY)的属性,你可以根据需要设置骨骼的水平镜像。
// 如果你的角色已经是面向右边的,那么这样设置后就会面向左边。skeletonAnimation.skeleton.FlipX = true;
然而,如果你的角色在设计时两边朝向的动画是不一样的,你可以使用Spine的皮肤功能,一个皮肤表示向左,而另一个向右,然后再设置一个变量根据不同的朝向切换皮肤。
bool facingLeft = (facing != "right"); skeletonAnimation.skeleton.FlipX = facingLeft;skeletonAnimation.skeleton.SetSkin(facingLeft ? "leftSkin" : "rightSkin");
通道(Track)
Track是把动画分层,让角色在同一时间可以播放几个Spine动画。
这在任何情况下都是有用的,比如,你的角色正在跑动,但是还想在跑动的时候拿枪进行射击。
如果你在Track 0播放一个动感,然后在Track 1中播放另一个动画,这两个动画在同时播放时可以根据各自的需要去控制结束点和是否循环。
在动画播放过程中,高层级的通道会覆盖低层级的Track:越大的通道数字就拥有越高的优先级。
// 跑步动画运行在Track 0,而射击动画运行在Track 1skeletonAnimation.state.SetAnimation(0, "run", true);skeletonAnimation.state.SetAnimation(1, "shoot", false);
-
SkeletonGraphic 用于 Unity 的 UI 中:
using UnityEngine;public class TestSpineAnimCtl : MonoBehaviour {public SkeletonGraphic sgp;// Use this for initializationvoid Start () {sgp = sgp ?? gameObject.GetComponent<SkeletonGraphic>();PlayAnim("run2");}public void PlayAnim(string animName) {sgp.AnimationState.SetAnimation(0, animName, false);}}
通过其 AnimationState 组件来控制动画,其实也是获取一个 Spine.AnimationState 对象。
- Spine.AnimationState 主要需要了解其事件和回调:Spine事件 & AnimationState回调
##添加事件:
skeletonAnimation.state.Start:开始播放
skeletonAnimation.state.End:动画被清除或者中断
skeletonAnimation.state.Interrupt:动画被打断
skeletonAnimation.state.Complete:播放结束
skeletonAnimation.state.Event:用户自定义事件
事件设置采用lambda表达式:
skeletonAnimation.state.Complete += (state, trackIndex,loopCount) => {
Debug.log("");
};
动态获取slot的坐标:
Vector3 pos
精灵纹理的渲染和Z轴次序
Spine一般使用阿尔法混合的方式去渲染你的Spine模型。
从Spine项目中绘制、上色和导出部件的时候,你导出PNG文件的像素编码格式是RGBA(红色、绿色、蓝色和阿尔法值)。在文件中,每个像素在阿尔法通道中有0-255的值,就像RGB通道一样,代表每个像素的透明度有256种层次。
这种透明与半透明在阿尔法通道中定义,展现深度/渲染顺序的经典问题。这些问题有许多不完善的解决方案,并且在我们今天的AAA游戏中也会发生一些奇怪的3D现象。
但是,在非常标准的方式中,也就是说i,像大多数2D阿尔法混合精灵渲染案例,包括Unity自己的Sprite/SpriteRenderer系统,spine-unity使用的渲染器,不使用3Dz-buffer和非阿尔法测试去确定哪些渲染在前,哪些渲染在后。
在一个网格中,它根据网格中三角形的顺序去渲染物体,然后绘制一个东西在另一个东西上面。该顺序是在Spine中控制槽的绘制顺序来决定的。
网格之间,spine-unity使用一些Unity的渲染顺序系统去确定 精灵/网格 应该谁上面。这是使用spine-unity标准配置的典型行为: Between meshes, spine-unity uses many of Unity’s render order systems to determine what sprite/mesh should be on top of which. Using the standard spine-unity setup, here is typical behavior for it:
- 网格数据被当作一个整体传递给Unity。
- 它使用GPU的triangle-by-triangle的方式渲染。
- 所有网格的渲染由多种因素确定顺序:
-
- 摄像机的距离。(摄像机判定哪一种距离:平面还是透视.)
-
- 渲染器Sorting Layer/Sorting Order。(所有的UnityEngine.Renderers都有排序属性.)
-
- 着色器的语言和队列标签(默认为其他精灵的“透明”队列)
-
- Material.renderQueue。(除非你设置它否则它就什么都不做。它只是覆盖shader语言的队列标签)
-
- 摄像机深度. (更多摄像机的设置.)
分层和排序
Sorting Layer和Sorting Order属性其实是在SkeletonRenderer/SkeletonAnimation的Inspector中,实际上它只是修改了MeshRenderer的sorting layer 和 sorting order 属性.
尽管被隐藏在MeshRenderer的Inspector中,这些属性实际上是MeshRendererserialized/stored的一部分,而不是SkeletonRenderer。
通过摄像头距离排序
如果你保持所有的渲染器同样的分层和排序,他们会收到上述其他排序方案的影响。
如果队列标签页一样(如果你使用相同的材质和着色器),那么你应该根据摄像机的距离去控制Spine游戏对象的排序。
别忘了这个透视摄像机的排序模式.
那么可以在我的Spine骨架的部分之间渲染任何东西吗?
有时候,你需要你的角色骑一辆自行车、举起一块石头或者拥抱一根柱子。对Spine而言,这意味着你要显示的东西,有些东西渲染在前面,有些东西渲染在后面。
在Unity中,网格是整体渲染的。那么你怎么知道哪个渲染在前面,哪个渲染在后面?
对于这个问题的答案是:可能将来会改变。
但是对于现在来说,你可以使用Spine-unity的SkeletonUtility方法调用"Submesh Renderers"。它会单独覆盖。
但基本上,它的作用是让你的SkeletonRenderer网格根据槽(上面和下面)去拆分。这些拆分的部分会根据他们自己的GameObject中的MeshRenderers去渲染。因为他们是单独渲染的,你可以分别设置他们的Sorting Order。
我已经调低了骨架的阿尔法值,为什么还会重复显示?
这在任何地方都适用的实时网格渲染。当三角形被绘制时透明度就被应用了,而不是之后。
这里大概有一些解决方案,或者一些变通方案。 去玩一些你最爱的游戏吧!你可以在各种2D游戏中找到一些变通方法的例子。
spine-unity怎么决定我的Spine模型的大小?
Spine使用 1像素:1单位。意思是,如果你只是包含图像在你的骨架中,并且没有任何旋转和缩放,在Spine中该图像的1个像素就对应1个单位高和1个单位宽。
在Unity中,1单位:1米。这是Unity默认的物理值和约束(包括2D和3D)。对于这点,使用1像素:1单位通常不是一个好主意。反而,Unity自己的精灵默认缩放为1/100;意味着100个像素就是1个Unity的单位大小。
为了方便,当你将Spine数据带入Unity的时候,可以将缩放设置为0.01用来匹配Unity的精灵。
效率优化
上面提到从 Spine 导出的文件有三种:.png 、.json 和 .atlas ,但使用 .json 格式读取动画数据是比较慢且运行效率较低的方式。Spine 新版本其实还支持更快的数据导出方式,即 Binary format ,那就是二进制的数据导出,采用这种方式导出的格式是:.png 、.skel 和 .atlas 。
导入前需要将 .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动画组件》 才发现项目中用到了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 initializationvoid Start () {#if UNITY_EDITOR#elseskg = 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 的地方挂上此组件即可解决移动端材质丢失的问题。