重要须知: 本页面描述的是 4.3 及以上版本 的Skeleton组件.
如需了解 4.2 及以下版本的主要组件内容, 请访问 该页面.

主要组件

spine-unity运行时提供了一套用于显示、动画化、跟随和修改从Spine导出的skeleton的组件. 如 资产导入 一节中所述, 这些组件将引用导入的skeleton data和texture atlas资产.

请注意: spine-unity 4.3 运行时引入了一项重大架构变更: 通过使用两个互相独立(而非组件继承)的组件实现了动画与渲染的分离. 这一模块化设计使得此前无法实现的灵活组合成为可能.

将Skeleton添加到场景中

在Unity项目中快速添加Spine skeleton, 只需:

  1. 导入 资产 一节中所述的skeleton data和texture atlas.
  2. _SkeletonData 资产拖入场景(Scene)视图或层级结构(Hierarchy)面板并选择 SkeletonAnimation. 运行时将在场景中实例化一个包含以下组件的新GameObject: SkeletonRenderer 组件用于渲染和管理skeleton, 而 SkeletonAnimation 则用于为其添加动画.

请注意: 步骤2的另一个实现方法是手动创建这个GameObject:

  1. 点击菜单 GameObject -> Create Empty 新建一个空的GameObject.
  2. 选中GameObject并在检查器中点击 Add Component, 然后选择 SkeletonRenderer 便可向GameObject添加 SkeletonRenderer 组件. 运行时会自动为GameObject添加 MeshRendererMeshFilter 组件.
  3. 将所需的 _SkeletonData 资产分配到 SkeletonRenderer 组件的 Skeleton Data Asset 属性上.
  4. 再次选择该 GameObject, 在检查器中点击 Add Component, 然后选择 SkeletonAnimation, 便可添加 SkeletonAnimation 组件.

请注意: 如果在场景视图中只能看见没有附件图片的skeleton骨骼, 你可以在 Initial Skin 属性中选择 default 以外的皮肤.

你现在可以使用组件的C# API来控制skeleton动画, 对动画触发的事件做出反应等等. 更多细节请参考下文.

渲染与动画组件

通过 拖放 方式实例化skeleton并选择 SkeletonAnimation, 是在 Unity 中使用 Spine skeleton的推荐方式. 此操作会创建一个包含 SkeletonAnimation 组件(用于动画)和 SkeletonRenderer 组件(用于渲染)的 GameObject, 二者共同实现完整 Spine 动画功能.

但在某些情况下无法使用 SkeletonRenderer, 例如在 UI 画布内进行渲染或需要使用第三方资源的 Mecanim 动画系统时. 此时, 可用的渲染与动画组件为:

渲染组件

  1. SkeletonRenderer - 使用标准的 MeshRenderer, 可像 Unity 的精灵(Sprite)一样与 SpriteMask 等遮罩系统交互. 这在 Unity 中渲染 Spine skeleton的推荐方式.
  2. SkeletonGraphic (UI) - 将 skeleton 视作 UI 元素并搭配 Unity Canvas 使用, 可像内置的 Unity UI 元素一样渲染并与 RectMask2D 等 UI 遮罩交互.

动画组件

  1. SkeletonAnimation - 使用 Spine 自有的动画与事件系统, 提供最大的自定义灵活性. 这是在 Unity 中使用 Spine Skeleton 动画的推荐方式.
  2. SkeletonMecanim - 使用 Unity 的 Mecanim 动画与事件系统来开始、mix和过渡动画. 相较于 SkeletonAnimation, 其动画混合与过渡选项更少. 使用 SkeletonMecanim 时, 无法保证动画过渡效果与 Spine 编辑器中的预览完全一致.

拖放 菜单中提供的四种选项, 代表以下的四种组件组合, 也遵循现有命名规范和自动升级机制:

  1. SkeletonAnimation: SkeletonAnimation + SkeletonRenderer. 在 Unity 中使用 Spine Skeleton 的推荐方式.
  2. SkeletonGraphic (UI): SkeletonAnimation + SkeletonGraphic (UI).
  3. SkeletonMecanim: SkeletonMecanim + SkeletonRenderer.
  4. SkeletonGraphic (UI) Mecanim: SkeletonMecanim + SkeletonGraphic (UI).

进阶操作 - 运行中实例化

请注意: 推荐通过正常工作流将skeleton添加到场景并将其存储于prefabs中, 或者从实例池中复用池化对象. 这样做会更容易修改和调整skeleton.

虽然这不是推荐的工作流, 但spine-unity API也可以让运行时用 SkeletonDataAsset 实例化 SkeletonAnimationSkeletonGraphic GameObjects, 甚至直接用导出的三种资产来直接实例化组件. 只有当你无法依赖常规Unity导入流程来自动创建 SkeletonDataAssetSpineAtlasAsset 资产时, 才建议直接通过导出资产来实例化.

C#
// instantiating a SkeletonAnimation GameObject from a SkeletonDataAsset
SkeletonComponents<SkeletonRenderer, SkeletonAnimation> instance = SkeletonAnimation.NewSkeletonAnimationGameObject(skeletonDataAsset);

// instantiating a SkeletonGraphic GameObject from a SkeletonDataAsset
SkeletonComponents<SkeletonGraphic, SkeletonAnimation> instance
= SkeletonGraphic.NewSkeletonGraphicGameObject(skeletonDataAsset, transform, skeletonGraphicMaterial);

// generic instantiation of arbitrary renderer and animation component pair
// SkeletonRenderer.NewSpineGameObject<RendererType, AnimationType>();
var instance = SkeletonRenderer.NewSpineGameObject<SkeletonRenderer, SkeletonMecanim>(skeletonDataAsset);
// SkeletonGraphic.NewSkeletonGraphicGameObject<AnimationType>();
var graphicInstance = SkeletonGraphic.NewSkeletonGraphicGameObject<SkeletonMecanim>(skeletonDataAsset, parent, material);
C#
// instantiation from exported assets without prior import
// 1. Create the AtlasAsset (needs atlas text asset and textures, and materials/shader);
// 2. Create SkeletonDataAsset (needs json or binary asset file, and an AtlasAsset)
SpineAtlasAsset runtimeAtlasAsset
= SpineAtlasAsset.CreateRuntimeInstance(atlasTxt, textures, materialPropertySource, true);
SkeletonDataAsset runtimeSkeletonDataAsset
= SkeletonDataAsset.CreateRuntimeInstance(skeletonJson, runtimeAtlasAsset, true);
// 3. Create SkeletonAnimation (needs a valid SkeletonDataAsset)
SkeletonAnimation instance = SkeletonAnimation.NewSkeletonAnimationGameObject(runtimeSkeletonDataAsset);

查看示例场景 Spine Examples/Other Examples/Instantiate from Script 以及示例脚本 SpawnFromSkeletonDataExample.csRuntimeLoadFromExportsExample.csSpawnSkeletonGraphicExample.cs 以详细了解运行时实例化.

SkeletonRenderer组件

SkeletonRenderer 组件负责绘制并管理骨架的当前状态. 它是两种skeleton渲染组件之一, 另一个是用于 Canvas UI 元素的 SkeletonGraphic (UI) 组件.

渲染通过 MeshRenderer 组件上的程序化网格(procedural mesh)完成. 该组件会使用 SkeletonDataAsset 引用的texture atlas资产, 查找绘制skeleton附件所需的texture和material.

请注意: 大多数情况下还需要添加一个动画组件, 例如 SkeletonAnimationSkeletonMecanim, 用以处理动画控制与动画过渡. 仅当你需要手动应用动画且无需过渡时(例如用于动画化 UI 仪表)才单独使用此组件. 可以参考示例场景 Spine Examples/Other Examples/SpineGauge, 了解如何直接使用 SkeletonRenderer 组件.

设置Skeleton Data

SkeletonRenderer 组件需引用skeleton data资产, 它能够给组件提供skeleton的骨骼层次结构、槽位等信息.

如果你通过 拖放 将skeleton添加到场景中, 会自动将skeleton data资产分配到组件中. 如果Skeleton GameObject已设置好但突然需要将skeleto改为不同的资产, 也可以手动更改检查器的属性.

要设置或改变skeleton data, 需要

  1. 选中 SkeletonRenderer 的GameObject
  2. _SkeletonData 资产分配到检查器中的 SkeletonData Asset 属性中.

SkeletonRenderer参数

SkeletonRenderer 检查器界面上公开暴露了以下参数:

  • Initial Skin. 动画开始时将应用此皮肤. 注意: 如果您只看到skeleton的骨骼而未显示任何图像, 可能需要切换到非 default 的其他皮肤以正确显示角色外观.
  • Initial Flip X, Initial Flip Y. 在动画开始时, 这两个选项分别将水平翻转和垂直翻转skeleton. 翻转后的skeleton中 ScaleXScaleY 值将被置为 -1.

高级参数

展开 SkeletonRenderer 检查器中的 Advanced 面板将展示进阶配置参数.

