许可证

在购买Spine之前,个人或公司内部可以使用运行库进行评估测试。如果您希望将Spine运行库集成到您的程序中、或者在您发布的程序包中包含Spine运行库、或者修改Spine运行库,您需要一个有效的Spine license。更多的信息您可以查看Spine Runtimes Software License。 在购买Spine之后,可以在Spine Software License的第二段中详细了解Spine运行库许可证的授权方式。Spine运行库主要职责是操作从Spine中导出的数据,以及在许可证授权的范围内进行创建或者扩展。

开始步骤

安装

  1. 下载和安装Unity (PERSONAL版本是个人免费版.)。
  2. 创建一个空的Unity项目。
  3. 下载最新版本的Spine运行库:spine-unity.unitypackage
  4. 导入spine-unity.unitypackage(你可以双击也可以将它拖进Unity中)。
  5. 在项目中打开Examples\Getting Started文件夹,然后打开并运行Unity Scene文件,您可以在scene中看到一些提示信息,还可以查看Inspector和示例脚本的内容。

在Unity中,spine-unity运行库可以加载、操作和渲染Spine骨骼动画。

在Unity中使用spine-unity可能跟别的插件结合使用。比如使用2D Toolkit的图集系统,您可以在Unity中点击Edit->Preferences->Spine,然后点击Enable。

如果您不熟悉C#编程和Unity的使用方式,我们建议您先查阅Unity官方教程。然后从Interface & EssentialsScripting这两个主题开始。但是Unity的Animation主题并不能直接适用于spine-unity,请直接学习spine-unity。

spine-unity是在(spine-csharp)基础上构建的。

示例代码

导入最新版unitypackage之后,进入Examples\Getting Started目录。

打开并运行Unity Scene文件。 您可以在scene中看到一些提示信息,还可以查看Inspector和示例脚本的内容。

这些信息将介绍基本的姿势和角色动画。

您可以访问spine-unity论坛查阅更多信息。

将Spine资源导入你的项目

