Spine iOS 运行时文档

Licensing

在将 Spine 运行时集成到你的应用程序之前, 请先阅读 Spine 运行时许可 页面.

简介

spine-ios 运行时是围绕 spine-c 实现的地道 Swift 轻量封装. 它可以同时用于 UIKit 和 SwiftUI, 并支持 Swift 和 Objective-C.

它使用 Metal 进行渲染, 支持除 tint black 外的所有 Spine 功能, 包括物理.

安装

spine-ios 支持 iOS 13.0、tvOS 13.0、macOS 10.15、macCatalyst 13.0、visionOS 1.0 和 watchOS 6.0 及以上版本. 要在项目中使用 spine-ios, 请使用 Swift 包管理器 安装.

确保 spine-runtimes 仓库分支的 major.minor 版本与你导出的 Spine 编辑器 major.minor 版本一致. 详见 Spine 版本控制.

Swift 包管理器

将 spine-ios SPM 包添加到你的项目:

通过 Xcode

  1. 在 Xcode 中打开你的项目
  2. 转到 File → Add Package Dependencies
  3. 输入仓库 URL: https://github.com/esotericsoftware/spine-runtimes.git
  4. 选择版本(例如, branch "4.3")
  5. 选择你需要的库:
    • SpineC - 用于底层访问的 C API
    • SpineSwift - 用于 Swift 项目的 Swift API
    • SpineiOS - 使用 Metal 的 iOS/tvOS 渲染

通过 Package.swift

swift
dependencies: [
   .package(url: "https://github.com/esotericsoftware/spine-runtimes.git", branch: "4.3")
],
targets: [
   .target(
      name: "YourTarget",
      dependencies: [
         .product(name: "SpineiOS", package: "spine-runtimes"),
         // Or use SpineSwift for cross-platform Swift-only code:
         // .product(name: "SpineSwift", package: "spine-runtimes"),
      ]
   )
]

使用

现在你可以在 Swift 文件中导入相应的模块:

swift
import SpineiOS // For iOS/tvOS with UI components
// or
import SpineSwift // For cross-platform Swift code

示例

spine-ios 运行时包含多个展示其功能集的示例.

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

  1. 在 Mac 上安装 Xcode
  2. 克隆 spine-runtimes 仓库: git clone https://github.com/esotericsoftware/spine-runtimes
  3. 用 Xcode 打开 spine-runtimes/spine-ios/Example/Spine iOS Example.xcodeproj
  4. 选择你的目标设备(模拟器或真机)
  5. 按下 Run (⌘R) 构建并运行示例

以下列出的所有示例都支持 SwiftUI 预览, 可以直接在 Xcode 的画布中渲染.

示例项目包含以下示例:

  • SimpleAnimation.swift: 演示了使用 SpineViewSpineController 加载导出的 Spine skeleton、在视图中显示并播放指定动画的基本用法.
  • PlayPauseAnimation.swift: 演示了如何暂停和恢复动画.
  • AnimationStateEvents.swift: 演示了如何设置槽的颜色、如何队列多个动画以及如何监听动画状态事件.
  • DebugRendering.swift: 展示了如何通过 SpineControlleronAfterPaint 回调在已渲染的skeleton上进行自定义绘制.
  • DressUp.swift: 演示了 Spine 的皮肤功能, 以及如何将skeleton渲染为用于角色创建 UI 的缩略图.
  • IKFollowing.swift: 演示了如何通过触控让用户拖动skeleton的某根骨骼.
  • Physics.swift: 演示了物理约束的交互式物理模拟.
  • DisableRendering.swift: 演示了当 SpineView 移出屏幕时如何禁用渲染. 这在需要节省 CPU/GPU 资源时非常重要.
  • SimpleAnimationViewController.m: 演示了如何在 UIKit 和 Objective-C 中使用 spine-ios.

更新 spine-ios 运行时

在更新项目的 spine-ios 运行时之前, 请查阅我们的 Spine 编辑器和运行时版本管理指南.

对于 Swift 包管理器, 请从正确的 major.minor 分支中选择正确的提交哈希或分支.

注意: 如果你更改了 spine-ios 的 major.minor 版本, 必须使用相同版本的 Spine 编辑器重新导出你的 Spine skeleton!

使用 spine-ios

