Spine iOS 运行时文档

Licensing

将官方的Spine运行时整合到你的应用程序之前, 请仔细阅读 Spine运行时许可页面.

简介

Spine iOS 运行时是基于 spine-cpp 实现的轻度封装. 它可以使用 UIKitSwiftUI 运行. 同时支持 SwiftObjective-C.

该运行时使用 Metal 进行渲染, 并支持除 tint black 外的所有 Spine 功能.

安装

Spine iOS 支持高于 iOS 13 及所有后续系统. 要在项目中使用 Spine iOS, 可以通过 Swift 包管理器CocoaPods 安装运行时.

应确保 spine-runtimes 仓库分支的 major.minor 版本与 Spine 编辑器的 major.minor 版本一致. 有关更多信息, 请参见 Spine 版本控制 一节.

Swift 包管理器

要将 Spine iOS SPM 包添加到您的项目中, 请:

  1. 在 Xcode 中打开你的项目.
  2. 导航到项目设置(Project Settings): 在项目导航器(Project Navigator)中点击并选择项目.
  3. 选中 Swift Packages: 点击 Package Dependencies 选项卡.
  4. 添加包: 点击 + 并在搜索框中输入 git@github.com:EsotericSoftware/spine-runtimes.git.
  5. 依赖规则: 选中 Commit 并输入你所需的 major.minor 分支中的最新提交哈希.
  6. 完成添加: 在Xcode解析包之后点击下一步(Next)和完成(Finish)按钮.

您还需要启用 C++ 支持:

  1. 导航到项目设置(Project Settings): 点击应用的主目标平台(main target).
  2. 打开 Built Settings 选项卡: 搜索 C++ and Objective-C Interoperability
  3. 选择 C++ / Objective-C++

CocoaPods

由于版本控制的工作方式不兼容CocoaPods, 因此Spine iOS 不在公共的 CocoaPods 索引中. 你必须手动链接.podspec 文件才能将其添加到 Podfile 中. 请确保使用正确的 major.minor 分支版本号.

python
# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'

target 'Spine iOS Example' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!

pod '
Spine', :podspec => 'https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.2/Spine.podspec'
pod 'SpineCppLite', :podspec => 'https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.2/SpineCppLite.podspec'
pod 'SpineShadersStructs', :podspec => 'https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/4.2/SpineShadersStructs.podspec'
end

使用运行时

两种安装方式都可以直接在Swift文件中直接引用 Spine :

Swift
import Spine

示例项目

Spine iOS 运行时内置了数个用于功能展示的示例.

你可按以下步骤运行示例项目:

  1. 在Max上安装 Xcode
  2. 克隆 spine-runtimes 仓库: git clone https://github.com/esotericsoftware/spine-runtimes
  3. 用 Xcode 打开 spine-runtimes/spine-ios/Example/Spine iOS Example.xcodeproj

也可以打开 spine-runtimes/spine-ios/Example - Cocoapods/Spine iOS Example.xcodeproj 来查看如何使用 CocoaPods 导入 Spine iOS.

以下列出的示例均支持 SwiftUI 预览, 并可直接在 Xcode 的画布(canvas)中渲染.

示例项目中包含了以下示例:

  • SimpleAnimation.swift: 演示了如何使用 SpineViewSpineController 加载导出的 Spine skeleton, 并在视图中显示skeleton以及播放指定的动画.
  • PlayPauseAnimation.swift: 演示了如何暂停和恢复动画.
  • AnimationStateEvents.swift: 演示了如何设置槽位颜色, 队列多个动画, 以及如何监听动画状态事件.
  • DebugRendering.swift: 展示了如何通过 SpineControlleronAfterPaint 回调在已渲染的skeleton上进行自定义绘制(custom drawing).
  • DressUp.swift: 演示了 Spine 的皮肤功能以及如何将skeleton渲染为用于角色创建UI的缩略图.
  • IKFollowing.swift: 演示如何让用户通过触控操作拖动某根skeleton的骨骼.
  • DisableRendering.swift: 演示如何在 SpineView 移出屏幕时停止渲染. 在需要节省 CPU/GPU 资源的时候该功能至关重要.
  • SimpleAnimationViewController.m: 演示如何结合 UIKitObjective-C 来使用 Spine iOS.