从Spine导出

  1. 在创建骨架和动画之后,点击Spine菜单 > 导出(CTRL+E)。这会打开导出窗口
  2. 在导出窗口的左边选择JSON
  3. 查看创建图集复选框。(推荐初学者查看非必要数据优质打印)。
    1. 创建图集复选框旁边点击设置。然后会打开纹理打包器设置窗口。
    2. 在窗口的右下角可以看到图集扩展名标签,你应该将文本框中的.atlas设置为.atlas.txt。(如果不这么做可能会出现一些问题,因为Unity默认不会识别以.atlas后缀的文件,虽然spine-unity可以识别这个文件。不管怎么样,设置为.atlas.txt将避免大部分的问题)。
    3. 现在你可以关闭纹理打包器设置窗口了,点击确定关闭。
  4. 导出窗口中,选择一个输出文件夹。(建议:创建一个空的文件夹,并且确定你可以找到它)。
  5. 点击导出
  6. 现在会导出三个文件:

    • .json文件,它包含所有骨架信息。
    • .png文件,它包含当前版本所有图片的集合。
    • .atlas.txt文件,它包含打包的图集信息。

      对于2D Toolkit用户,第3步(打包.png.atlas.txt)不是必须的。相反,你应该适当得在你的SkeletonDataAsset字段中添加一个tk2dSpriteCollectionData引用。然后在Unity中点击`Edit->Preferences->Spine面板中点击Enable启用对TK2D的支持。

导入Unity

  1. 确保已经打开你的Unity项目

    • 项目中应该已经有spine-unity运行库。

  2. 在文件夹中找到刚才导出的3个文件。(json, .atlas.txt and .png)
  3. 将3个文件(或者包含它们的文件夹)拖进Unity的Project面板

    • spine-unity运行库会根据这些文件自动生成必要的Unity资源。
    • 然后你会看到3个新文件。
      • _Material资源包含一个着色器引用和.png纹理。
      • _Atlas资源包含一个材质引用和.atlas.txt
      • _SkeletonData资源包含一个json引用和_Atlas资源。

  4. 右键点击_SkeletonData资源然后选择Spine > Instantiate (SkeletonAnimation),实例化一个Spine游戏对象。

    • 可以通过查看Examples\Getting Started中的示例学习更多关于Spine游戏对象的知识。

  • 资源配置手册 后期,你可以自己创建者3个文件。这种方式可以关注第3个步骤。
  • 替换着色器 使用spine-unity默认的着色器(Spine/Skeleton 或者 Spine/SkeletonLit)会使用Premultiplied Alpha的方式处理材质。这是Spine纹理打包器默认的设置。如果选择其他的着色器可能会产生意想不到的效果,你需要重新导出不使用Premultiplied Alpha的材质。您可以在Premultiply Alpha主题中获得更多信息。
  • 纹理大小 Unity默认会缩放过于庞大的图片。spine-unity运行库自动设置图集的最大大小为2048x2048。如果你的纹理比2048x2048还要大,这将导致图集坐标是错误的。所以请确保合理的导入设置,或者在Spine纹理打包器中减小最大页面的宽度和高度。
  • 纹理压缩伪像 Unity2D项目默认将导入图片的纹理格式设置为"Sprite",当使用Spine/Skeleton着色器时会导致伪像。为了避免伪像,请确保纹理类型设置为"Texture"。spine-unity在自动导入时会尝试应用这些设置,但是在这个过程中会更新你的纹理,这些设置可能会恢复。

更新你项目中的spine-unity运行库

一些Spine编辑器更新时会要求你更新spine-unity运行库,所以它会读取和解析导出Spine数据的正确性。

  • 比如Unity的更新,我们总是建议您在更新之前备份整个Unity项目。
  • 总是在更新spine-unity运行库之前与你的主程和美工一起检查。spine-unity运行库可以基于不同的项目需求而修改源代码。在你的项目中可能spine-unity运行库已经被程序修改过了。基于这种情况,当更新spine-unity运行库之后,还需要重新修改一遍。
  • 更新spine-unity运行库,你有3个选择。便捷更新的方法是,在Unity中点击Assets->Improt Package->Custom Package的方式导入。另一种比较繁琐的方式是,你可以删除旧的版本,然后在导入新的版本。

便捷更新 (.unitypackage)

  1. 下载最新版本的spine-unity.unitypackage
  2. 双击或者拖动unitypackage导入你的Unity项目中。
  3. 不管你怎么移动之前导入的运行库,在本次导入时,对话框都将显示哪些文件将会被更新。

  • 如果你的源文件损坏或者替换这个方式可能不能正确工作。对于这种情况,在导入unitypackage之前你可能需要先删除旧版本。
  • 许多unitypackage旧版本的spine-c#源文件都是不一样的。你或许应该在更新时删除这些旧的spine-csharp文件夹。否则它会工作不正常。
  • 偶尔,一些文件会被删除或者合并。但是在导入unitypackage时并不会删除这些文件。你可以查看公告

手动 (.unitypackage)

  1. 打开一个空的scene. (为保险起见)
  2. 在你的Unity项目中删除之前的spine-unity和spine-csharp运行库。
  3. 导入unitypackage

手动 (zip from github)

  1. 打开一个空的scene. (为保险起见)
  2. 从Github下载zip格式的Spine运行库
    1. 在你找得到的地方解压文件。(你只需要spine-csharpspine-unity)。
  3. 找到解压文件存放并且在你的项目中替换它们。

    • 你需要以下这些文件夹:
      • spine-csharp/src (在unitypackage中它叫做"spine-csharp")
      • spine-unity/Assets/Gizmos.
      • spine-unity/Assets/spine-unity
      • spine-unity/Assets/Examples(可选)

  • Gizmos在Unity中是一个特殊文件。它只有放在assets根目录才能发挥作用。(例如:Assets/Gizmos)
  • spine-csharpspine-unity可以放在任何文件夹内。
  • 有时候,你会复制许多文件。如果你这么做Unity会发出警告,请确保你复制以后删除旧的文件。
  • 一些文件可能已经被移除或者合并。请拷贝整个文件夹并且不要删除他们。然后查看关于这方面的公告


整体结构

spine-unity是怎么组合到一起的? spine-unity的功能由以下几个组件组成:

  • spine-csharp
  • spine-unity
  • ... Unity.

spine-csharp

spine-csharp是Spine运行库使用C#接口的公共核心。

你可以在Spine编辑器中看到这个公共核心包含的类型和数据结构的代码,其中包括Skeleton, Bone, Slot, Skin, Attachment, Animation。你可以在Spine官方文档中查看这些类型的描述。

它会将JSON(.json)和二进制(在Unity中显示为.skel.bytes)的骨架数据反序列化然后写进SkeletonData对象模型中。

它包含了基本的AnimationState实现、Spine的基础、以及直接可以使用的动画控制。(更多内容请往下看)

所有的类都是在Spine命名空间下。

spine-csharp可以让spine-unity, spine-xna and spine-monogame — 这些C#运行库共同使用的。因为spine-csharp没有使用UnityEngine的类功能,所以其它适用.NET/mono编写的Spine运行库都可以在spine-csharp基础上扩展。

为了保持老版Unity的Mono运行库和编译器,spine-csharp没有使用C#语言的最新功能。

有状态(Stateful) vs 无状态(Stateless)

Spine的核心类(定义在spine-csharp中)主要分为stateful-instancestateless-data这两种:

类似SkeletonBoneSlotAnimationState这些类都是有状态的对象,对它们进行修改不会影响到其它实例。你可以翻转Skeleton、复位Bone、修改Slot或者改变AnimationState的时间轴,并且它只适用于单例。

类似SkeletonData, Animation, Attachment, BoneData, SlotData这些类都是无状态的对象,设计初衷就是让他们可以被所有Skeleton实例交叉访问(比如每个Spine GameObject可以创建一个骨架)。通常,你不应该去改动这些无状态的对象。

经验之谈:

  • 如果想得到有状态的对象,你可以使用Skeleton。(比如:设置骨架的姿势或者替换一个槽的附件)
  • 如果想得到无状态的对象,你可以使用SkeletonData。(比如:想要获得一些信息,例如Animation的持续性、Event、或者Setup/Bind姿势)

这只是最简单的设计方案。如果你是一个高级用户或者你知道运行库是如何运行的,那么你可以根据项目需要去自定义一些"无状态"的对象。

你可以在这里找到更多关于Spine核心类的信息。

spine-csharp和spine-libgdx的区别

spine-csharp主要使用C#语言,而spine-libgdx使用的是Java语言,然而因为C#和Java语言的语法和传统都非常相似,这里主要说一下除了语言之外的区别。

在spine-csharp中:

  • spine-csharp引用的核心库是mscorlib。它包含DictionaryArray
  • 在C#中方法和枚举变量使用Pascal命名法代替camel命名法。(由于序列化兼容性的原因,一些枚举使用camel命名法)
  • 属性和自动属性代替Java中的getter和setter方法。这纯粹是语法和API的原因。在引擎中,属性会被MonoJIT编译器编译为方法,有时也会是内联的。
  • 事件的定义和订阅可以使用C#的事件和代理。

出于对性能的考虑,spine-csharp在经常使用的代码里用ExposedList(T)替代System.Collections.Generic.List(T)

spine-unity

spine-unity层是以spine-csharp为基础的,所以它被允许工作在UnityEngine下,还会在Unity编辑器中使用inspectors和windows。

它包含Spine MonoBehaviours定义和资源文件,就像别的编辑器工具一样,为用户自动处理资源,其代码工具就像是PropertyAttributes

spine-unity大部分的周期管理方法都依靠spine-csharp遵循的UnityEngine的生命周期和游戏循环。 但是它也包含主要的渲染代码(在.LateUpdate中)将Spine骨架转化为可见网格。

spine-unity也在编辑器和运行时管理spine-csharp类的反序列化和实例化。

在编写脚本的时候,请注意:所有的spine-csharp都应该在Awake中实例化。这意味着不能随意控制脚本执行顺序,不然不能保证当Awake被调用时已经实例化骨架和动画状态。

你应该在Start之后访问(或缓存).state.skeleton引用。请查看Unity关于Awake的文档.

SkeletonUtility和spine-unity Modules

spine-unity还包括一些可选的功能类和工作流

SkeletonAnimator允许用户使用Mecanim动画控制器控制动画。 SkeletonUtility允许Spine.BonesUnityEngine.Transform之间进行转换。 这可以让你控制UnityEngine.Transform驱动Bone,或者让GameObject随着Bone运动。

spine-unity\Modules\文件夹中还包括其它几个模块,它们都是直接可以用的,虽然都是一些简单得实现,但是只要稍加研究或修改就可以得到更高级得功能。

您可以在项目中任性得删除一些没有使用的模块,不用担心影响Spine的其他功能。注意:一些模块可能依赖其它的模块,所以在删除的时候应该一个一个得删。如果出现依赖问题,Unity会在编译之前提示警告。

控制动画

SkeletonAnimation

一般来说,Spine.AnimationState 管理时间轨道、更新骨架、队列、分层以及混合/交叉混合动画。如果你使用过"Mecanim",Spine.AnimationState更简单,虽然属于不可编程类,但是它足够灵活,可以作为大部分动画的基本逻辑。

但是Spine.AnimationState是一个C#类。要在Unity使用它,有一个包含MonoBehaviourSpine.AnimationState对象叫做SkeletonAnimation。你可以在SkeletonAnimation里面找到一个叫做state的成员变量,它是Spine.AnimationState对象的引用。

SkeletonAnimation既管理更新时间,也生成Mesh对象,因为它是SkeletonRenderer的派生类。你可以把骨架数据资源实例化成"Spine GameObject"然后挂到GameObject上。你可以称SkeletonAnimation为Spine组件。

怎么使用SkeletonAnimation

对初学者来说

最开始,可以使用SkeletonAnimation组件实例化一个GameObject。然后可以在Inspector中的找到SkeletonAnimation组件,然后点击Animation Name下拉框选择一个初始动画,当Scene启动时会直接运行这个动画。

可以设置Loop(循环播放),也是可以调整TimeScale(播放速度)。那些在Inspector属性会映射到.AnimationName, .loop and .timeScale

刚开始写代码的话,你可以给skeletonAnimation.AnimationName属性赋值动画的名字来切换动画。你也可以给skeletonAnimation.loop属性赋值为True,这样在游戏运行时你的动画会重复播放。

skeletonAnimation.timeScale = 1.5f;
skeletonAnimation.loop = true;
skeletonAnimation.AnimationName = "run";

左右方向动画

不比在Spine编辑对话框中设置动画的朝向(向左、向右)。SkeletonAnimationSkeleton对象有FlipX(还有FlipY)的属性,你可以根据需要设置骨骼的水平镜像。

// 如果你的角色已经是面向右边的,那么这样设置后就会面向左边。
skeletonAnimation.skeleton.FlipX = true;

然而,如果你的角色在设计时两边朝向的动画是不一样的,你可以使用Spine的皮肤功能,一个皮肤表示向左,而另一个向右,然后再设置一个变量根据不同的朝向切换皮肤。

bool facingLeft = (facing != "right");
skeletonAnimation.skeleton.FlipX = facingLeft;
skeletonAnimation.skeleton.SetSkin(facingLeft ? "leftSkin" : "rightSkin");

一个典型的应用

控制骨架的主要类是Spine.AnimationStateSkeletonAnimation是基于AnimationState构建的,它的SkeletonAnimation.state引用就是Spine.AnimationState。它会在Awake中初始化,所以应该在Start或之后访问它。

AnimationState是Spine动画状态机的基础实现。它的底层在某种层面来说只能管理:更新、队列、分层和混合/交叉混合动画(它不是Mecanim意义上的"State Machine")。

在代码中,你可以使用AnimationState的方法来播放动画:

// 在Trank0中播放“stand”动画。
skeletonAnimation.state.SetAnimation(0, “stand”, false);

// 在Track0中添加一个“run”动画,当Track0的最后一个动画播放结束后循环播放“run”动画。
skeletonAnimation.state.AddAnimation(0, “run”, true, 0f);

你可以在SkeletonDataAsset的Inspector中找到混合/淡入淡出选项。你可以在"Default Mix"中调整同一个Track中两个动画切换时的平滑程度。你也可以在这里添加指定动画之间的平滑过度程度。

通道(Track)

Track是把动画分层,让角色在同一时间可以播放几个Spine动画。

这在任何情况下都是有用的,比如,你的角色正在跑动,但是还想在跑动的时候拿枪进行射击。

如果你在Track 0播放一个动感,然后在Track 1中播放另一个动画,这两个动画在同时播放时可以根据各自的需要去控制结束点和是否循环。

在动画播放过程中,高层级的通道会覆盖低层级的Track:越大的通道数字就拥有越高的优先级。

// 跑步动画运行在Track 0,而射击动画运行在Track 1
skeletonAnimation.state.SetAnimation(0, "run", true);
skeletonAnimation.state.SetAnimation(1, "shoot", false);

通道实体(TrackEntry)

调用SetAnimation 或者AddAnimation这两个方法后,它们会返回一个TrackEntry对象。

该对象代表一个正在播放或者排队的动画实例。它包含了动画的一些信息:已播放时间、播放速度和其它一些属性。

TrackEntry可以让你在SetAnimationAddAnimation之外控制正在播放或排队动画的播放参数。

如果你不选择保留SetAnimation和AddAnimation返回的TrackEntry对象,你可以这样获得有效的TrackEntry对象。

   var trackEntry = skeletonAnimation.state.GetCurrent(myTrackNumber); // 返回null,就表示没有动画在运行
当前时间 (开始时间)

你可以从动画的任意时间点开始播放。

例如,你可以在SetAnimation之后设置.Time的值,让动画从指定的帧数开始播放。但是要注意.Time的单位是秒。因为Spine的摄影表每秒30帧,所以你需要把帧转换为秒,公式是time = (帧数/30f)

    // 从第10帧开始播放"dance"动画
    var trackEntry = skeletonAnimation.state.SetAnimation(0, "dance", false);
    trackEntry.Time = 10f/30f;

    // 你可以像这样更方便得设置Time,而不需要使用另一个变量去储存TrackEntry
    skeletonAnimation.state.SetAnimation(0, "dance", false).Time = 10f/30f;

如果你这么做事为了动画事件,请确保lastTime.Time设置了一样的值。 如果lastTime为0, 在Time0和.Time之间的所有事件都将被捕获,并在下一个Update中增加/减少。

你也可以设置.EndTime改变动画的结束时间点。

播放速度(TimeScale)

可以设置TrackEntry.TimeScale去改变播放速度。还可以从SkeletonAnimation.timeScaleAnimationState.timeScale中获得最后修改的播放速度。

你可以将timeScale设置为0来暂停播放。要知道,即使你将timeScale = 0来暂停骨骼的运动,但是每一帧的骨骼动画仍然存在,同时你对骨骼得任何更改都将会覆盖更新。

这个简单的方法可以帮助你理解怎么跳转到指定时间点:

static public Spine.TrackEntry JumpToTime (SkeletonAnimation skeletonAnimation, int trackNumber, float time, bool skipEvents, bool stop) {
    if (skeletonAnimation == null) return null;
    return JumpToTime(skeletonAnimation.state.GetCurrent(trackNumber), time, skipEvents, stop);
}

static public Spine.TrackEntry JumpToTime (Spine.TrackEntry trackEntry, float time, bool skipEvents, bool stop) {
   if (trackEntry != null) {
      trackEntry.time = time;
      if (skipEvents)
         trackEntry.lastTime = time; // 在3.0中,这也会忽略附件关键帧。

      if (stop)
         trackEntry.timeScale = 0;
   }
   return trackEntry;
}
TrackEntry-特定事件.

你可以为特定动画实例订阅事件。更多信息可以查看事件文档

Spine.Animation

Spine.Animation对象对应于Spine中的一个单独的"动画片段"。如果在Spine中跑步动画命名为"run",你会得到一个叫做"run"的Spine.Animation对象。

每个Spine.AnimationTimeline对象的集合。每个Timeline对象是关键帧的集合,它定义了某个动画的值(缩放、旋转、位置、颜色、网格、IK、事件、绘制顺序)是怎么随时间改变的。每个Timeline都有它自己的目的:Bone的缩放、旋转和位置; slot的附件和颜色; MeshAttachment的自由变形(FFD)/网格顶点、还有骨架各个部分的绘制顺序事件

在Spine运行库中,使用Animation.Apply(...)Spine.Animation应用在一个骨架上。这只能根据该骨架各个部分的Timeline和关键帧来设置姿势。如果Timeline不存在,则原动画值不会改变。如果Timeline存在,它会覆盖之前的值。

这个就是说,如果你使用Spine.AnimationState的Track机制同时播放两个动画,高通道的Timeline会覆盖低通道的Timeline,但是不会改变任何东西。

该系统允许用户组合骨架各个部分的不同动画,使它们相互独立得播放。

按照动画的帧/时间摆姿势

如果你想按照某个动画的时间点去摆姿势,可以直接调用Animation.Apply。spine-unity运行库还有一个叫做Skeleton.PoseWithAnimation的扩展方法,它允许你按照动画的名字去摆姿势。

其中一个参数始终是时间。该时间使用秒作为单位。如果你想按照Spine中那样摆姿势,你需要在调用Animation.ApplySkeleton.PoseWithAnimation之前调用skeleton.SetToSetupPose()

动画连续性 (pre-3.0)

这也意味着,如果没有自动复位逻辑,动画在连续播放的时候不一定和Spine中保持一致。相反,播放一个动画序列会导致之后的动画会继承之前动画的值和骨骼姿势。

在3.0中,Spine.AnimationState可以让你选择性地使用自动复位逻辑句柄,或者你也可以使用原来的自由模式。

Animation Callbacks + Spine Events

Spine.AnimationState 指定动画回掉的格式请参照C# 事件。你可以在里面找到一些处理动画播放的基础知识。

Spine.AnimationState 支持的事件:

  • Start 当动画开始播放,
  • End 当动画被清除或中断,
  • Complete 当动画完成它全部的持续时间,
  • Event 当检测到用户自定义的事件。

警告:

永远不要在订阅End的方法中调用SetAnimation。因为当一个动画被中断就会触发End,然而SetAnimation会打断所有现有的动画,这会引发无限递归(End->Handle->SetAnimation->End->Handle->SetAnimation),导致Unity没有响应,直到堆栈溢出。

学习更多关于Spine事件和AnimationState回调,请查看事件文档

高级动画控制

Tracks和TrackEntry只是Spine.AnimationState的一部分,在Spine运行库中包括AnimationState是为了访问更快捷。它在大部分情况下可以增加性能。 然而,spine-unity和spine-csharp即使没有AnimationState也可以起作用。一个例子是,SkeletonAnimator使用UnityEngine.Animator代替Spine.AnimationState,然后使用Mecanim去管理混合、分层和队列的数量和数据。

如果你想要用不同的方式统计你的动画,你可以根据打算如何使用它或者根据特殊情况去构建一个不一样的系统。

关于这点,研究Spine运行库官方文档会派得上用场。

渲染

一般来说,Spine的系统利用现代3D游戏引擎的优势。它的高级功能更是如此,比如FFD(自由变形)和权重。

它通过框架和引擎(3D术语)连接; 在术语中有网格和顶点、还有纹理映射和UVs。所以每个图像部分由多个三角形组成的多边形来定义,或者一个矩形由两个三角形组成。这样的引擎用于2D图形,这是不常见的。

基本理念是这样的: 你已经做了三角形网格。这是Spine骨架/模型成型的基础。 你有纹理数据。这个来自纹理图集。 你有着色器程序或者混合指令。

你把这些东西都交给游戏引擎,它再交给GPU,最后由GPU渲染。纹理映射到网格,着色器和混合模式是渲染的基础。

spine-unity使用Unity的MeshRendererMeshFilter组件,还有材质资源进行渲染。这些组件都是Unity用来渲染3D模型的。

SkeletonRenderer类(它的基类是SkeletonAnimation)在它的LateUpdate方法中构建Mesh。它生成顶点数组、顶点颜色、三角形索引和UVs,然后把这些都放进一个UnityEngine.Mesh对象中。 MeshFilter再去获得Mesh,它将被MeshRenderer使用。 MeshRenderer会在适当的时间渲染网格,详情请见Unity’s game loop

MeshFilterMeshRenderer都有Retained ModeAPI(与Immediate Mode API截然相反); 也就是说,网格和材质对于持续可见并不需要每帧传递。这意味着,虽然SkeletonRenderer/SkeletonAnimation可以更新网格的每一帧,但是就算在运行时禁用更新(通过禁用SkeletonAnimationSkeletonRenderer)也不会销毁现有的网格。

高级运用:骨架跳帧 这也意味着,在网格生成后,你可以控制和跳过网格更新去实现特殊的跳帧和逐步更新行为。这可以减少不太重要游戏元素的更新, 或者实现一个看起来像逐帧的动画。

材质

spine-unity也使用材质存储信息,包括纹理、着色器和必要的材质属性。该材质通过AtlasAsset分配。

MeshRenderer的材质数组是由SkeletonRenderer管理的,每一帧都取决于AtlasAssets需要用到什么。直接修改该数组并不是Unity典型的设置方法。

更多的材质

你可能注意到在你的MeshRenderer中有很多材质,特别是,比你实际设置的AtlasAsset还要多。

如果你有一个以上的图集页是不正常的。渲染器的材质数组不能体现AtlasAsset中每一项的顺序和编号。SkeletonRenderer根据那些需要被渲染的Spine附件材质规划了一个材质数组。

如果所有附件共享一个材质,SkeletonRenderer只会把这个材质放进MeshRenderer。

如果一些附件用到了材质A和一些材质B,材质数组会根据材质的需要去排列顺序。这是基于附件绘制的顺序以及哪些附件可以在哪些材质纹理中。

如果是这样的顺序:

Attachment 来自 A

Attachment 来自 A

Attachment 来自 B

Attachment 来自 A

得到的材质数组是这样的:

Material A (第一和第二)

Material B (第三)

Material A (第四)

换句话说,有更多的附件交替来自于A和B的话,就会有更多的材质进入材质数组,在材质数组中的每一项都表示需要转换的材质。

Dragon的例子可以说明这点:

更多的材质在这个数组中,就表示会有更多的draw calls,这会对相同骨架实例化产生不好的影响。如果你只有一个这样的骨架,它可能不是拖慢速度的主要原因。

学习更多关于Spine资源的排列,请看这个页面Spine纹理打包器: 目录结构

为每个实例设置材质属性

同样的,改变MeshRenderer.material的值是没用的。

Renderer.material属性只是渲染器生成的副本,但是它会立即被SkeletonRenderer的渲染代码给覆盖。

另一方面,Renderer.sharedMaterial会修改原始材质。如果你使用这个材质生成更多的Spine游戏对象,对于它的修改应用会对所有的实例进行修改。

在这个例子中,Unity的Renderer.SetPropertyBlock是有用的方法。记住,SkeletonRendererSkeletonAnimation都使用MeshRenderer。设置MeshRenderer的MaterialPropertyBlock允许你改变渲染器的属性值。

MaterialPropertyBlock mpb = new MaterialPropertyBlock();
mpb.SetColor("_FillColor", Color.red); // "_FillColor" 是假设的着色器名字。
GetComponent<MeshRenderer>().SetPropertyBlock(mpb);

优化的注意事项

  • 使用Renderer.SetPropertyBlock允许具有相同材质的渲染器去处理那些由不同的MaterialPropertyBlocks改变的材质属性。
  • 当你在MaterialPropertyBlock中增加或改变一个属性值的时候,你需要调用SetPropertyBlock。但是你可以把MaterialPropertyBlock作为类的一部分,所以每当你想改变属性时,不必总是实例化一个新的
  • 如果你需要频繁设置一个属性,你可以使用静态方法:Shader.PropertyToID(string)去缓存一个整数ID,这个ID可以代替String,使MaterialPropertyBlock的Setter可以使用该ID去设置属性。

精灵纹理的渲染和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的方式渲染。
  • 所有网格的渲染由多种因素确定顺序:

在大多数情况下,你不需要也不应该去碰Material.renderQueue和着色器队列标签。

通过改变材质上的着色器,你可以选择用不同的着色器去获得不同的融合种类。怎么处理混合行为完全取决你。

分层和排序

Sorting LayerSorting Order属性其实是在SkeletonRenderer/SkeletonAnimation的Inspector中,实际上它只是修改了MeshRenderersorting layersorting 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的精灵。

如果你想设置骨架的基本比例的高低,你可以在Skeleton Data Asset的Inspector中改变这个值。当你的SkeletonData被读取时这个值是一个乘数。当SkeletonData被加载并且在运行的时候去改变这个值是没有用的。

如果你想在游戏中动态得改变骨架的视觉比例(像球这种的),你可以设置gameObject.transform.localScale属性的值。

水平翻转 or 垂直翻转

访问Skeleton.FlipXSkeleton.FlipY可以允许骨架的水平翻转和垂直翻转。 这通常是个好主意,这可以让骨架朝着一个方向,所以你可以在代码中控制翻转逻辑。如果没有,你可以随时增加一个额外的bool变量用来控制和取消预期翻转的布尔逻辑.

你可能已经在Unity中学会怎么反转2D精灵,设置Transform的scale为负数,或者沿着Y轴旋转180度。 这两件事纯粹是视觉的目的。但是它们有其副作用,请记住:

  • 不均匀的缩放会导致一个网格绕过Unity的配料系统。这意味着每个实例都会有它自己的draw calls。所以对于你的主要角色这没什么问题。如果你的不均匀缩放骨架的规模为数十个,它就是有害的。
  • 旋转会导致法线随着网格旋转。对于2D精灵的光照,这意味着它们会指向错误的方位。
  • 旋转X或者Y也可能会导致Unity的2D碰撞发生不可预知的结果。
  • 负比例的缩放会导致附加的物理碰撞和一些脚本逻辑发生不可预料的结果。