spine-ios 运行时是围绕通用 spine-c 运行时的地道 Swift 封装, 支持加载、播放和操作用 Spine 创建的动画. spine-ios 运行时将几乎所有 spine-cpp API 以地道的 Swift 方式公开, 并提供了 SwiftUI 和 UIKit 专用的类, 可以轻松显示 Spine skeleton并与之交互.

spine-ios 运行时支持除 tint black 外的所有 Spine 功能, 包括物理.

资产管理

为 spine-ios 导出

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

  1. 导出skeleton和动画数据
  2. 导出包含skeleton图像的纹理图集

导出的skeleton数据和纹理图集将产生以下文件:

  1. skeleton-name.jsonskeleton-name.skel, 包含你的skeleton和动画数据.
  2. skeleton-name.atlas, 包含纹理图集的相关信息.
  3. 一个或多个 .png 文件, 每个代表纹理图集的一页, 包含skeleton使用的打包图像.

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

这些文件可以通过 spine-ios 类(如 AtlasSkeletonDataSkeletonDrawableSpineView)加载.

注意: 如果你使用非预乘资产, 需要在应用程序目标的构建设置中禁用 Compress PNG FilesRemove Text Metadata From PNG Files. 或者, 你可以在 Xcode 中选择 .png 文件, 然后将其类型设置为 Other - Data, 这样可以防止任何预处理.

更新 Spine 资产

在开发过程中, 你可能需要频繁更新 Spine skeleton数据和纹理图集文件. 你可以直接覆盖这些源文件(.json.skel.atlas.png), 方法是从 Spine 编辑器重新导出并替换 Xcode 项目中的现有文件.

确保 spine-ios 的 major.minor 版本与导出文件的 Spine 编辑器 major.minor 版本一致. 详见 Spine 版本控制.

核心类

spine-ios API 构建在通用 spine-c 运行时之上, 该运行时提供了平台无关的核心类和算法, 用于加载、查询、修改和动画化 Spine skeleton. 核心类通过 SpineSwift 模块以地道的 Swift 类方式公开.

在这里, 我们将简要讨论你在日常使用 spine-ios 时会接触到的最重要的核心类. 有关 Spine 运行时架构、核心类和 API 使用的详细概述, 请查阅 Spine 运行时指南.

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

SkeletonData 类存储了从 .json.skel skeleton文件中加载的数据. skeleton数据包含骨骼层次结构、槽位、附件、约束、皮肤和动画的信息. SkeletonData 实例通常需要同时提供一个 Atlas, 从中获取skeleton使用的图像. 它是创建 Skeleton 实例的蓝图. 多个skeleton可以从同一个 atlas 和skeleton数据实例化, 它们共享已加载的数据, 从而最大程度减少加载时间和运行时内存消耗.

Skeleton 类存储从 SkeletonData 实例创建的skeleton实例. skeleton存储其当前姿态, 即骨骼位置和槽位、附件及活动皮肤的当前配置. 当前姿态可以通过手动修改骨骼层次结构来计算, 但更常见的是通过 AnimationState 应用动画来设置.

AnimationState 类负责跟踪应该应用于skeleton的动画, 根据上一帧和当前渲染帧之间经过的时间推进和混合这些动画, 并将动画应用于skeleton实例, 从而设置其当前姿态. AnimationState 查询 AnimationStateData 实例来获取动画之间的混合时间, 如果没有为动画对设置混合时间, 则获取默认混合时间.

spine-iOS 运行时正是基于这些核心类构建了 iOS 专有功能.

SpineView / SpineUIView

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

SpineView 结构体是围绕 SpineUIViewUIViewRepresentable, 因此后者可以在 SwiftUI 项目中使用. SpineUIViewMTKView 的子类.

接下来, 我们将两者统称为 SpineView.

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

SpineController 是一个 ObservableObject, 应该保存在 @StateObject 变量中. 在最简单的情况下, 可以像这样在另一个视图的 body 中实例化 SpineView:

swift
@StateObject
var controller = SpineController(
   onInitialized: { controller in
      controller.animationState.setAnimation(0, "walk", true)
   }
)

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

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

加载完成后, 将调用 SpineControlleronInitialized 回调, 允许它修改skeleton的状态, 例如设置一个或多个动画、操作骨骼层次结构或修改皮肤. 详见下文的 SpineController 部分.