更新Spine iOS运行时

在更新项目中的 Sine iOS 运行时之前, 请先查阅Spine 编辑器和运行时版本管理指南.

对于 CocoaPods 的用户, 链接到正确的 major.minor 分支即可.

对于 SPM 用户, 则应从 major.minor 分支中选择正确的提交哈希.

注意: 若更改了 Spine iOS 包的 major.minor 版本, 则必须使用同版本的 Spine 编辑器重新导出 Spine skeleton !

使用 Spine iOS

Spine iOS 运行时是对通用 spine-cpp 的地道(idiomatic) Swift 包装, 支持加载、播放和操作通过 Spine 创建的动画. Spine iOS 运行时几乎将所有 spine-cpp API 以 Swift 类惯用的方式公开, 并额外提供了 SwiftUI 和 UIKit 类, 以便轻松显示Spine skeleton并与之交互.

Spine iOS 运行时支持除 tint black 外的所有 Spine 功能.

资产管理

导出适用于Spine iOS的Spine资产

请按 Spine 用户指南中的说明进行操作来:

  1. 导出skeleton和动画数据
  2. 导出包含skeleton图像的texture atlases

导出的skeleton数据和texture atlas将产生以下文件:

  1. skeleton-name.jsonskeleton-name.skel 文件, 包含了skeleton和动画数据.
  2. skeleton-name.atlas, 包含了texture atlas的相关信息.
  3. 一张或多张 .png 文件, 每一个文件表示texture atlas中的一页, 每个页则指打包成页的skeleton所引用的图像.

注意: 您应该优先选择skeleton的二进制导出而非 JSON 格式的导出文件, 因为二进制文件体积更小, 加载速度更快..

可以通过如Atlas, SkeletonData, SkeletonDrawableSpineView 的 Spine iOS 类来加载这些文件.

注意: 如果你使用了非预乘(non-premultiplied)资产, 则应在应用程序的目标构建设置中禁用 Compress PNG FilesRemove Text Metadata From PNG Files.

更新 Spine 资产

在开发过程中, 你可能会频繁更新 Spine skeleton 数据和texture atlas文件. 简单地覆盖掉这些源文件(.json, .skel, .atlas, .png)即可完成更新, 具体做法是从 Spine 编辑器重新导出并替换 Xcode 项目中的现有文件.

在此过程中应该确保 Spine iOS 的 major.minor 版本与导出文件的 Spine 编辑器的 major.minor 版本一致. 有关更多信息, 请参阅 Spine 版本控制 一节.

核心类

Spine iOS API 基于 spine-cpp 通用运行时, 提供了平台无关的核心类和算法, 可用于加载、查询、修改并动画化 Spine skeleton. 其核心类以 Swift 类惯用的方式公开.

本节将简要讨论你在日常使用 Spine iOS 时会接触到的关键核心类. 有关 Spine 运行时架构、核心类和 API 使用的详细概述, 请查阅 Spine Runtimes Guide.

Atlas 类存储了从 .atlas 文件及其对应的 .png 图像文件中加载的数据.

SkeletonData 类存储了从 .json.skel 格式的skeleton文件中加载的数据. skeleton数据包含了骨骼层次结构、槽位、附件、约束、皮肤和动画的信息. 通常是通过一个 Atlas 类来实例化 SkeletonData 实例, 实例便可从 Atlas 中获取skeleton所使用的图像. 该类是创建 Skeleton 实例的蓝图. 可以从同一个atlas和skeleton数据实例化多个skeleton, 从而共用已加载的数据, 这样可最大限度地降低运行时的加载时长和内存消耗.

