spine-android 运行时文档

Licensing

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

简介

spine-android 运行时基于 spine-libgdx 构建. 它支持加载、播放和操作 Spine 创建的动画.

安装

它最低支持的 SDK 版本为 24 (Android 7.0, Nougat). 请确保您的项目至少为该 SDK 版本, 以便利用 Spine 的功能.

如需在你的 Android 应用中使用 spine-android, 首先应将 mavenCentral 和 SonaType 快照仓库添加进你的 Gradle 项目中:

gradle
repositories {
   mavenCentral()
   maven {
      url = uri("https://oss.sonatype.org/content/repositories/snapshots")
   }
}

接下来需要将 spine-android 依赖项添加到应用的 build.gradle 文件中:

gradle
dependencies {
   implementation(""com.esotericsoftware.spine:spine-android:4.2.+"")
}

该配置将拉取与 Spine Editor 4.2 兼容的最新 spine-android. 一般来说拉取的就是 spine-runtimes 仓库中最新提交的快照. 可以认为我们的快照是稳定版本, 且保持经常更新. 如果你仍然希望或需要使用发布版本而非快照版本, 你可以在 Maven Central 上找到与 Spine Editor对应的最新发布版本.

应确保spine-haxe的 major.minor 版本与用来导出资产的Spine编辑器的 major.minor 版本一致. 更多详情请见 同步版本号 一节.

示例

spine-android 运行时内置了数个用于功能展示的示例. 你可以按照以下步骤运行示例项目:

  1. 安装 Android Studio
  2. 克隆 spine-runtimes 仓库: git clone https://github.com/esotericsoftware/spine-runtimes
  3. 在 Android Studio 中打开 spine-android/ 文件夹.
  4. 运行 app 配置.

所有示例都是使用 Kotlin 结合 Jetpack Compose 编写的, 但 spine-android 也兼容 Java 和原生 Android 视图. 示例项目中包含了以下示例:

  • SimpleAnimation.kt: 演示了如何使用基本的 SpineViewSpineController 加载 Spine skeleton、在视图中显示它, 并播放特定动画.
  • PlayPause.kt: 演示了如何暂停和恢复动画.
  • AnimationStateEvents.kt: 演示了如何设置槽位的颜色、如何队列多个动画以及如何监听动画状态事件.
  • DebugRendering.kt: 演示了如何通过 SpineControlleronAfterPaint 回调在 skeleton 上实现自定义渲染.
  • DressUp.kt: 演示了 Spine 的皮肤功能, 以及如何将 skeleton 渲染为缩略图以用于角色创建 UI.
  • IKFollowing.kt: 演示了如何让用户通过鼠标或触摸输入拖动 skeleton 的骨骼.
  • DisableRendering.kt: 演示了当 SpineView 移出屏幕后如何停止渲染. 在需要节省 CPU/GPU 资源的时候该功能至关重要.
  • SimpleAnimationActivity.java: 演示了如何在基于 XML 的相对布局中使用 JavaSpineView.

更新 spine-android 运行时

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

只需在 build.gradle 依赖中修改 spine-android 包的版本字符串便可更新 spine-android 运行时.

注意: 如果你更改了 spine-android 包的 major.minor 版本, 则必须使用同版本的 Spine Editor 重新导出 Spine skeleton !

使用 spine-android

spine-android 运行时基于 spine-libgdx 构建, 后者依赖于 libGDX. 它支持加载、播放和操作 Spine 动画. spine-android 运行时使用 spine-libgdx, 以便轻松地在 Android 上显示 Spine skeleton 并与之交互.

资产管理

导出适用于 spine-android 的 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 中的一页就对应着这样一个文件, 而 texture atlas 则保存着 skeleton 所需的图片包.

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

可以通过如 AndroidTextureAtlas, SkeletonDataUtils, AndroidSkeletonDrawable, SpineView 的 spine-android 类来加载这些文件.

注意: 由于 Android 的技术限制, spine-android 运行时目前不支持使用预乘(pre-multiplied) alpha 导出的 atlas. Android 的渲染引擎只能确保不出现常见的非预乘 alpha 伪影.

更新 Spine 资产

在开发过程中可能需要频繁更新 Spine skeleton 数据和 texture atlas 文件. 直接覆盖这些原始文件(.json, .skel, .atlas, .png)即可完成资产更新, 具体做法是从 Spine Editor 中重新导出文件并替换 Android 项目中已有的文件.

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

核心类

spine-android API 基于 spine-libgdx 构建, 后者提供了加载、查询、修改和动画化 Spine skeleton 的核心类和算法.

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

spine-android 类

AndroidSkeletonDrawable 类将加载和更新 AndroidTextureAtlas, Skeleton, 以及 AnimationState 的功能集成到一个易于使用的类中.

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

SkeletonDataUtils 类公开了从 skeleton 文件中加载 SkeletonData 的静态函数.

SkeletonRenderer 类负责将 Skeleton 的当前 pose 转换为 SkeletonRenderer.RenderCommand 命令并将其渲染到 Canvas 上.

这些类既是 spine-android 的一部分, 也是添加了 Android 特定功能的 spine-libgdx 扩展.