SpineView 类接受 SpineViewSource 枚举作为第一个参数, 用于从不同来源加载skeleton和 atlas 文件:

  • SpineViewSource.bundle 从主 bundle 或提供的 bundle 加载文件.
  • SpineViewSource.file 从文件系统加载文件.
  • SpineViewSource.http 从 URL 加载文件.
  • SpineViewSource.drawable()SkeletonDrawable 构造视图. 当需要预加载、缓存和/或在 SpineView 实例之间共享skeleton数据时, 这非常有用. 详见下文的"预加载和共享skeleton数据"部分.

此外, SpineView 还有可选参数, 可以进一步定义 Spine skeleton在视图内的适配方式和对齐方式:

  • mode, skeleton在 SpineUIView 内的适配方式. 默认为 .fit
  • alignment, skeleton在 SpineUIView 内的对齐方式. 默认为 .center
  • boundsProvider, 用于计算适配和对齐时使用的边界框的像素大小. 默认使用skeleton的 setup pose 边界框. 详见 SetupPoseBoundsRawBoundsSkinAndAnimationBounds 的类文档.
  • backgroundColor: 视图的背景色. 默认使用 UIColor.clear

SpineView 还有一个额外的可选绑定参数 isRendering, 可以通过它禁用渲染. 详见 DisableRendering.swift 示例.

SpineController

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

控制器通过返回 Spine 运行时 API 对象的属性(如 AtlasSkeletonDataSkeletonAnimationState)暴露skeleton状态, 通过这些对象可以操作状态. 详见 Spine 运行时指南类文档.

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

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

接下来会调用可选的 onBeforeUpdateWorldTransforms() 回调, 它可以在使用 Skeleton.updateWorldTransform() 计算当前姿态之前修改skeleton.

在计算完当前姿态后, 会调用可选的 onAfterUpdateWorldTransforms() 回调, 它可以在渲染skeleton之前进一步修改当前姿态. 这是手动定位骨骼的好地方.

SpineView 渲染skeleton之前, 会调用可选的 onBeforePaint() 回调, 它允许渲染背景或其他应该位于skeleton之后的物体.

SpineView 渲染完当前skeleton姿态后, 会调用可选的 onAfterPaint() 回调, 它允许在skeleton之上渲染其他物体.

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

SkeletonDrawableWrapper / SkeletonDrawable

SkeletonDrawableWrapper 持有 SkeletonDrawable, 并将加载、存储、更新和渲染 Skeleton 及其关联的 AnimationState 打包成一个易于使用的类. SpineView 通过 SkeletonDrawableWrapper 实例封装其显示的skeleton状态.

使用 fromBundle()fromFile()fromHttp() 方法从文件资源构造 SkeletonDrawableWrapper. 要在多个 SkeletonDrawableWrapper 实例之间共享 AtlasSkeletonData, 请通过构造函数实例化 drawable, 向每个实例传入相同的 atlas 和skeleton数据.

SkeletonDrawableWrapper 公开 SkeletonDrawableSkeletonAnimationStateAnimationStateEventManager 用于查询、修改和动画化skeleton. 它还公开构建skeleton和动画状态的 AtlasSkeletonData.

要动画化skeleton, 请通过 AnimationState API(如 AnimationState.setAnimation()AnimationState.addAnimation())在一个或多个轨道上队列动画.

要更新动画状态、将其应用于skeleton并更新当前skeleton姿态, 请调用 SkeletonDrawableWrapper.update() 方法, 提供以秒为单位的增量时间来推进动画.

要将 Skeleton 的当前姿态渲染为 CGImage, 请使用 SkeletonDrawableWrapper.renderToImage(size:backgroundColor:scaleFactor:).

SkeletonDrawable 存储在原生堆上分配的对象. 如果不再需要 SkeletonDrawable, 需要手动调用 SkeletonDrawable.dispose() 来释放对象. 否则会导致原生内存泄漏.

注意: SpineController 在销毁时会自动执行此操作. 但是, 如果你在 SpineController 外部持有 SkeletonDrawableWrapper, 则需要按上述方法手动释放. 在这种情况下, 请将 SpineController 的可选构造函数参数 disposeDrawableOnDeInit 设置为 false.

应用动画

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

注意: 详见 Spine 运行时指南中的 应用动画, 了解动画轨道和动画队列的更多信息.