SkeletonRenderer 检查器界面上公开暴露了以下进阶参数:

  • Update When Invisible. 设置当MeshRenderer不可见时的更新模式. 当网格恢复可见性时, 更新模式会自动重置为 UpdateMode.FullUpdate.
  • Fix Draw Order. 该选项仅适用于使用3个以上子网格 (即渲染顺序交错的多个materials, 例如渲染顺序为 "A B A") 的用例. 若启用该选项, 运行时将在每个material中加入MaterialPropertyBlocks, 以防止LWRP等渲染器对子网格进行过度合批, 过度合批会导致绘制顺序错误 (例如, "A1 B A2" 将变为 "A1 A2 B"). 当绘制正确时, 可以禁用该参数来避免额外的性能开销.
  • Immutable triangles. 附件可见性固定不变时, 则启用该参数可优化skeletons的渲染. 若启用该参数, 那么运行时将不会更新三角形. 如果skeleton不交换或隐藏附件, 或者不使用绘制顺序键(keys), 则应启用此选项来优化渲染. 若不是这样的使用场景, 那么将该参数置为禁用则可能会导致渲染错误.
  • Clear State on Disable. 当禁用该组件或其GameObject时, 将清空渲染状态和skeleton状态. 这可以避免在再次启用它们时还保持着之前的状态. 当把skeleton放进了对象池之后, 启用该设置可能会有所裨益.
  • Separator Slot Names. 设置渲染分割的槽位. SkeletonRenderSeparator 等组件将读取该设置, 以此在多个GameObjects上用各自独立的渲染器渲染skeleto.
  • Use Clipping. 当置为 false 时, 将忽略所有 Spine 剪裁附件(clipping attachments).
  • Z-Spacing. Skeleton渲染器组件在X/Y平面上自底向顶渲染附件图片. 每个附件都可以在Z轴上自定义z-spacing(z轴间距)值, 以避免 z-fighting 现象.
  • Use single submesh. 若你的 skeleton 只使用了单个 Material 且仅包含单个子网格, 则启用该选项可以简化子网格的生成. 该选项将禁用 skeleton 上的多 materials 渲染、分割渲染和自定义 materials 槽位功能.
  • Fix Prefab Override MeshFilter. 该选项能修复了Prefab一直显示已更改的问题 (将 MeshFilter 的隐藏标志位置为 DontSaveInEditor), 但会导致其他组件丢失对 MeshFilter 的引用. 当置为 Use Global Settings 时, 运行时将使用 Spine 首选项 中的设置.
  • Threaded Mesh Generation
    • Use Threading. 启用后, 网格生成将在多个线程上并行执行, 该组件将注册到共享的 SkeletonUpdateSystem 中. 若置为Use Global Settings, 则将采用 Spine 首选项 中的配置.

  • PMA Vertex Colors. 将顶点色的RGB值与顶点色的alpha值相乘. 如果用于渲染的着色器是Spine着色器 (即使用的是 Straight Alpha Texture) 或者是使用了PMA additive blend模式 Blend One OneMinusSrcAlpha 的第三方着色器, 则应启用该参数. 对于使用普通blend模式 Blend SrcAlpha OneMinusSrcAlpha 的普通着色器, 则应禁用此选项. 该参数启用时, 将在同一个绘制调用(draw call)中一起渲染additive槽位和普通槽位. 而禁用该参数时, 需要在additive槽位上启用 SkeletonDataBlend Mode Materials - Apply Additive Material 选项, 不过这样做会产生额外绘制调用, 从而降低性能表现.
  • Tint Black (!). 将black tint顶点数据添加到网格里. 若skeleton中有任一插槽设置了black tint, 则应启用该选项. Black tinting要求着色器将UV2和UV3作为black tint色来实现着色效果. 使用运行时附带的 Spine/Skeleton Tint Black 着色器便能实现. 但若只需tint整个skeleton而非某个特定部件, 则建议你使用 Spine/Skeleton Tint 着色器, 它的效率更高, 且可以通过 MaterialPropertyBlock 改变或动画化 _Black material属性. 更多信息请参见 着色器 一节. 如果在tint多个不同skeletons时希望能进行合批渲染, 建议 用Skeleton.R .G .B .A来tinting各个skeleton.
  • Add Normals. 启用该选项后网格生成器会在输出的网格中添加法线. 如果你的着色器需要顶点法线请启用该选项. 为了更佳性能和更低的内存占用, 建议使用可以接收法线的着色器, 比如 Spine/Skeleton Lit 着色器. 请注意也能将 Spine/Sprite 着色器配置为接收 Fixed Normal.
  • Solve Tangents. 某些光照着色器需要顶点切线(tangents), 通常是应用法线贴图时会用到. 启用该选项后, 运行时每一帧都会计算切线并将其添加到输出网格.
  • Physics Inheritance. 该项控制变换的运动将如何应用于skeleton的PhysicsConstraints.
    • Position. 该选项中的X和Y缩放值将用于加权变换的位移量. 当该值非零时, 将把加权变换位移量应用于skeleton的物理约束. 几种 (X,Y) 的典型值是:
      (1,1) 表示正常应用 XY 移动,
      (2,2) 说明应用了双倍速移动,
      (1,0) 表示仅应用水平移动, 而
      (0,0) 表示完全不影响变换位移.
    • Rotation. 当该值非零时, 将把该值用于加权变换的旋转量, 加权旋转量将应用于skeleton的物理约束. 其几种典型值为:
      1 表示正常旋转,
      2 表示二倍速旋转, 而
      0 代表完全不影响变换旋转.
    • Movement relative to. 设置变换移动的参考变换(如父级变换等), 该选项表示物理约束计算相对移动时应参考的变换. 将此项置为 None 即表示使用绝对世界位置 (默认值).
  • Add Skeleton Utility. 该按钮可以将用于跟随或覆盖骨骼位置的 SkeletonUtility 组件添加到GameObject上. 更多信息请参见 SkeletonUtility 一节.
  • Debug. 在游戏运行时如果希望了解槽位当前的颜色或骨骼的缩放比例, 按下 Debug 按钮可以打开为此而建的 Skeleton Debug 窗口. 你可以在该窗口中查看skeletion当前的骨骼状态、槽位、约束、绘制顺序、事件和skeletion的统计信息.

Skeleton

SkeletonRenderer 组件提供了 SkeletonRenderer.Skeleton 属性以便访问底层的 Skeleton类. Skeleton属性可以设置皮肤、缩放比例和附件, 重置骨骼为setup pose以及翻转整个skeleton.

设置附件

设置附件需要传入槽位和附件的名称.

C#
bool success = skeletonRenderer.Skeleton.SetAttachment("slotName", "attachmentName");
C#
// using properties
[SpineSlot] public string slotProperty = "slotName";
[SpineAttachment] public string attachmentProperty = "attachmentName";
...
bool success = skeletonRenderer.Skeleton.SetAttachment(slotProperty, attachmentProperty);

这段代码中的 [SpineSlot][SpineAttachment] 便是 该节 中描述的 String Property Attributes(字符串属性特性).

重置为Setup Pose

使用 程序性动画 时可能需要将骨骼和/或槽位重置为 setup pose. 只需如下文 设置皮肤 一节中所述, 在设置好 皮肤 后调用 Skeleton.SetupPoseSlots 即可重置.

C#
skeleton.SetupPose();
skeleton.SetupPoseBones();
skeleton.SetupPoseSlots();

设置皮肤

Spine skeleton可包含多套 皮肤, 皮肤定义了哪张附件图片应该用在哪个槽位上. Skeleton组件提供了一个切换皮肤的简单方法.

C#
bool success = skeletonRenderer.Skeleton.SetSkin("skinName");
skeletonRenderer.Skeleton.SetupPoseSlots(); // see note below
C#
// using properties
[SpineSkin] public string skinProperty = "skinName";
...
bool success = skeletonRenderer.Skeleton.SetSkin(skinProperty);
skeletonRenderer.Skeleton.SetupPoseSlots(); // see note below

若不希望之前设置的附件影响当前附件的可见性, 则应在更改皮肤后调用 Skeleton.SetupPoseSlots. 详情请参见 这篇 文档.

组合皮肤

可以组合Spine皮肤, 比如用一套衣物皮肤组合出一个完整的角色. 更多详情请见 新的皮肤API 文档.