spine-libgdx 类

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

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

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

spine-android 运行时正是构建在这些核心类之上.

SpineView

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

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

使用 Jetpack Compose 时, 可以在 AndroidView 中实例化 SpineView:

kotlin
AndroidView(
   factory = { context ->
      SpineView.loadFromAssets(
         "spineboy.atlas",
         "spineboy-pro.json",
         context,
         SpineController {
            it.animationState.setAnimation(0, "walk", true)
         }
      )
   }
)

在实例化过程中, SpineView 将异步加载指定的文件并从中构造底层核心类实例, 即 AndroidTextureAtlas, SkeletonData, Skeleton, AnimationStateDataAnimationState 的实例.

加载完成后, 将调用 SpineController 的构造函数回调 onInitialized, 它可以修改视图的状态, 例如设置一个或多个动画、调整骨骼层次结构或修改 skeleton 皮肤. 请参见下面文中的 SpineController 一节.

SpineView 类提供了多个静态工厂方法, 用于从不同来源加载 skeleton 和 atlas 文件:

  • SpineView.loadFromAsset() 从主包或指定的包中加载文件.
  • SpineView.loadFromFile() 从文件系统加载文件.
  • SpineView.loadFromHttp() 从 URL 加载文件.
  • SpineView.loadFromDrawable()AndroidSkeletonDrawable 构造视图. 当需要预加载、缓存和/或在 SpineView 实例间共享 skeleton 数据时, 这一选项时非常有用. 详情请参阅 DisableRendering.kt 示例.

当你需要进一步调整 Spine skeleton 在部件(widget)中如何缩放和对齐时, 可以使用 SpineView 的 setter 方法:

kotlin
val spineView = SpineView.loadFromAssets("dragon.atlas", "dragon-ess.skel", ctx, controller)
spineView.setContentMode(ContentMode.FILL)
spineView.setAlignment(Alignment.BOTTOM_CENTER)
spineView.setBoundsProvider(SkinAndAnimationBounds("flying"))

也可以使用 SpineView.Builder:

kotlin
SpineView.Builder(ctx, controller)
.setLoadFromAssets("dragon.atlas", "dragon-ess.skel")
.setContentMode(ContentMode.FILL)
.setAlignment(Alignment.BOTTOM_CENTER)
.setBoundsProvider(SkinAndAnimationBounds("flying"))
.build()

SpineView 包含一对 getter/setter 方法 isRendering/setRendering, 它可以禁用渲染. 要在 Jetpack Compose 中如 isRendering 般更新 SpineView 的属性, 可以添加 AndroidView.update 参数:

kotlin
AndroidView(
   factory = ...
   update = { spineView ->
      spineView.isRendering = isSpineBoyVisible.value
   }
)

更多详情请见 DisableRendering.kt 示例.

SpineController

SpineController 控制如何动画化和渲染 SpineView 中的 skeleton. 控制器可以通过方法注入或 SpineController.Builder 类传入一组回调. SpineView 生命周期中的特定时刻将会调用这些回调.

控制器通过返回 Spine 运行时 API 对象 (如 AndroidTextureAtlas, SkeletonData, SkeletonAnimationState) 的 getter 暴露 skeleton 状态, 使用这些对象便可调整 skeleton 状态. 更多信息请参阅 Spine 运行时指南 和相关类文档.

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

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

接下来会按需调用 onBeforeUpdateWorldTransforms() 回调, 它可以在 Skeleton.updateWorldTransform() 计算 skeleton 的当前 pose 前修改 skeleton. 可以通过 SpineController.setOnBeforeUpdateWorldTransforms()SpineController.Builder.setOnBeforeUpdateWorldTransforms() 设置该回调.

计算出当前 pose 后会按需调用 onAfterUpdateWorldTransforms() 回调, 它可以在渲染 skeleton 前进一步修改当前 pose. 这是一个手动调整骨骼位置的好时机. 可以通过 SpineController.setOnAfterUpdateWorldTransforms()SpineController.Builder.setOnAfterUpdateWorldTransforms() 设置该回调.

SpineView 渲染 skeleton 之前可以按需调用 onBeforePaint() 回调, 它可以在 Canvas 上渲染 skeleton 背景或其他下层的对象. 可以通过 SpineController.setOnBeforePaint()SpineController.Builder.setOnBeforePaint() 设置该回调.

SpineView 将当前 skeleton pose 渲染到 Canvas 后可以按需调用 onAfterPaint() 回调, 它可以在 skeleton 之上渲染其他对象. 可以通过 SpineController.setOnAfterPaint() 或者 SpineController.Builder.setOnAfterPaint() 设置该回调.

默认情况下, 视图每帧都会更新和渲染 skeleton. SpineController.pause() 方法用于暂停 skeleton 的更新和渲染. SpineController.resume() 方法则会恢复更新和渲染. SpineController.isPlaying() 的 getter 会返回当前播放状态. 更多详细信息请参阅 PlayPause.kt 示例.

AndroidSkeletonDrawable