要在轨道 0 上设置特定动画, 请调用 AnimationState.setAnimation():

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

第一个参数指定轨道, 第二个参数是动画名称, 第三个参数定义是否循环播放动画.

你可以队列多个动画:

swift
controller.animationState.setAnimation(0, "walk", true)
controller.animationState.addAnimation(0, "jump", false, 2)
controller.animationState.addAnimation(0, "run", true, 0)

addAnimation() 的第一个参数是轨道. 第二个参数是动画名称. 第三个参数指定是否循环播放动画. 最后一个参数定义延迟秒数, 在此动画应该替换轨道上的前一个动画之前经过的时间.

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

从一个动画过渡到另一个动画时, AnimationState 将在指定的时间内混合动画. 这些混合时间在 AnimationStateData 实例中定义, AnimationState 从中获取混合时间.

AnimationStateData 实例也可以通过控制器访问. 你可以设置默认混合时间, 或为特定动画对设置混合时间:

swift
controller.animationStateData.defaultMix = 0.2
controller.animationStateData.setMix("walk", "jump", 0.1)

设置或添加动画时, 会返回一个 TrackEntry 对象, 通过它可以进一步修改该动画的播放. 例如, 你可以设置轨道条目来反转动画播放:

swift
let entry = controller.animationState.setAnimation(0, "walk", true)
entry.reverse = true

更多选项请参阅 TrackEntry 类文档.

注意: 不要在使用它们的函数之外保留 TrackEntry 实例. 轨道条目在内部会被重用, 一旦它所代表的动画完成, 该实例就会失效.

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

swift
controller.animationState.setEmptyAnimation(0, 0.5)
controller.animationState.addEmptyAnimation(0, 0.5, 0.5)

setEmptyAnimation() 的第一个参数指定轨道. 第二个参数指定混合时长(以秒为单位), 用于淡出前一个动画并淡入\"空\"动画.

addEmptyAnimation() 的第一个参数指定轨道. 第二个参数指定混合时长. 第三个参数是延迟秒数, 在此之后空动画应该通过混合替换轨道上的前一个动画.

可以使用 AnimationState.clearTrack() 立即清除轨道上的所有动画. 要一次清除所有轨道, 可以使用 AnimationState.clearTracks(). 这将使skeleton保持在其最后一个姿态.

要skeleton的姿势重置为 setup pose, 请使用 Skeleton.setupPose():

swift
controller.skeleton.setupPose()

这将把骨骼和槽位都重置为 setup pose 配置. 使用 Skeleton.setupPoseSlots() 可以只将槽位重置为 setup pose 配置.

AnimationState 事件

AnimationState 在动画播放的生命周期中会发出事件. 你可以监听这些事件并根据需要做出响应. Spine 运行时 API 定义了以下 EventType 类型:

  • EventType.start: 动画开始时发出.
  • EventType.interrupt: 动画轨道被清除或设置了新动画时发出.
  • EventType.complete: 动画完成一个循环时发出.
  • EventType.end: 动画将不再被应用时发出.
  • EventType.dispose: 动画的轨道条目被释放时发出.
  • EventType.event: 发生用户定义的 事件 时发出.

要接收事件, 你可以向 AnimationState 注册事件监听器回调以接收所有动画的事件, 或者向特定的 TrackEntry 注册以接收该动画的事件:

swift
let walkEntry = controller.animationState.setAnimation(0, "walk", true)
walkEntry.setListener { type, entry, event in
   if type == .event, let event = event {
      print("User defined event: \(event.data.name)")
   }
}

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

详见 AnimationStateEvents.swift 示例.

皮肤

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

许多应用程序和游戏允许用户通过组合许多独立物品(如头发、眼睛、裤子或耳环、包等配饰)来创建自定义角色. 使用 Spine, 可以通过混合和搭配皮肤来实现这一功能.

你可以像这样从其他皮肤创建自定义皮肤:

swift
let data = controller.skeletonData
let skeleton = controller.skeleton
let customSkin = Skin.create("custom-skin")
customSkin.addSkin(data.findSkin("skin-base")!)
customSkin.addSkin(data.findSkin("nose/short")!)
customSkin.addSkin(data.findSkin("eyelids/girly")!)
customSkin.addSkin(data.findSkin("eyes/violet")!)
customSkin.addSkin(data.findSkin("hair/brown")!)
customSkin.addSkin(data.findSkin("clothes/hoodie-orange")!)
customSkin.addSkin(data.findSkin("legs/pants-jeans")!)
customSkin.addSkin(data.findSkin("accessories/bag")!)
customSkin.addSkin(data.findSkin("accessories/hat-red-yellow")!)
skeleton.setSkin2(customSkin)
skeleton.setupPose()

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