Skeleton 类存储了从 SkeletonData 实例创建的skeleton实例. skeleton实例存储了其当前pose, 即当前的骨骼位置和槽位设置、附件和活动皮肤. 当前pose可以通过手动修改骨骼层次结构来得到, 而更常见做法的是通过 AnimationStateWrapper 来应用动画, 以此改变当前pose.

AnimationStateWrapper 持有 AnimationState 类, 该类负责跟踪应应用于skeleton的动画, 并基于最后渲染帧和当前渲染帧间的时差来推进和mixing这些动画, 最后将动画应用于skeleton实例从而设置其当前pose. AnimationState 会查询 AnimationStateData 实例来检索动画间的mixing时长, 若没有设置mixing时长时使用默认mixing时长.

Spine iOS 运行时正是构建在这些核心类之上.

SpineView / SpineUIView

/img/spine-runtimes-guide/spine-ios/simple-animation.png

SpineView 结构体是基于 SpineUIViewUIViewRepresentable, 因此后者可以在 SwiftUI 项目中直接使用. SpineUIView 本质是一个派生自 MTKView的类.

下文中将会把这两者统称为 SpineView.

SpineView 负责加载和显示 Spine skeleton. 至少, 该视图需要知道从哪里加载skeleton和atlas文件, 并接受传入一个 SpineController 实例, 该实例负责修改widget的状态, 例如设置动画或更改skeleton的皮肤.

SpineController 是一个 ObservableObject, 可通过 @State 变量声明. 然后就能在另一个视图 body 中直接实例化出一个 SpineView, 如下例所示:

Swift
@StateObject
var controller = SpineController(
   onInitialized: { controller in
      controller.animationState.setAnimationByName(
         trackIndex: 0,
         animationName: "walk",
         loop: true
      )
   }
)

var body: some View {
   SpineView(
      from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
      controller: controller,
      mode: .fit,
      alignment: .center
   )
}

在实例化时, SpineView 将异步加载你指定的文件, 并从中构建底层核心类实例——即 Atlas, SkeletonData, Skeleton, AnimationStateDataAnimationState 类的实例.

加载完成后, 将调用 SpineControlleronInitialized 方法来修改widget的状态, 例如设置一个或多个动画、操作骨骼层次结构或修改skeleton的皮肤. 详情请参阅下文中的 SpineController 一节.

SpineView 类的首个参数是 SpineViewSource 枚举, 以确定从何种来源加载skeleton和atlas文件:

  • SpineViewSource.bundle 从主包或指定的包中加载文件.
  • SpineViewSource.file 从文件系统中加载文件.
  • SpineViewSource.http 从 URL 加载文件.
  • SpineViewSource.drawable()SkeletonDrawable 构建视图. 这在需要预加载、缓存和/或在 SpineView 实例之间共享skeleton数据时非常有用. 详情请参阅下文的预加载和共享skeleton数据一节.

SpineView 还有额外的可选参数, 可进一步更改 Spine skeleton 在视图内的适配方式、对齐方式和视图尺寸.

mode: skeleton在 SpineUIView 内的适配方式. 默认值为 .fit

  • mode, skeleton在 SpineUIView 内的适配方式. 默认值为 .fit
  • alignment, skeleton在 SpineUIView 内的对齐方式. 默认值为 .center
  • boundsProvider, 用于计算skeleton边界框在不同适配方式和对齐模式下的尺寸(像素). 默认情况下使用skeleton的setup pose边界框. 更多详细信息请参阅 SetupPoseBoundsRawBoundsSkinAndAnimationBounds 文档.
  • backgroundColor: 视图的背景色. 默认情况下使用 UIColor.clear

SpineView 还有一个额外的可选绑定参数 isRendering, 它可以禁用渲染. 更多信息请参阅 DisableRendering.swift 示例.

SpineController

SpineController 用于控制 SpineView 中的skeleton如何应用动画和渲染. 控制器在构造函数中提供了一些可选的回调, 这些回调会在 SpineView 生命周期中的特定时刻触发.