C#
var skeleton = skeletonRenderer.Skeleton;
var skeletonData = skeleton.Data;
var mixAndMatchSkin = new Skin("custom-girl");
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("skin-base"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("nose/short"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("eyelids/girly"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("eyes/violet"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("hair/brown"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("clothes/hoodie-orange"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("legs/pants-jeans"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("accessories/bag"));
mixAndMatchSkin.AddSkin(skeletonData.FindSkin("accessories/hat-red-yellow"));
skeleton.SetSkin(mixAndMatchSkin);
skeleton.SetupPoseSlots();
skeletonAnimation.AnimationState.Apply(skeletonRenderer.Skeleton); // skeletonMecanim.Update() for SkeletonMecanim

运行时重打包

当组合皮肤时会不可避免地会使用到到多种material, 这将导致额外绘制调用. 此时可使用 Skin.GetRepackedSkin() 方法, 将不同皮肤中使用到的texture区域合并为单页texture.

C#
using Spine.Unity.AttachmentTools;

AtlasUtilities.RepackAttachmentsOutput repackingOutput; // add this as a member to your MonoBehaviour

// Cleanup from the last repack operation.
// Note: materials and textures returned by previous GetRepackedSkin() calls behave like 'new Texture2D()'
// and need to be destroyed. Also add this call to OnDestroy.
repackingOutput.DestroyGeneratedAssets();

// Create the repacked skin.
AtlasUtilities.RepackAttachmentsSettings settings = AtlasUtilities.RepackAttachmentsSettings.Default;
settings.UseSourceMaterialsFrom(skeletonAnimation.SkeletonDataAsset);
settings.maxAtlasSize = 1024; // change additional settings as needed.
Skin repackedSkin = collectedSkin.GetRepackedSkin("Repacked skin", settings, ref repackingOutput);
collectedSkin.Clear();

// Use the repacked skin.
skeletonRenderer.Skeleton.Skin = repackedSkin;
skeletonRenderer.Skeleton.SetupPoseSlots();
skeletonAnimation.AnimationState.Apply(skeletonRenderer.Skeleton); // skeletonMecanim.Update() for SkeletonMecanim

// You can optionally clear the cache after multiple repack operations.
AtlasUtilities.ClearCache();

重要须知: 如果重打包失败或打包有误, 其原因多为以下几种:

  1. 禁用了texuture读/写. 你需要在需打包的源textures上启用 Read/Write Enabled 参数.
  2. 启用了压缩: 需要确保源texture的Texture导入设置中 Compression 参数置为 None , 而非 Normal Quality.
  3. 质量等级有误: 使用了half/quarter分辨率的textures. 这个是Unity的bug, 当你使用half/quarter分辨率的texture时, Unity会复制错误的附件区域. 因此请确保Unity项目设置(Project Settings)中的质量等级均置为全分辨率texture.
  4. 源texture尺寸并非2的幂次, 但Unity将其尺寸放大到了最接近的幂次:a) 从Spine导出时启用 Pack Settings 中的 Power of two 选项, 或者b) 确保Unity的Atlas Texture导入设置中的 Non-Power of Two 选项被置为 None.

更多信息请查看示例场景 Spine Examples/Other Examples/Mix and MatchSpine Examples/Other Examples/Mix and Match Equip, 以及 MixAndMatch.cs 示例脚本.

进阶操作-带有法线的运行时重打包

也可以在运行时中把主texture的法线贴图(normal maps)和其他的texture层同时重打包. 只需设置 settings.additionalTexturePropertyIDsToCopy = new int[] { Shader.PropertyToID("_BumpMap") }; 来重打包主texture和法线贴图层. 同时确保初始化 repackingOutput.additionalOutputTextures = new Texture2D[1]; 以便接收输出.

C#
AtlasUtilities.RepackAttachmentsSettings settings;
AtlasUtilities.RepackAttachmentsOutput repackingOutput;

if (repackingOutput.additionalOutputTextures == null)
   repackingOutput.additionalOutputTextures = new Texture2D[1];

if (settings.additionalTexturePropertyIDsToCopy == null) {
   settings = AtlasUtilities.RepackAttachmentsSettings.Default;
   settings.UseSourceMaterialsFrom(skeletonAnimation.SkeletonDataAsset);
   settings.maxAtlasSize = 2048;
   settings.additionalTexturePropertyIDsToCopy = new int[] { Shader.PropertyToID("_BumpMap") };
   // settings.additionalTextureIsLinear and .additionalTextureFormats defaults already fit normalmaps
}
Skin repackedSkin = prevSkin.GetRepackedSkin("Repacked skin", settings, ref repackingOutput);
prevSkin.Clear();

// Use the repacked skin.
skeletonRenderer.Skeleton.Skin = repackedSkin;
skeletonRenderer.Skeleton.SetupPoseSlots();
skeletonAnimation.AnimationState.Apply(skeletonRenderer.Skeleton); // skeletonMecanim.Update() for SkeletonMecanim

请注意: 法线贴图属性名后缀一般为 "_BumpMap". 当使用自定义着色器时, 一定要保持属性名的一致性. 这个名字是着色器中的属性名, 而非是Unity检查器的 "Normal Map" 标签中显示的字符串.

Atlas texture 的懒加载

可能项目中每个skeleton都需要多套皮肤和 atlas 页 texture, 但只有一小部分需要预先加载. 请注意, 所有atlas texture都通过 SkeletonDataAsset 间接引用, 因此在加载skeleton时会被加载. 这遵循标准的Unity加载行为.

为了优化内存使用和应用程序下载大小, 可以使用单独的 懒加载 UPM 软件包 在运行时动态加载 texture. 这些软件包允许与 Unity Addressables 等系统集成, 只在分配了皮肤后再去下载高分辨率的 atlas texture. 这减少了初始加载大小, 不过代价是会略微增大加载延迟.

缩放和翻转Skeleton

垂直或水平翻转skeleton是复用动画的常见方式, 例如一个面向左侧的行走动画以右朝向回放就能作为一段新动画了.

C#
bool isFlippedX = skeleton.ScaleX < 0;
skeleton.ScaleX = -1;
bool isFlippedY = skeleton.ScaleY < 0;
skeleton.ScaleY = -1;

skeleton.ScaleX = -skeleton.ScaleX; // toggle flip x state

手动设置骨骼变换(Transforms)

请注意: 该做法仅适用于极特殊的场合. 使用Spine的 BoneFollowerSkeletonUtilityBone 组件与骨骼交互更简单安全.

Skeleton可以设置和获取骨骼变换, 以便实现IK地形适配或者让其他角色和组件(如粒子系统)跟随骨骼运动.

请注意: 由于手动获取和应用新的骨骼位置需要在更新世界变换这一阶段实现, 因此务必要订阅 SkeletonRenderer.UpdateWorld 委托来介入组件生命周期. 否则要么读取到的位置会晚一帧, 要么修改骨骼位置后就被动画覆盖掉.

C#
Bone bone = skeletonRenderer.Skeleton.FindBone("boneName");
Vector3 worldPosition = bone.GetWorldPosition(skeletonRenderer.transform);
// note: when using SkeletonGraphic, all values need to be scaled by the parent Canvas.referencePixelsPerUnit.

Vector3 position = ...;
bone.SetPositionSkeletonSpace(position);

Quaternion worldRotationQuaternion = bone.GetQuaternion();

SkeletonRenderer的生命周期

SkeletonRenderer 组件中, Skeleton 对象保存了所有骨骼, 皮肤, 附件, 约束等的状态信息. 在每次 LateUpdate 中, MeshRenderer 所使用的网格和 Material 都会被更新, 以渲染 skeleton 的当前姿态(pose).

C#

你的脚本可以在 SkeletonRendererLateUpdate 之前或之后执行. 如果你在 SkeletonRendererLateUpdate 之前修改了Material, 这些更改可能会被 SkeletonRenderer 后续的更新操作覆盖, 导致这一修改失效.

SkeletonRendererSkeletonGraphic (UI) 组件提供了回调委托(callback delegates)作为属性, 以便在恰当时机介入其生命周期. 你可以绑定这些委托, 在无需关心组件更新顺序的情况下, 安全地修改 material 及其他 skeleton 属性.

SkeletonRenderer更新回调

  • OnRebuild 在 Skeleton 成功初始化后触发.
  • OnMeshAndMaterialsUpdatedLateUpdate() 结束时触发, 此时网格和所有 material 均已更新完毕.

C#
// your delegate method for OnMeshAndMaterialsUpdated
void AfterMeshAndMaterialsUpdated (SkeletonRenderer renderer) {
// this is called after mesh and materials have been updated.
}

// your delegate method for OnRebuild
void AfterRebuild (SkeletonRenderer renderer) {
// this is called after the Skeleton is successfully initialized.
}

// register your delegate method
void Start() {
skeletonRenderer.OnMeshAndMaterialsUpdated -= AfterMeshAndMaterialsUpdated;
skeletonRenderer.OnMeshAndMaterialsUpdated += AfterMeshAndMaterialsUpdated;

skeletonRenderer.OnRebuild -= AfterRebuild;
skeletonRenderer.OnRebuild += AfterRebuild;
}

有关更多与骨骼和动画相关的回调委托, 请参见 SkeletonAnimation 一章中的 SkeletonAnimation的生命周期 一节.

通过修改 脚本执行顺序 这一替代方案, 也能确保你的脚本在 SkeletonRendererLateUpdate 之后运行, 从而避免更改被覆盖的问题.

SkeletonAnimation组件

SkeletonAnimation 组件是 Unity 中用于驱动 Spine skeleton 动画的两个动画组件之一, 另一个是 SkeletonMecanim 组件. 它需要一个渲染器组件才能工作, 通常与 SkeletonRenderer 组件配合使用, 后者负责管理并渲染skeleton.

SkeletonAnimation 组件可以播放 Spine skeleton 动画, 并响应动画事件.

SkeletonAnimation参数

SkeletonAnimation 检查器暴露了以下参数:

  • Animation Name. 开始播放时将播放的动画.
  • Loop. 定义初始动画是循环播放还是单次播放.
  • Time Scale. 设置时间倍率来减慢或加快动画的播放速度.
  • Unscaled Time. 当置为true时, 更新将基于 Time.unscaledDeltaTime 时间而非 Time.deltaTime. 这对独立于慢动作效果的动画UI元素来说非常有用.
  • Animation Update. 该选项将决定是以普通的 Update (默认) 还是用基于物理的 FixedUpdate 方式来更新动画, 或是通过用户调用来手动更新动画. 当使用分配了 RigidbodyRigidbody2DSkeletonRootMotion 组件时, 推荐将更新模式置为In FixedUpdate, 其他情况则建议置为 In Update.
  • Threaded Animation
    • Use Threading. 启用后, 动画将在多个线程上并行执行, 该组件将注册到共享的 SkeletonUpdateSystem 中. 若置为Use Global Settings, 则将采用 Spine 首选项 中的配置.
  • Root Motion - Add Component. 通过向 GameObject 添加 SkeletonRootMotion 组件来启用RootMotion, 具体说明见下文.

启用Root Motion

SkeletonAnimationSkeletonGraphic (UI) 组件的root motion功能需要搭配独立的 SkeletonRootMotion 组件才能实现. SkeletonAnimation 检查器中的 Root Motion Add Component 按钮可以一键将所需组件添加到 skeleton 的 GameObject 上.

SkeletonAnimation的生命周期

SkeletonAnimation 组件使用 AnimationState 保存了所有正在播放和已队列动画的状态. 每次 Update 都会更新 AnimationState, 使动画随着时间的推移而推进, 然后将新一帧应用到 Skeleton 上来生成新的 pose.

C#

通过代码访问 SkeletonAnimation 组件便能与Skeleton交互. 与大多数Unity组件一样, 建议也在查询到引用对象后将其存储起来, 以供后续使用.

C#
...
using Spine.Unity;

public class YourComponent : MonoBehaviour {

SkeletonAnimation skeletonAnimation;
Spine.AnimationState animationState;
Spine.Skeleton skeleton;

void Awake () {
skeletonAnimation = GetComponent<SkeletonAnimation>();
skeleton = skeletonAnimation.Skeleton;
//skeletonAnimation.Initialize(false); // when not accessing skeletonAnimation.Skeleton,
       // use Initialize(false) to ensure everything is loaded.
animationState = skeletonAnimation.AnimationState;
}

在SkeletonAnimation的 Update 时间点前后均能执行你的代码. 如果在SkeletonAnimation的 Update 之前用代码获取了Skeleton或骨骼的值, 那么代码中取到的是上一帧而非当前帧的值.

组件会将事件回调委托暴露为属性, 使用这些属性便能通过代码介入 SkeletonAnimation 组件的生命周期, 在运行时计算所有骨骼的世界变换之前或之后均可介入. 你可以绑定这些委托来修改骨骼位置和其他Skeleton参数, 而无需关注角色和组件的更新顺序.

SkeletonAnimation Update回调

  • SkeletonAnimation.BeforeApply 在应用该帧动画之前触发该事件. 当你想在动画应用到skeleton上之前改变skeleton状态时, 可以使用该回调.
  • SkeletonAnimation.UpdateLocal 在该帧动画更新完成并应用于skeleton的局部值之后触发该事件. 如果你需要读取或修改骨骼的局部值, 请使用该回调.
  • SkeletonAnimation.UpdateComplete 在Skeleton中所有骨骼的世界值计算完成后触发该事件. 在该事件之后, Update阶段的SkeletonAnimation不再有其他操作. 如果你只需要读取骨骼的世界值, 请使用该回调. 如果其他代码在SkeletonAnimation的Update之后修改了这些值, 这时读取到的世界值仍可能会变化.
  • SkeletonAnimation.UpdateWorld 在计算了Skeleton中所有骨骼的世界值后触发该事件. 若在代码中订阅了该事件, 运行时则将再次调用 skeleton.UpdateWorldTransform. 如果你的skeleton复杂或正在执行其他计算, 该行为将显得非必要甚至有些浪费. 如果需要根据骨骼的世界值来修改骨骼的局部值, 请使用该回调. 该回调在Unity代码中实现自定义约束时会很有帮助.

C#
// your delegate method
void AfterUpdateComplete (ISkeletonAnimation anim) {
// this is called after animation updates have been completed.
}

// register your delegate method
void Start() {
skeletonRenderer.UpdateComplete -= AfterUpdateComplete;
skeletonRenderer.UpdateComplete += AfterUpdateComplete;
}

另一种做法是更改代码执行顺序, 使代码在 SkeletonAnimation 的 Update 之后执行.

代码执行顺序

Unity会根据脚本执行顺序 (详见 ExecutionOrderDefaultExecutionOrder) 排序各个组件的 UpdateLateUpdate 执行. 当调用某个组件的 UpdateLateUpdate 方法来修改skeleton或动画状态时, 应重点关注在 SkeletonRenderer 和 SkeletonAnimation 组件生命周期中的正确时机执行这些代码.

SkeletonRenderer 和 SkeletonAnimation的更新顺序如下:

  • SkeletonAnimation.Update: 更新动画, 将动画应用于skeleton.
  • SkeletonRenderer.LateUpdate: 根据skeleton状态更新skeleton的网格.

若要修改 skeleton 状态:

  • 若要在 应用动画 之前执行代码, 请在 Update 中调用代码, 并将执行顺序设置 在 SkeletonAnimation 之前.
  • 若要在 应用动画 后到生成 skeleton 网格前这段时间内执行代码, 同样请在 Update 中调用代码, 但应将执行顺序设置 在 SkeletonAnimation 之后. 也可以在 LateUpdate 中调用, 并将执行顺序设置 在 SkeletonAnimation 之前.

C#
// At execution order -1, this component executes before SkeletonAnimation and SkeletonRenderer.
[DefaultExecutionOrder(-1)]
public class SetupPoseComponent : MonoBehaviour {
...

void Update() {
// This call lets the skeleton start from setup-pose each frame before applying animations.
// SetupPoseComponent.Update needs to be called before SkeletonAnimation.Update, which is ensured
// by [DefaultExecutionOrder(-1)] above.
skeleton.SetupPose();
}
}

如果实际情况不允许你将代码执行顺序调得太靠后, 你可以按下一节 手动更新 中所述的做法, 在同一帧中手动更新skeleton或skeleton网格.

手动更新

在进行某些修改后, 可能需要立即将动画重新应用到skeleton上, 或者基于修改后的skeleton重新生成skeleton网格. 相较于 通用运行时, SkeletonAnimation 组件的方法可以实现单次调用便完成一致更新. 下例中, skeleton.UpdateWorldTransform() 就是在 Update(deltaTime)ApplyAnimation() 中调用的.

  • Update(deltaTime) 更新了整个skeleton. Skeleton网格维持不变.
    SkeletonAnimation.Update(deltaTime) 则更新了底层的 AnimationState, 然后在这一帧根据skeleton物理约束推进Transform运动, 并将动画应用于skeleton, 最后更新所有骨骼的世界变换(transform). 当你需要在不推进时间的情况下进行更新整个skeleton, 或者你想按照自定义的delta time来推进更新时, 是有必要的手动维护更新步调的.

C#
// After setting bones and slots to setup pose, perform a full skeleton update without advancing time.
skeleton.SetupPose();
skeletonAnimation.Update(0);

// Setting only slots to setup pose usually (except for when active skin bones change)
// does not require bone world transform updates, so AnimationState.Apply(skeleton) is sufficient.
skeleton.SetupPoseSlots();
skeletonAnimation.AnimationState.Apply(skeleton)
C#
// If you don't want to advance by Unity Time.deltaTime or unscaledDeltaTime but by a custom delta time.
skeletonAnimation.timeScale = 0f;
...
skeletonAnimation.Update(customDeltaTime);
  • ApplyAnimation() 将动画重新应用到了skeleton上.
    SkeletonAnimation.ApplyAnimation() 也会将动画重新应用到skeleton上, 但它不会更新底层的 AnimationState 或将变换运动传递给物理约束.

  • UpdateMesh() 用于将 skeleton 网格更新至 skeleton 的当前状态.
    SkeletonRenderer.UpdateMesh() (类似于 SkeletonRenderer.LateUpdate(), 但会忽略 UpdateMode) 会根据 skeleton 的状态更新 skeleton 网格. 当在 LateUpdate() 中修改 skeleton 属性, 且脚本执行顺序导致脚本执行过晚————即在 SkeletonAnimation.LateUpdate() 已经为当前帧更新了 skeleton 网格之后才执行————此时使用 UpdateMesh() 便是有必要的.

C#
// If your script modifies skeleton properties in LateUpdate() and script execution order
// of the script executes after SkeletonAnimation.
void LateUpdate () {
skeleton.SetupPose(); // perform some skeleton modifications
skeletonAnimation.Update(0);
skeletonAnimation.UpdateMesh(); // otherwise SkeletonAnimation.LateUpdate would be called next frame
}

动画-AnimationState

AnimationState的生命周期

SkeletonAnimation 组件实现了 Update 方法, 它按照delta时间更新底层的 AnimationState, 同时根据skeleton物理约束推进变换运动, 再将 AnimationState 应用于skeleton, 最后更新skeleton上所有骨骼的世界变换.

Skeleton动画组件通过 SkeletonAnimation.AnimationState 属性暴露了 AnimationState API. 本节假定你熟悉轨道、轨道条目、mix时间或动画队列等概念, 这些内容的详情可见Spine通用运行时指南中的 应用动画 一节.

时间缩放因子

你可以设置 skeleton 动画组件的时间缩放比例来减慢或加快动画的播放速度. Delta time 用于推进动画, 它直接乘上时间缩放便是动画的播放速度, 例如, 例如缩放因子为0.5时, 动画以半速播放; 缩放因子为2时, 动画则会以二倍速播放.

C#
float timeScale = skeletonAnimation.timeScale;
skeletonAnimation.timeScale = 0.5f;

设置动画

要设置一个动画, 只需提供其轨道索引(index)、动画名称和并确定是否循环动画.

C#
TrackEntry entry = skeletonAnimation.AnimationState.SetAnimation(trackIndex, "walk", true);
C#
// using properties
[SpineAnimation] public string animationProperty = "walk";
...
TrackEntry entry = skeletonAnimation.AnimationState.SetAnimation(trackIndex, animationProperty, true);

传入 AnimationReferenceAsset 作为参数来替代动画名称也是可以的.

C#
// using AnimationReferenceAsset
public AnimationReferenceAsset animationReferenceAsset; // assign a generated AnimationReferenceAsset to this property
...
TrackEntry entry = skeletonAnimation.AnimationState.SetAnimation(trackIndex, animationReferenceAsset, true);

在当前动画变化后, 会自动应用mixing(淡入淡出)以实现两个连续动画间的平滑过渡. 如果要在一条空轨道上mix到某段动画, 请参阅 空动画与清空动画 一节.

重要须知: 不要每帧都调用 SetAnimation. 如果在脚本的 Update 方法中调用 SetAnimation 来设置目标动画, 可以无视动画是否已在播放, 这听起来很有诱惑力, 因为我们会假定 spine-unity 运行时会自动检测动画是否发生了变化. 但这么做不仅会在每一帧重新开始动画播放, 还会让对象冻结在动画的首帧. 由于 Spine 运行时是为动画间的平滑过渡而设计的, 因此每次调用该函数时还会在轨道种添加一个 TrackEntry, 于是已添加在轨道中的动画会持续不断地过渡到新添加的动画.

因此请勿每帧都调用 SetAnimation, 而是跟踪角色的当前状态, 只有当你想切换到不同的动画时再去调用 SetAnimation. 如果想在按住按钮的情况下将动画保持在第一帧, 请设置 TrackEntry.trackTime参数. 如果不能这样做, 可以试试用 AnimationState.ClearTrack 清空轨道上的全部动画, 然后再去设置新动画.

队列动画

要将动画加入队列, 需要提供轨道索引、动画名称、是否循环播放该动画, 以及该动画在轨道上开始播放的延迟时间(以秒为单位).

C#
TrackEntry entry = skeletonAnimation.AnimationState.AddAnimation(trackIndex, "run", true, 2);
C#
// using properties
[SpineAnimation] public string animationProperty = "run";
...
TrackEntry entry = skeletonAnimation.AnimationState.AddAnimation(trackIndex, animationProperty, true, 2);

空动画与清空动画

如果轨道当前没有动画但已设置了动画时, 运行时会立即开始播放动画. 清空轨道后将不再应用轨道动画, Skeleton 将保持清空动画时的姿势. 如通用运行时指南所述, empty动画就是用来mix-in(淡入)或mix-out(淡出)到另一个动画的.

Skeleton 动画组件提供了设置空动画、队列空动画、清空某条或全部轨道的方法. 这些方法的用法与上文中队列动画的方法和参数相似.

C#
TrackEntry entry = skeletonAnimation.AnimationState.SetEmptyAnimation(trackIndex, mixDuration);
entry = skeletonAnimation.AnimationState.AddEmptyAnimation(trackIndex, mixDuration, delay);
skeletonAnimation.AnimationState.ClearTrack(trackIndex);
skeletonAnimation.AnimationState.ClearTracks();

轨道条目

所有可以修改某段动画回放的方法都会返回一个 TrackEntry (轨道条目) 对象, 它可以进一步定制如何回放某段动画, 也能绑定轨道条目中某个事件的委托. 详见下文中的 处理AnimationState事件 一节.

请注意: 运行时从底层的动画状态中删除了轨道条目所对应的动画后, 这些方法返回的轨道条目便会失效. Unity 的垃圾收集器会自动释放它们. 在接收到轨道条目的销毁事件(dispose event)后, 就不应再存储或访问这个轨道条目了.

C#
TrackEntry entry = ...
entry.EventThreshold = 2;
float trackEnd = entry.TrackEnd;

处理AnimationState事件

底层 AnimationState 在回放动画时将触发以下事件来通知监听器:

  1. 动画播放开始(started事件) .
  2. 动画播放中断(interrupted事件), 例如清空了一条轨道或设置了一段新动画.
  3. 无中断地完成了动画播放(completed事件) , 如果是循环动画, 该事件可能会多次出现.
  4. 动画播放停止(ended事件) .
  5. 已销毁动画及其的 TrackEntry (disposed事件) .
  6. 触发了用户定义事件(event事件).

请注意: 当设置了新动画时中断了之前的动画, 这时触发的不是播放完成( complete )事件, 而是播放中断( interrupt )和播放停止( end )事件.

Skeleton动画组件提供了可绑定的委托, 使得C#代码能够响应任意轨道上任意动画的任意事件. 监听器也可以只绑定某个 TrackEntry 的委托. 所以你既可以监听 AnimationState.Complete 这样的事件来响应所有后续动画的 Complete 事件的, 也可以只监听 TrackEntry.Complete 事件来响应某段具体动画触发的 Complete 事件.

C#

在响应 AnimationState 事件的类中, 可按如下方式添加你希望监听的事件的委托:

C#
SkeletonAnimation skeletonAnimation;
Spine.AnimationState animationState;

void Awake () {
skeletonAnimation = GetComponent<SkeletonAnimation>();
animationState = skeletonAnimation.AnimationState;

// registering for events raised by any animation
animationState.Start += OnSpineAnimationStart;
animationState.Interrupt += OnSpineAnimationInterrupt;
animationState.End += OnSpineAnimationEnd;
animationState.Dispose += OnSpineAnimationDispose;
animationState.Complete += OnSpineAnimationComplete;

animationState.Event += OnUserDefinedEvent;

// registering for events raised by a single animation track entry
Spine.TrackEntry trackEntry = animationState.SetAnimation(trackIndex, "walk", true);
trackEntry.Start += OnSpineAnimationStart;
trackEntry.Interrupt += OnSpineAnimationInterrupt;
trackEntry.End += OnSpineAnimationEnd;
trackEntry.Dispose += OnSpineAnimationDispose;
trackEntry.Complete += OnSpineAnimationComplete;
trackEntry.Event += OnUserDefinedEvent;
}

public void OnSpineAnimationStart(TrackEntry trackEntry) {
// Add your implementation code here to react to start events
}
public void OnSpineAnimationInterrupt(TrackEntry trackEntry) {
// Add your implementation code here to react to interrupt events
}
public void OnSpineAnimationEnd(TrackEntry trackEntry) {
// Add your implementation code here to react to end events
}
public void OnSpineAnimationDispose(TrackEntry trackEntry) {
// Add your implementation code here to react to dispose events
}
public void OnSpineAnimationComplete(TrackEntry trackEntry) {
// Add your implementation code here to react to complete events
}


string targetEventName = "targetEvent";
string targetEventNameInFolder = "eventFolderName/targetEvent";

public void OnUserDefinedEvent(Spine.TrackEntry trackEntry, Spine.Event e) {

if (e.Data.Name == targetEventName) {
// Add your implementation code here to react to user defined event
}
}

// you can cache event data to save the string comparison
Spine.EventData targetEventData;
void Start () {
targetEventData = skeletonAnimation.Skeleton.Data.FindEvent(targetEventName);
}
public void OnUserDefinedEvent(Spine.TrackEntry trackEntry, Spine.Event e) {

if (e.Data == targetEventData) {
// Add your implementation code here to react to user defined event
}
}`

详情请参考 Spine API文档.

在回调中更改AnimationState或游戏状态

虽然可以在 AnimationState 事件回调中通过调用 AnimationState.SetAnimation() 这类方法修改 AnimationState, 但同时需要考虑时点(timing)问题. 若通过事件回调更改游戏状态, 也会受到时点问题的影响.

  1. 使用 SkeletonAnimation.Update() 中应用动画后到 SkeletonAnimation.LateUpdate() 更新网格之前这段时间, 才会触发 AnimationState 事件回调.
  2. 如果在 AnimationState.End 事件回调中调用了AnimationState.SetAnimation(), 将会在同一帧中触发 AnimationState.Start 事件.
  3. 用mix从一个动画过渡到另一个动画时, 第二个动画的 Start 事件会在第一个动画的 End 事件 之前 触发. 修改游戏状态时常遇到由此引发的问题.

如果需要将事件回调中的触发推迟到下一个 Update() 循环, 使用 StartCoroutine 即可:

C#
trackEntry.End += e => {
StartCoroutine(NextFrame(() => {
YourCode();
}));
};

...

IEnumerator NextFrame (System.Action call) {
yield return 0;
if (call != null)
call();
}

Coroutine中的Yield指令

spine-unity运行时提供了几个用于Unity Coroutines的yield指令. 如果你尚不熟悉Unity Coroutines, 那么Coroutine教程Unity Coroutine文档 值得阅读一番.

以下是运行时提供的yield指令:

  1. WaitForSpineAnimation. 在 Spine.TrackEntry 触发某种特定事件前等待.
C#
var track = skeletonAnimation.state.SetAnimation(0, "interruptible", false);
var completeOrEnd = WaitForSpineAnimation.AnimationEventTypes.Complete |
                      WaitForSpineAnimation.AnimationEventTypes.End;
yield return new WaitForSpineAnimation(track, completeOrEnd);
  1. WaitForSpineAnimationComplete. 在 Spine.TrackEntry 触发 Complete 事件前等待.
C#
var track = skeletonAnimation.state.SetAnimation(0, "talk", false);
yield return new WaitForSpineAnimationComplete(track);
  1. WaitForSpineAnimationEnd. 在 Spine.TrackEntry 触发 End 事件前等待.
C#
var track = skeletonAnimation.state.SetAnimation(0, "talk", false);
yield return new WaitForSpineAnimationEnd(track);
  1. WaitForSpineEvent. 在 Spine.AnimationState 触发用户自定义的 Spine.Event 事件(事件名称在Spine编辑器中设置)前等待.
C#
yield return new WaitForSpineEvent(skeletonAnimation.state, "spawn bullet");
// You can also pass a Spine.Event's Spine.EventData reference.
Spine.EventData spawnBulletEvent; // cached in e.g. Start()
..
yield return new WaitForSpineEvent(skeletonAnimation.state, spawnBulletEvent);

请注意: 和Unity自带的yield指令一样, 也可以复用spine-unity的yield实例以减少内存消耗.

教程页面

该页中 可找到关于spine-unity事件的教程.

在编码中使用字符串属性特性

在检查器中手动输入动画名称十分繁琐易错. 为此spine-unity提供了一种将字符串参数转换为下拉框(popup)选项的方法. 在 string 属性前加上属性特性(property attribute), 运行时便能将该属性自动转换为一个下拉框中的可选字段, 例如你可以在下拉框中列出一个skeleton上的所有可用动画. 如果你在某个Spine组件中见过同样的字段, 那么在自定义组件中也可以通过属性特性实现相同字段的下拉框. 以下展示了可用的属性特性.

C#
[SpineBone] public string bone;
[SpineSlot] public string slot;
[SpineAttachment] public string attachment;
[SpineSkin] public string skin;
[SpineAnimation] public string animation;
[SpineEvent] public string event;
[SpineIkConstraint] public string ikConstraint;
[SpineTransformConstraint] public string transformConstraint;
[SpinePathConstraint] public string pathConstraint;

关于字符串属性特性的更多详细信息, 请见spine-unity包内置的 示例场景.

SkeletonGraphic组件

SkeletonGraphic 组件负责在 Canvas UI 中绘制 skeleton 并管理 skeleton 的当前状态. 它是两种 skeleton 渲染组件之一, 另一个是用于普通(非 Canvas 元素)的SkeletonRenderer组件.

SkeletonGraphic 组件使用 Unity 的 UI 系统进行布局定位、渲染和遮罩交互.
与 skeleton 相关的一般操作(如设置皮肤、修改骨骼等)与 SkeletonRenderer 组件完全一致.

为何要特意设计一种UI组件?

Unity UI (UnityEngine.UI) 使用 CanvasCanvasRenderers 系统来排序和管理其可渲染对象. Unity内置的可渲染 UI 组件——例如 Text, Image, 和 RawImage ——都依赖于 CanvasRenderer 来工作. 将 MeshRenderers (如默认的 Cube 对象) 或 SpriteRenderers (例如 Sprite) 等对象置入UI, Unity 也不会在 Canvas 中渲染它们. 由于 SkeletonRenderer 使用的是 MeshRenderers, 因此得到的对待同样是不予渲染. 因此 spine-unity 运行时为此提供了 SkeletonRenderer 的等效组件 SkeletonGraphic, 它是使用 CanvasRenderers 组件进行渲染的 UnityEngine.UI.MaskableGraphic 类的子类.

重要Material使用须知

SkeletonGraphic 组件中的material只能使用兼容 CanvasRenderer 的特殊着色器, 例如默认的 Spine/SkeletonGraphic* 着色器. 不要在 SkeletonGraphic 组件上使用URP、LWRP或如 Spine/Skeleton 这样的普通着色器. 没有肉眼可见的视效错误并不意味着这些着色器搭配上 SkeletonGraphic 可以正常工作. 现在已知这样的搭配会导致组件在移动设备上无法正确渲染, 但在 Unity 编辑器中的渲染效果却看不出任何问题. 和其他UI组件一样, SkeletonGraphic 使用 CanvasRenderer 而非 MeshRenderer 组件, 这两种组件使用了迥然不同的渲染管线.

SkeletonDataAsset 被实例化为 SkeletonGraphic 时, 组件将忽略 SpineAtlasAsset 中的普通 material, 只使用其中的 texture. 对此, 可以使用 SkeletonGraphicCustomMaterials 组件来覆盖掉 SkeletonGraphic 组件中的 material.

重要须知: 由于Unity CanvasRenderer 组件本身的限制, SkeletonGraphic 默认情况下只能使用单张texture. 此时你可以在 SkeletonGraphic 的组件检查器中启用 Advanced - Multiple CanvasRenderers, 这样便能给每个子网格生成一个带 CanvasRenderer 的子 GameObject 来支持多张texture. 不过出于性能方面的考虑, 最好尽可能地避免这样做. 因此这意味着在UI中使用的Skeletons应被打包成一个single-texture (单页) atlas, 而非多页atlas. 要了解如何将附件图片打包为单页atlas texture, 请见 进阶操作 - 单页Texture Atlas导出与SkeletonGraphic 一节.

修正Shader和Materials的配置

使用特殊着色器 (如"Spine/SkeletonGraphic Tint Black") 或在 CanvasGroup 中使用SkeletonGraphic时, 需为其生成顶点数据. 你可以在 Advanced - Vertex Data 检查器中找到相关参数, 包括 Tint Black, CanvasGroup CompatiblePMA Vertex Color. 运行时在这三种属性旁都放置了 Detect 按钮, 点击即可自动推断出正确设置. 运行时同时还提供了一个 Detect Settings 按钮可一键检测这三种属性设置是否配置有误.

修改了 Advanced - Vertex Data 设置后, 需更新material来适配新设置. 在 SkeletonGraphic 中, materials与textures相对独立, 因此material属性相同的不同skeleton可以使用同一个material. 基于此, 运行时针对常用material参数和着色器搭配提供了一种特殊的共享SkeletonGraphic material. 点击 Material 属性旁的 Detect 按钮, 运行时可根据 Advanced - Vertex Data 设置自动选择适合的material.

如果skeleton使用了多种blend模式并启用了 Advanced - Multiple CanvasRenderers 选项, 那么使用 Blend Mode Materials 旁的 Detect 按钮, 运行时也会为该blend模式自动选择material.

若发现最终显示效果与预期不符, 多半是atlas texture导入设置有误 (请参阅该节 进行错误排查).

CanvasGroup的alpha问题

当使用带有CanvasGroupSpine/SkeletonGraphic*着色器时, 你会发现随着 CanvasGroupAlpha 值的减小, 但 skeleton 反而变亮, 效果如下图所示:

这是由于Unity在后台运算中修改了顶点色的alpha值, 而Unity的这一机制与spine-unity运行时的premultiplied alpha顶点着色器并不兼容.

重要须知: 新增的 Detect 按钮可自动设置 Advanced - Vertex Data 面板中的参数; 此外还添加了 Detect Material 按钮, 用于根据当前设置自动选择合适的material. 如无需了解全部细节, 可跳过下两节内容.

不带Tint Black的SkeletonGraphic

本节内容适用于在 material 中使用了 Spine/SkeletonGraphic 着色器 (Spine/SkeletonGraphic TintBlack* 除外) 的情况. 如果在带有alpha淡出功能的 CanvasGroup 中使用此类 material, 则 Material 必须启用 CanvasGroup Compatible 参数并禁用 SkeletonGraphic 组件的 PMA Vertex Colors 选项:

  1. 可以在 Materials/UI-PMATexture/CanvasGroupMaterials/UI-StraightAlphaTex/CanvasGroup 文件夹下分别找到每种材质的 CanvasGroup Compatible 版本.
  2. 将这个 CanvasGroup Compatible 的material分配给 CanvasGroup 中的任意 SkeletonGraphic 组件, 换掉原版的material.
  3. 任何使用这种 CanvasGroup 兼容版material的 SkeletonGraphic 组件都需要一并禁用 Advanced - PMA Vertex Colors 选项, 以避免半透明部件的变暗效果超级加倍. 遗憾的是, 该选项会阻碍 additive 槽位和普通槽位的渲染合批, 进而增加绘制调用数.

带有Tint Black的SkeletonGraphic

在带有alpha淡出功能的 CanvasGroup 中使用附加了 Spine/SkeletonGraphic TintBlack* 着色器的material时, 需要进行以下设置:

  1. 使用 CanvasGroup 文件夹下的 SkeletonGraphic TintBlack materials.
  2. SkeletonGraphic 组件中选择该material, 换掉原版的material.
  3. SkeletonGraphic 组件中启用 Advanced - CanvasGroup Compatible
  4. 启用 SkeletonGraphicAdvanced - PMA Vertex Colors 选项与否均可. 不过我们建议启用 Advanced - PMA Vertex Colors 选项, 因为在可以将additive槽位同普通槽位在单次绘制调用中合批渲染.

边界与正确可见性

RectTransform 的边界将决定 SkeletonGraphic 的可见性. 当通过拖放 Skeleton 将其实例化为 Canvas GameObject 的子对象时, RectTransform 边界会自动适配 skeleton 的初始 pose. 单击 Match RectTransform with Mesh 按钮也能手动让RectTransform 边界适配当前 pose. 需要注意的是, RectTransform边界尺寸切不能小于网格尺寸, 否则如 RectMask2D 这样的组件会认为 RectTransform 位于网格外——这会导致即使需要被渲染的网格部件仍在 RectTransform 内部, 组件也会在绘制时忽略掉该 skeleton. 当激活了 RectTransform 工具 的五种Transform 模式之一后, 当前 RectTransform 边界就会显示在场景视图中.

SkeletonGraphic参数

SkeletonGraphic 组件的参数与 SkeletonRenderer 组件的参数类似, 请参考SkeletonRenderer 一节以了解更多参数信息.

SkeletonGraphic 检查器暴露的额外参数如下:

  • Material - Detect 按钮. 该按钮将根据 Advanced - Vertex Data 设置指定适合的 "Spine/SkeletonGraphic*" material. 与下文中 Advanced 面板里的 Detect Material 按钮参数相同.

  • Freeze. 当置为 true 时, 运行时将暂停更新 SkeletonGraphic.

  • Layout Scale Mode. SkeletonGraphic 支持基于 RectTransform 边界的自适应缩放. 默认值 None 表示保持已有行为. 将该参数设置为 Width Controls HeightHeight Controls Width, Fit In ParentEnvelope Parent (该项细节详见 该页面) 将启用自动缩放. 若需要修改参考布局边界, 请点击 Edit Layout Bounds 按钮进入编辑模式即可调整skeleton边界. 调整后运行时将自动缩放Skeleton, 以适应对象的 RectTransform 参考布局边界.

  • Edit Layout Bounds. 该选项用于修改上文中提到的 Layout Scale Mode 所用的参考布局边界, 点击该按钮将进入编辑模式, 然后就可以手动调整边界或者点击 Match RectTransform with Mesh 按钮自动调整边界以适配当前pose. 当完成调整后再次点击 Edit Layout Bounds 按钮即可退出编辑模式.

  • Match RectTransform with Mesh 点击 Match 按钮会使 SkeletonGraphic 的RectTransform匹配当前pose. 需要注意的重点是, RectTransform边界尺寸万万不能比网格小, 否则如 RectMask2D 这样的组件会认为RectTransform位于网格外, 即使需要被渲染的网格部件仍在RectTransform内部, 组件也会在绘制时忽略掉这个skeleton. 仅当 Layout Scale Mode 被置为 None, 或者点击了 Edit Layout Bounds 进入了编辑模式后, 该按钮才可用.

  • Advanced

    • Update When Invisible. 指定 SkeletonGraphic 位于父级 RectMask2D 的边界之外(即不可见)时所使用的更新模式. 当SkeletonGraphic 重新可见后, 该更新模式将自动重置为 UpdateMode.FullUpdate.

    请注意: 仅使用 Canvas 或普通 Mask 组件无法进行可见性检测, 必须在有需要的地方添加 RectMask2D 组件, 以确保正确的剔除状态更新.

    • Separator Slot Names.Enable Separationtrue 时, 该选项表示渲染分割点的槽位名称. 关于渲染分割(render separation)的详情, 请参考 SkeletonRenderSeparator 一节, 不过使用 SkeletonGraphic 时无需额外组件便能实现渲染分割.

    • Enable Separation. 该选项无需添加任何额外组件 (如 SkeletonRenderSeparatorSkeletonRendererSkeletonPartsRenderer 组件) 便能直接启用渲染分割. 启用该选项后运行时将自动创建分拆部件的 GameObjects, 同时附加所需的 CanvasRenderer GameObjects 到层次结构中. 可在层次结构中按需调整分拆部件 GameObjects 的层级关系, 以便调整组件在 Canvas 中的绘制顺序.

    • Update Part Location. 当启用该选项后, 将更新部件 GameObject 的位置来匹配 SkeletonGraphic 的位置. 在需要将部件重新挂载到其他 GameObject 下时, 该功能便会派上用场.

    • Multiple CanvasRenderers. 当该选项置为 true 时, SkeletonGraphic 将不再局限于单个 CanvasRenderer, 而是对应每个绘制调用 (即子网格) 自动创建子 CanvasRenderer GameObjects. 如此做法虽可以突破组件的 单页txture限制, 但会增大性能开销.

      • Blend Mode Materials. 在这里可以为各种槽位Blend模式指定不同的 SkeletonGraphic material. 不过此处仅支持兼容 "Spine/SkeletonGraphic *" 或其他兼容 CanvasRenderer 的Materials.

      • Assign Default 用来设置默认blend模式的material SkeletonGraphicAdditive, SkeletonGraphicMultiplySkeletonGraphicScreen.

        请注意: 应确保正确设置了 SkeletonDataAssetBlend Mode Materials, 因为 SkeletonGraphic 的blend模式material依赖于 SkeletonDataAsset 的material. 当启用 PMA Vertex Colors 后, 运行时将忽略 Additive Material 设置.

    • Tint Black (!). 向网格中添加black tint顶点数据. 若有任意 skeleton 槽位设置了black tint, 则应启用该选项.
      Black tinting功能要求着色器将 UV2 和 UV3 视为 black tint 色来实现着色效果, 比如运行时内置的 Spine/SkeletonGraphic Tint Black* 着色器就是这样工作的.
      要在父 Canvas 上使用 UV2 和 UV3 数据, 需要选中 Canvas 组件并启用 Additional Shader Channels 下的TexCoord1TexCoord2 选项.

    若 SkeletonData 中包含设置了 tint black 颜色的槽位, 则会显示一个 Detect 按钮来启用该参数, 反之则将禁用该参数.

  • CanvasGroup Compatible. 当在 CanvasGroup 中使用 SkeletonGraphic 时应启用该选项. 也需要在 Material 中启用 CanvasGroup Compatible, 因此在此设置更改后请通过 Detect Material 按钮或手动分配来重新选择适合的 material.
    当使用 SkeletonGraphic Tint Black* 着色器时, 若同时启用了 Tint BlackPMA Vertex Color 选项, 此时运行时将在 uv2.g 中存储 alpha 值而非将其存储于主顶点色 color.a, 同时 color.a 将被置为常量 1.0 以支持 CanvasGroupcolor.a 的更改.

    若 SkeletonGraphic 组件位于 CanvasGroup 层次结构中, 则会显示一个 Detect 按钮来启用该参数, 反之则将禁用该参数.

  • PMA Vertex Colors (用于解决 CanvasGroup alpha 问题). 启用该选项将把顶点色的RGB与其alpha值相乘. 默认启用此参数; 若非正在使用第三方着色器, 或无需在非 "SkeletonGraphic TintBlack*" 着色器上启用 CanvasGroup Compatible 选项, 则默认值即为最佳设置.

    正确设置了上述的 Tint BlackCanvasGroup Compatible 选项后界面中会显示 Detect 按钮. 如果使用了第三方 (即非Spine) 着色器, 或启用了 CanvasGroup Compatible 的同时禁用了 Tint Black 选项, Detect 按钮将禁用该参数. 反之它则会启用该参数.

    应用 PMA Vertex Color 的具体规则 (仅当 Detect 失效时才需关注这些规则) 如下:

    应启用该参数的场合为 (默认启用):

    • 使用了 "Spine/SkeletonGraphic*" 着色器却禁用了 CanvasGroup Compatible 选项 (无关 Straight Alpha Texture 的启用与否),
    • 使用了 "Spine/SkeletonGraphic TintBlack*" 着色器 (无关 Straight Alpha Texture 的启用与否),
    • 使用了需要顶点色作PMA的第三方着色器 (例如使用了PMA additive输出blend模式 Blend One OneMinusSrcAlpha 的着色器).

    应禁用该参数的场合为:

    • 未使用 "Spine/SkeletonGraphic TintBlack*" 着色器, 而是使用启用了 CanvasGroup Compatible 的普通 "Spine/SkeletonGraphic*" 着色器,
    • 使用了需要常规顶点色的第三方着色器 (例如使用了普通输出blend模式 Blend SrcAlpha OneMinusSrcAlpha 的着色器).

    启用 PMA Vertex Color 参数后, 将把additive槽位和普通槽位在单次绘制调用中合批渲染. 而禁用该选项后, 则需要在additive槽位上启用 SkeletonDataBlend Mode Materials - Apply Additive Material 选项, 但这样做会产生额外绘制调用, 从而降低性能表现.

  • Detect Settings. 该按钮可一键检测 Tint Black, CanvasGroup CompatiblePMA Vertex Colors 参数.

  • Detect Material. 根据前文中的 Tint BlackCanvasGroup Compatible 参数及atlas texture的导入设置(即PMA和straight alpha 设置), 运行时将自动选择适合的 SkeletonGraphic material. 如果显示结果不正确, 很可能是你的texture设置有误(请参阅该节 进行错误排查).

示例场景

示例场景 Spine Examples/Getting Started/6 Skeleton Graphic 中演示了 SkeletonGraphic 组件的基本用法. 在 Spine Examples/Other Examples/SkeletonRenderSeparator 中提供了高级示例场景, 它展示了如何设置渲染分割槽位并修改绘制顺序.

SkeletonMecanim组件

SkeletonMecanim 组件是 Unity 中用于驱动 Spine skeleton动画的两种动画组件之一, 另一种是 SkeletonAnimation 组件. 它需要一个渲染器组件的支持才能工作, 通常与 SkeletonRenderer 组件搭配使用, 后者负责管理并渲染skeleton.

SkeletonMecanim 同时使用了 Unity 的 Mecanim 动画系统和 Spine 动画系统, 后者负责 Skeleton 的姿态控制和 Skeleton 设置, 以此实现高级动画控制. 该组件使用 Mecanim 系统来确定应播放哪段 Spine 动画, 以及每段动画的轨道时间和透明度(alpha). 然后将对应的 Spine 动画 应用于Spine skeleton.

SkeletonAnimation 组件类似, 通过 SkeletonMecanim 也可以将 Spine skeleton 附加到GameObject 上, 然后动画化该 skeleton 并处理各种动画事件等等. 在应用动画后到绘制 skeleton 前的这段时间里, 你可以用 SkeletonAnimation 一样的方式修改 skeleton. 但相较于 SkeletonAnimation, 该组件有一些限制和额外需求:

请注意:SkeletonAnimation 组件不同的是, 如果要从前一段动画的时间轴状态平滑过渡(mix out)到下一段动画, SkeletonMecanim 强制要求后一段动画的时间轴首帧为关键帧. 更多信息参见下文中的 额外关键帧需求 一节.

请注意: TrackEntry.AttachmentThreshold 和类似的mix阈值功能在 SkeletonMecanim 中不可用.

SkeletonMecanim 组件提供的参数与 SkeletonAnimation组件 类似, 请查阅SkeletonAnimation 一节以了解 SkeletonMecanim 的参数信息.

额外关键帧需求

为了平滑地从上一段动画的时间轴状态(如骨骼旋转)过渡到下一段动画, 后者在setup pose时的首帧必须为一个关键帧. 否则无法清除前一段动画的时间轴状态. 这也是 SkeletonMecanim 相较于 SkeletonAnimation 组件的一个缺点.

简短示例: 前一段 jump 动画应平滑地过渡到 idle 动画. 假设 jump 动画结束时, 骨骼 bone1bone2 不在setup-pose位置上, 那么为了正确地mix out到 jump 状态必须将 idle 动画的首帧添加关键帧, 以照顾 bone1bone2 的位置(即把骨骼置为setup pose或任意自定义pose).

Auto Reset 参数可以重置动画状态, 但它在动画过渡结束时粗暴地快速mix out, 无法实现平滑过渡的效果.

此外, 在将skeleton导出为 .json.skel.bytes 格式时务必禁用 Animation cleanup 选项, 否则key进setup pose中的设置均不会被导出!

SkeletonMecanim参数与动画Blending

SkeletonMecanim 在检查器中还暴露了以下参数:

  • Mecanim Translator

    • Auto Reset. 当其置为 true 后, 在一段动画结束时skeleton状态会mix out为setup pose. 如果一段动画改变了某些附件的可见性, 该功能就特别重要了: 当mix out时, 附件可见性将重置为setup pose状态, 否则当前附件状态将一直保持, 除非其他动画中的某个关键帧再次设置该附件.

    • Custom MixMode. 当禁用该参数后, 运行时会根据图层的blend模式使用 MixMode 的建议值. 而当启用该参数时则会在下方显示 Mix Modes 设置面板, 方便你为每个Mecanim图层单独指定 MixMode.

    • Mix Modes. 当启用了上文中的 Custom MixMode 选项后显示该面板. 在该面板中可以设置连续动画间的mix模式和跨图层动画间的mix模式.

      • Mix Next (建议用于 Base LayerOverride 图层)
        将动画应用于前一条轨道, 然后按照Mecanim过渡权重mix in到下一条轨道.
      • Always Mix (建议用于 Additive 图层)
        淡出前一条轨道中的动画(当启用了Auto Reset时则是淡出到setup pose), 并使用Mecanim过渡权重mix in到下一条轨道. 要注意在基础图层(base layer)上使用该选项可能会造成动画出现非预期的浸润(dipping)效果.
      • Hard (以前叫 Spine Style)
        将动画立即应用于下一条轨道.
    • Scene Preview. 预览在动画窗口中选中的动画剪辑. 打开动画窗口, 选中某段动画片段, 同时保持检查器中的 SkeletonMecanim 组件处于选中状态, 便可在动画窗口中拖动时间轴进行预览.

Auto Reset 与图层 Mix Mode 参数的搭配结果

当动画有过渡时, 存在四种pose - current state last frame, setup pose, previous clip posenew clip pose - 其组合结果如下:

  1. current state last frame 开始 (或者在SkeletonMecanim更新前对该帧进行了修改).
  2. 应用 setup pose:
  • 当启用了 Auto Reset, setup pose 将取代 current state last frame.
  • 当禁用了 Auto Reset, setup pose 将不参与mix.

  1. 应用 previous clip pose:
  • 当mix模式置为 Always Mix 时, previous clip pose 将与当前状态mix (因此当启用 Auto Reset 后会mix setup pose).
  • 当mix模式置为 HardMix Next 时, 将按100%权重应用 previous clip pose 并完全覆盖当前状态 (因此它会覆盖 setup pose).

  1. 应用 new clip pose:
  • 当mix模式置为 Always MixMix Next 时, new clip pose 将与当前状态mix. 因此启用了 Auto ResetAlways Mix 将会是对 setup pose, previous clip posenew clip pose 三者的mix.
  • 当mix模式置为 Hard, 将按100%权重应用 new clip pose 并完全覆盖已应用的pose.

下表显示的是修改了相同时间轴值 (例如相同的骨骼旋转) 的当前剪辑片段(Clip) P 和新剪辑片段 N 的各种mix结果. S 代表启用了 Auto Reset 时的setup pose, 若禁用了 Auto Reset 则代表当前状态 (例如前一帧). w 表示过渡权重 (过渡开始时为0, 过渡结束时为1). 每种图层blend模式的默认mix模式(即推荐值)以粗体标出.

Always Mix Mix Next Hard
Base Layer lerp(lerp(S, P, 1-w), N, w) lerp(P, N, w) N
Override lerp(
lerp(lower_layers_result,
P, (1-w) * layer_weight),
N, w * layer_weight)
lerp(
lerp(lower_layers_result,
P, layer_weight),
N, w * layer_weight)
lerp(lower_layers_result,
N,
layer_weight)
Additive lower_layers_result +
layer_weight * lerp(P, N, w))
counts as Always Mix lower_layers_result +
layer_weight * N
缩写 含义
S Setup pose
P Previous clip pose
N New clip pose
w Transition weight
lerp(a, b, weight) Linear interpolation from a to b by weight.

SkeletonMecanim的控制器和动画器

SkeletonMecanim 组件和Unity Mecanim的 Animator 组件以相同的方式使用控制器(Controller)资产. 当通过 拖放 将一个skeleton实例化为 SkeletonMecanim 时, 运行时会自动生成并添加控制器资产.

请注意: 当启用 Apply Root Motion 后, 运行时会将 SkeletonMecanimRootMotion 组件自动添加到 SkeletonMecanim 的GameObject上.

将Spine动画拖放到Animator面板便能向控制器的动画状态机中添加动画. 添加过的动画将显示在控制器资产中.

SkeletonMecanim 将忽略在 [SkeletonDataAsset](/spine-unity-assets#Skeleton Data资产) 中设置的 Mix时长 值. 因为Mecanim是读取的是Animator面板中设置的过渡时间.

SkeletonMecanim事件

SkeletonMecanim 组件的事件存储在各个 AnimationClip 中, 其触发方式同其他Unity动画事件一样. 例如, 若在Spine中把事件命名为 "Footstep", 那么需要在 SkeletonMecanim 的GameObject上添加一个 MonoBehaviour 脚本, 使用名为 Footstep() 的方法来处理该事件. 当在Spine中使用文件夹时, 方法名则是文件夹名和动画名的拼接.例如, 若前文中的事件被放在一个名为 Foldername 的文件夹中时, 方法名将变为 FoldernameFootstep().

C#
public class YourComponentReceivingEvents : MonoBehaviour {
// to capture event "Footstep" when it's placed outside of folders
void Footstep() {
Debug.Log("Footstep event received");
}

// to capture event "Footstep" when it's placed in a folder named "Foldername"
void FoldernameFootstep () {
Debug.Log("Footstep (in folder Foldername) received");
}
}

关于Unity Mecanim事件的详细信息, 请参见 Unity的Animation Events文档.

下一节: 工具组件 上一节: 资产