接下来从控制器获取 SkeletonData. 通过 SkeletonData.findSkin() 按名称查找皮肤.

通过 Skin.addSkin() 将要组合的所有皮肤添加到新的自定义皮肤中.

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

注意: Skin 包装底层 C++ 对象. 当不再使用时需要手动调用 Skin.dispose() 释放.

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

物理

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

spine-ios 完全支持物理约束, 允许创建响应运动和力量的动态逼真动画. 物理可以通过更新世界变换时的 Physics 枚举来控制:

swift
// Update with physics simulation
skeleton.updateWorldTransform(.update)

// Reset physics state
skeleton.updateWorldTransform(.reset)

// Pose without physics
skeleton.updateWorldTransform(.pose)

物理系统支持各种约束类型, 包括质量、阻尼、重力和惯性. 详见 Physics.swift 示例的交互式演示.

设置骨骼变换

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

在 Spine 编辑器中创作skeleton时, skeleton定义在所谓的skeleton坐标系中. 这个坐标系可能与 SpineView 渲染skeleton的坐标系不一致. 如果用户应该能够通过触控移动骨骼, 需要将相对于 SpineView 的触控坐标转换为skeleton坐标系.

SpineController 提供了 toSkeletonCoordinates(position:) 方法, 它接受相对于关联的 SpineViewCGPoint, 并将其转换为skeleton坐标系.

详见 IKFollowing.swift 示例.

你也可以使用 fromSkeletonCoordinates(position:) 反向转换坐标. 详见 DebugRendering.swift 示例以了解更多.

访问 Spine 运行时 API

spine-ios 将几乎所有 Spine 运行时 API 映射到 Swift. 由 SpineControllerSkeletonDrawableWrapper/SkeletonDrawable 返回的对象(如 SkeletonAnimationState)是 spine-cpp API 到 Swift 的 1:1 翻译. 因此, 你可以将通用 Spine 运行时指南 中的内容几乎完全应用于你的 Swift 代码.

然而, 由于 spine-cpp 桥接的本质, 存在一些限制:

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

Objective-C 支持

spine-ios 通过适当的桥接提供完整的 Objective-C 兼容性:

在 Objective-C 中使用 spine-ios

  1. 在 Objective-C 文件中导入 SpineiOS 模块:
objc
@import SpineiOS;
  1. 使用 Objective-C 类名(带 \"Spine\" 前缀):
objc
SpineUIView *spineView = [[SpineUIView alloc] initWithAtlasFileName:@"spineboy.atlas"
                                       skeletonFileName:@"spineboy-pro.skel"
                                              bundle:[NSBundle mainBundle]
                                           controller:nil
                                                mode:SpineContentModeFit
                                           alignment:SpineAlignmentCenter
                                        boundsProvider:[[SpineSetupPoseBounds alloc] init]
                                        backgroundColor:[UIColor clearColor]];
  1. Objective-C 中可用的关键类:
  • SpineUIView - 用于渲染的 UIKit 视图
  • SpineSkeletonDrawableWrapper - Drawable 包装器
  • SpineBoundsProviderSpineSetupPoseBoundsSpineRawBounds - 边界提供器

详见 SimpleAnimationViewController.m 的完整示例.

开发

对于想要从源码修改或构建 spine-ios 的开发者:

构建模块

bash
cd spine-runtimes/spine-ios

# Build SpineC (C API)
swift build --product SpineC

# Build SpineSwift (Swift API)
swift build --product SpineSwift

# Build SpineiOS (requires iOS/tvOS SDK)
# Use Xcode for SpineiOS as it requires platform-specific SDKs

运行测试

bash
cd spine-runtimes/spine-ios/test
swift build
swift run SpineTest

生成 Swift 绑定

如果你需要在修改 spine-c 后重新生成 Swift 绑定:

bash
cd spine-runtimes/spine-ios
./generate-bindings.sh

这将在 Sources/SpineSwift/Generated/ 中重新生成 Swift 包装器代码.