控制器通过 getter 返回的 Spine Runtimes API 对象(如 Atlas, SkeletonData, SkeletonAnimationState)公开skeleton状态, 通过这些对象便可更改状态. 更多信息请参阅 Spine 运行时指南类文档.

SpineView 初始化时, 会触发一次控制器的 onInitialized() 回调方法. 此方法可用于设置待播放的初始动画或设置skeleton的皮肤等.

初始化完成后, SpineView 将以屏幕刷新率连续渲染. 在每一帧都会根据当前队列的动画更新 AnimationState, 然后应用于 Skeleton.

接下来控制器会触发可选的 onBeforeUpdateWorldTransforms() 回调, 它可以在 Skeleton.updateWorldTransform() 计算当前pose前修改skeleton.

在计算完当前pose后, 会触发可选的 onAfterUpdateWorldTransforms() 回调, 可以在渲染skeleton之前深度修改当前pose. 这也是手动定位骨骼的完美时机.

SpineView 渲染skeleton前, 会触发可选的 onBeforePaint() 回调, 此时可渲染背景, 或渲染其他视图层次结构中位于skeleton之下的对象.

SpineWidget 渲染了当前skeleton pose后, 会触发可选的 onAfterPaint() 回调, 它用于渲染在视图层次结构中处于skeleton之上的其他对象.

默认情况下, 视图每帧都会更新和渲染skeleton. SpineController.pause() 方法可以暂停skeleton的更新和渲染. SpineController.resume() 方法则会恢复skeleton的更新和渲染. SpineController.isPlaying 属性用于报告当前播放状态. 更多详情请参阅 AnimationStateEvents.swift 示例.

SkeletonDrawableWrapper / SkeletonDrawable

SkeletonDrawableWrapper 持有 SkeletonDrawable 类, 并将加载、存储、更新和渲染 Skeleton 及其关联的 AnimationState 类等功能打包到一个易于使用的类中. SpineView 通过 SkeletonDrawableWrapper 实例封装了其需呈现的skeleton状态.

可使用 fromBundle(), fromFile()fromHttp() 方法从文件构建 SkeletonDrawableWrapper. 要在多个 SkeletonDrawableWrapper 实例间共享 AtlasSkeletonData, 则需要通过构造函数实例化drawables对象, 并给他们传入同一套atlas和skeleton数据.

SkeletonDrawableWrapper 还公开了 SkeletonDrawable, Skeleton, AnimationStateAnimationStateWrapper 对象来查询、修改和动画化skeleton. 同时一并公开了构建了skeleton和动画状态的 AtlasSkeletonData.

要播放skeleton动画, 应使用 AnimationState API(如 AnimationState.setAnimation()AnimationState.addAnimation())在轨道上队列动画.

若要更新动画状态, 将其应用于skeleton, 然后更新当前的skeleton pose, 则请调用 SkeletonDrawableWrapper.update() 方法, 并提供以秒为单位的时长参数以播放动画.

要将 Skeleton 的当前pose渲染为 CGImage, 可使用 SkeletonDrawableWrapper/renderToImage(size:backgroundColor:scaleFactor:).

SkeletonDrawable 中的对象分配在堆(native heap)上. 如果不再需要 SkeletonDrawable, 则需调用 SkeletonDrawable.dispose() 手动销毁对象. 否则会导致内存泄漏.

注意: SpineController 在被销毁时会自动执行此操作. 但若在 SpineController 作用域外持有其他 SkeletonDrawableWrapper, 则需按上述方法释放内存. 此时请将 SpineController 的可选构造函数参数 disposeDrawableOnDeInit 置为 false.

应用动画

SpineController 回调中的 AnimationState 可将动画应用于显示在 SpineView 中的skeleton.

注意: 关于动画轨道和动画队列的详情请参阅 Spine 运行时指南中的 应用动画一节.

调用 AnimationState.setAnimationByName() 可在轨道 0 上设置一段动画:

Swift
@StateObject
var controller = SpineController(
   onInitialized: { controller in
       // Set the walk animation on track 0, let it loop
      controller.animationState.setAnimationByName(
         trackIndex: 0,
         animationName: "walk",
         loop: true
      )
   }
)