AndroidSkeletonDrawable 将加载、存储、更新和渲染 Skeleton 及其关联的 AnimationState 类等功能打包到一个易于使用的类中. 该类可用作实现自定义视图的基类. SpineView 通过 AndroidSkeletonDrawable 实例封装了 skeleton 状态.

fromAsset(), fromFile()fromHttp() 等静态函数可以从文件构造出 AndroidSkeletonDrawable. 如需在多个 AndroidSkeletonDrawable 实例间共享 AndroidTextureAtlasSkeletonData, 应该通调用构造函数来实例化多个 drawable, 并将相同的 atlas 和 skeleton 数据传入各个实例.

AndroidSkeletonDrawable 公开 SkeletonAnimationState 以便查询、修改和动画化 skeleton. 它同时公开了用于构造 skeleton 和动画状态的 AndroidTextureAtlasSkeletonData.

如需动画化 skeleton, 可以通过 AnimationState API(如 AnimationState.setAnimation()AnimationState.addAnimation())在一条或多条轨道上队列动画.

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

应用动画

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

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

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

kotlin
SpineController { initializedController ->
// Set the walk animation on track 0, let it loop
   initializedController.animationState.setAnimation(0, "walk", true)
}

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

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

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

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

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

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

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

kotlin
controller.animationStateData.setDefaultMix(0.2f)
controller.animationStateData.setMix("walk", "jump", 0.1f)

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

kotlin
val entry = controller.animationState.setAnimation(0, "walk", true)
entry.setReverse(true)

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

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

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

kotlin
controller.animationState.setEmptyAnimation(0, 0.5f)
controller.animationState.addEmptyAnimation(0, 0.5f, 0.5f)

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

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

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

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

kotlin
controller.skeleton.setToSetupPose()

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

AnimationState 事件

AnimationState 在动画播放的生命周期中会触发事件. 监听这些事件便能根据需要做出响应. Spine Runtimes API 定义了 AnimationState.AnimationStateListener 接口, 其中包含以下事件的回调:

  • start: 当动画开始时触发.
  • interrupt: 当清空动画轨道或设置了新动画时触发.
  • end: 当不再应用动画时触发.
  • dispose: 当销毁动画的轨道条目时触发.
  • complete: 当动画播放完一个循环时触发.
  • event: 在发生用户自定义的 事件 时触发.

AnimationState 上注册一个 AnimationState.AnimationStateListener 回调便可监听所有动画的事件; 也可以用 AnimationState.TrackEntry 来监听某个队列中的动画:

kotlin
val entry = controller.animationState.setAnimation(0, "walk", true)
entry.setListener(object : AnimationState.AnimationStateListener {
   override fun start(entry: AnimationState.TrackEntry?) {}

   override fun interrupt(entry: AnimationState.TrackEntry?) {}

   override fun end(entry: AnimationState.TrackEntry?) {}

   override fun dispose(entry: AnimationState.TrackEntry?) {}

   override fun complete(entry: AnimationState.TrackEntry?) {}

   override fun event(entry: AnimationState.TrackEntry?, event: Event?) {
      if (event != null) {
         print("User defined event: ${event.data.name}");
      }
   }
})
controller.animationState.addListener(object : AnimationState.AnimationStateListener {
   override fun start(entry: AnimationState.TrackEntry?) {}

   override fun interrupt(entry: AnimationState.TrackEntry?) {}

   override fun end(entry: AnimationState.TrackEntry?) {}

   override fun dispose(entry: AnimationState.TrackEntry?) {}

   override fun complete(entry: AnimationState.TrackEntry?) {}

   override fun event(entry: AnimationState.TrackEntry?, event: Event?) {
      if (event != null) {
         print("Animation state event $event")
      }
   }
})

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

皮肤

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

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

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

kotlin
val data = controller.skeletonData
val skeleton = controller.skeleton
val customSkin = Skin("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.setSkin(customSkin)
skeleton.setSlotsToSetupPose()

构造函数 Skin() 可以创建自定义皮肤.

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

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

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

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

设置骨骼变换

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

在 Spine Editor 中创作 skeleton 时, skeleton 使用的是所谓的 skeleton 坐标系. 这个坐标系可能与用来渲染 skeleton 的 SpineView 坐标系并不一致. 因此触控操作使用的 SpineView 坐标需要转换为 skeleton 坐标系才便于使用(例如, 需要用户通过触控操作来移动骨骼).

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

请参阅 IKFollowing.kt 示例以了解更多信息.

使用 SkeletonRenderer 渲染

如果你需要在 SpineView 外渲染 skeleton, 比如将其渲染到 CavasBitmap, 那么可以使用 SkeletonRenderer.

使用渲染方法 SkeletonDrawable.render() 创建一个 SkeletonRenderer.RenderCommand 对象列表便可渲染 skeleton 的当前 pose. 通过这些对象, 使用 SkeletonDrawable.renderToCanvas() 可以将 skeleton 渲染到 Canvas, 而使用 SkeletonDrawable.renderToBitmap() 则会渲染到 Bitmap.

访问 Spine 运行时 API

spine-android API 基于 spine-libgdx 构建, 因此你可以使用其完整 API. 请参阅 Spine 运行时指南 以获取更多信息.