其首个参数指定轨道编号, 第二个参数是动画名称, 第三个参数设置是否循环播放动画.

可以用这种方式队列多个动画:

Swift
controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2)
controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0)

addAnimationByName() 的第一个参数也是轨道号. 第二个参数仍是动画名称. 第三个参数还是循环动画的开关. 但最后一个参数表示播放延迟(以秒为单位), 这段延迟之后该轨道中的前一段动画会mix到这段动画上.

在上文示例中, 运行时会首先播放 "walk" 动画. 2 秒后播放一次 "jump" 动画, 然后过渡到循环播放的 "run" 动画.

从一段动画过渡到另一段动画时, AnimationState 将在指定的持续时长内mix这两段动画. 这些mix时长定义在 AnimationStateData 实例中, AnimationState 要用的时候会从中检索mix时长.

AnimationStateData 实例也可以通过控制器实例访问. 您可以设置默认mix时长, 或为某对动画设置mix时长:

Swift
controller.animationStateData.defaultMix = 0.2
controller.animationStateData.setMixByName(fromName: "walk", toName: "jump", duration: 0.1)

设置或添加动画后会返回一个 TrackEntry 对象, 通过这个对象能进一步定制该动画的播放. 例如可以设置轨道条目的reverse属性来倒放某段动画:

Swift
var entry = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
entry.reverse = true

其他选项请参阅 TrackEntry 类文档 类文档.

注意: 不要在使用它们的函数之外保有 TrackEntry 实例. 函数内部会重用轨道条目, 一旦它所代表的动画播放完后, 这些实例就会成为无效实例.

可以在动画轨道上设置或队列空动画, 就能平滑地将skeleton置为其 setup pose:

Swift
controller.animationState.setEmptyAnimation(trackIndex: 0, mixDuration: 0.5)
controller.animationState.addEmptyAnimation(trackIndex: 0, mixDuration: 0.5, delay: 0.5)

setEmptyAnimation() 的第一个参数指定轨道号. 第二个参数指定mix时长(以秒为单位), 这个时长用于设置淡出前一个动画并淡入"空"动画的持续时间.

addEmptyAnimation() 的第一个参数指定轨道号. 第二个参数指定mix时长. 第三个参数是空动画播放延迟(以秒为单位), 这段延迟之后该轨道中的前一个动画会mix到空动画.

AnimationState.clearTrack() 可以立即清空轨道上的所有动画. 用 AnimationState.clearTracks() 则会一次清空所有轨道. 这时skeleton将保持为最后一个pose.

调用 Skeleton.setToSetupPose() 就能将skeleton重置为setup pose:

Swift
controller.skeleton.setToSetupPose()

如果需要将skeleton的骨骼和槽位均重置为setup pose中的设置, 则应调用 Skeleton.setSlotsToSetupPose().

AnimationState 事件

AnimationState 在动画播放的生命周期中会触发事件. 监听这些事件便能根据需要做出响应. Spine Runtimes API 定义了以下几种 EventType 类型:

  • SPINE_EVENT_TYPE_START: 当动画开始时触发.
  • SPINE_EVENT_TYPE_INTERRUPT: 当清空动画轨道或设置了新动画时触发.
  • SPINE_EVENT_TYPE_COMPLETE: 当动画播放完一个循环时触发.
  • SPINE_EVENT_TYPE_END: 当不再应用动画时触发.
  • SPINE_EVENT_TYPE_DISPOSE: 当销毁动画的轨道条目时触发.
  • SPINE_EVENT_TYPE_EVENT: 在发生用户自定义的 时间 时触发.

AnimationStateWrapper 注册一个 AnimationStateListener 回调便可监听所有动画的事件; 或者用 AnimationStateWrapper.setTrackEntryListener() 监听某个队列中动画的 TrackEntry:

Swift
var walkEntry = controller.animationState.setAnimationByName(0, "walk", true);
controller.animationStateWrapper.setTrackEntryListener(entry: walkEntry) { type, entry, event in
   if let eventk, type == SPINE_EVENT_TYPE_EVENT {
      print("User defined event: \(event.data.name ?? "--")")
   }
}

controller.animationStateWrapper.setStateListener { type, entry, event in
   print("Animation state event \(type)")
}

更多细节请参阅 AnimationStateEvents.swift 示例.

皮肤

/img/spine-runtimes-guide/spine-ios/skins.png

许多应用程序和游戏可以让用户组合各种独立部件(例如头发、眼睛、裤子或耳环、包等配饰)来创建自定义形象. Spine可以通过皮肤混搭来实现这一功能.

下例是从其他皮肤中重组出一套自定义皮肤的做法:

Swift
var data = controller.skeletonData
var skeleton = controller.skeleton
var customSkin = Skin.create(name: "custom-skin")
customSkin.addSkin(other: data.findSkin(name: "skin-base")!)
customSkin.addSkin(other: data.findSkin(name: "nose/short")!)
customSkin.addSkin(other: data.findSkin(name: "eyelids/girly")!)
customSkin.addSkin(other: data.findSkin(name: "eyes/violet")!)
customSkin.addSkin(other: data.findSkin(name: "hair/brown")!)
customSkin.addSkin(other: data.findSkin(name: "clothes/hoodie-orange")!)
customSkin.addSkin(other: data.findSkin(name: "legs/pants-jeans")!)
customSkin.addSkin(other: data.findSkin(name: "accessories/bag")!)
customSkin.addSkin(other: data.findSkin(name: "accessories/hat-red-yellow")!)
skeleton.skin = customSkin
skeleton.setToSetupPose()

静态函数 Skin.create() 会创建自定义皮肤.

然后从控制器中获取 SkeletonData. SkeletonData.findSkin() 通过它便能按名称查找皮肤.

Skin.addSkin() 会将所指定的皮肤组件添加到新建的自定义皮肤中.

最后在 Skeleton 上设置新皮肤, 并调用 Skeleton.setSlotsToSetupPose() 以确保没有残留原先的皮肤和/或动画附件.

注意: Skin 类是一个对底层 C++ 对象的包装. 当不再使用它时应调用 Skin.dispose() 手动将其销毁.

详情请参阅 DressUp.swift 示例, 该示例还演示了如何使用 SkeletonDrawableWrapper 渲染皮肤的预览缩略图.

设置骨骼变换

/img/spine-runtimes-guide/spine-ios/bone-transform.png

在 Spine 编辑器中创建skeleton时, skeleton采用的是所谓的skeleton坐标系. 这个坐标系与 SpineView 渲染skeleton的坐标系并不一致. 因此当用户需要通过触控移动骨骼的这种用例下, 触控点相对于 SpineView 的坐标就需要转换为skeleton坐标.

SpineController 为此提供了 toSkeletonCoordinates() 方法, 该方法可以传入一个 SpineView 中的 CGPoint 坐标, 再将其转换为skeleton坐标.

详情请参阅 IKFollowing.swift 示例.

还可以使用 fromSkeletonCoordinates() 将给坐标换向. 请参阅 DebugRendering.swift 示例以了解更多信息.

访问 Spine 运行时 API

Spine iOS 将几乎全部 Spine 运行时 API 映射到了 Swift 上. SkeletonAnimationState 这种由 SpineControllerSkeletonDrawableWrapper/SkeletonDrawable 返回的对象, 均是从 spine-cpp API 到 Swift 的1:1直译. 因此可以将Spine 运行时指南中的通用写法通过 Swift 代码 几乎全部复现.

然而由于本质上是桥接的 spine-cpp, 因此会产生一些局限性:

  • 返回的数组或map都只是内部数组的一份副本. 修改它不会产生任何效果.
  • 不能直接地创建、添加或删除骨骼、槽位和其他 Spine 对象.
  • 时间轴的 C++ 类结构在 Swift 中不可见.