spine-flutter 运行时文档

Licensing

将官方的Spine Runtime整合到你的应用程序中前请阅读 Spine 运行时许可.

入门篇

spine-flutter运行时是基于spine-cpp运行时, 以Flutter FFI插件的形式实现的. 它支持Flutter所支持的所有平台(桌面、Android、iOS和Web), 并支持除tint black外的全部Spine功能.

安装

spine-flutter支持Flutter 3.10.5之后的所有版本. 要在Flutter项目中使用spine-flutter, 请在项目的pubspec.yaml文件中添加以下依赖关系(dependency):

yaml
dependencies:
...
spine_flutter: ^4.1.1

应确保spine-flutter的major.minor版本与导出资产的Spine编辑器中的major.minor版本一致. 更多信息请参阅Spine 版本控制指南.

在你的main()函数的开头添加以下两行, 以初始化 spine-flutter 运行时:

dart
void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   await initSpineFlutter(enableMemoryDebugging: false);
   ...
}

注意: main()方法必须为async.

Web部署依赖于CanvasKit, 截止本文撰写时, 将其加入Web部署的额外开销约 2MB. 您可以像这样为Web部署添加Canvaskit:

flutter build web --web-renderer canvaskit

有关 Web 渲染器 的更多信息, 请参阅 Flutter 文档.

运行时示例

spine-flutter 运行时包括几个展示其功能的示例.

若你的spine_flutter软件包是直接下载自pub.dev, 那么按照以下步骤运行位于 example/ 文件夹中的示例项目即可:

bash
cd path/to/downloaded/spine_flutter
cd example
flutter run

如若不然, 你需要按照以下步骤运行示例项目:

  1. 安装Flutter SDK, 然后运行flutter doctor, 它会指示你安装其他依赖项.
  2. 克隆 spine-runtimes 仓库: git clone https://github.com/esotericsoftware/spine-runtimes
  3. 运行spine-flutter/文件夹中的setup.sh脚本. 在 Windows 下, 可以使用 Git for Window 里的Git Bash来运行setup.sh Bash脚本.

然后, 你可以在支持Flutter的集成开发环境或编辑器, 如 IntelliJ IDEA/Android StudioVisual Studio Code中打开spine-flutter项目以查看并运行示例.

当然, 也可以用命令行直接运行示例项目.

项目中包含以下示例:

更新spine-flutter运行时

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

要更新 spine-flutter 运行时, 只需修改 pubspec.yaml 中的 spine_flutter 软件包版本号即可.

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

Using spine-flutter

spine-flutter 运行时是基于通用spine-cpp运行时的Dart FFI 封装器(wrapper), 支持加载、回放和修改用 Spine 创建的动画. spine-flutter 运行时将几乎全部的 spine-cpp API 都公开为原生化的Dart类代码, 并提供了 Flutter 和 Flame 的专用类, 以便轻松显示 Spine skeleton并与之交互.

Spine-flutter 运行时支持除tint black外的全部 Spine 功能.

管理资产

为spine-flutter导出资产

请参照Spine用户指南中的说明以了解如何:

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

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

  1. 包含skeleton和动画数据的 skeleton-name.jsonskeleton-name.skel 文件.
  2. skeleton-name.atlas, 包含texture atlas信息.
  3. 一个或多个 .png 文件, 每个文件代表texture atlas中的一页, 每页中都打包了skeleton将使用的图像.

注意: 与 JSON 导出相比, 你也许会更喜欢二进制skeleton导出, 因为它们体积更小, 加载速度更快.

可通过如 AtlasSkeletonDataSkeletonDrawableSpineWidget等spine-flutter 类加载文件.

注意: 由于 Flutter 的技术限制, spine-flutter 运行时目前不支持使用预乘 alpha 导出的atlas. 但Flutter 的渲染引擎可确保常规的非预乘 Alpha 伪影不会出现.

更新Spine资产

在开发过程中, 你可能会经常更新 Spine skeleton数据和texture atlas文件. 只需从 Spine 编辑器重新导出并替换 Flutter 项目中的已有文件, 即可覆盖这些源文件(.json, .skel, .atlas, .png)以实现资产更新.

确保 spine-flutter 的major.minor版本与用于导出的 Spine 编辑器的major.minor版本一致. 更多信息, 请参阅 Spine 版本控制页.

核心类(Core classes)

spine-flutter API 基于通用 spine-cpp 运行时, 它提供了与平台无关的核心类和算法, 用于加载、查询、修改 Spine skeleton并呈现skeleton动画. 核心类通过 Dart FFI 封装, 并作为原生化的 Dart 类对外公开.

我们将在本节简要讨论你在日常使用 spine-flutter 时会遇到的最重要的核心类. 请查阅 Spine 运行时指南 详细了解 Spine 运行时架构、核心类和 API 用例.

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

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

Skeleton类存储了一个skeleton实例, 该skeleton由SkeletonData实例创建. Skeleton存储其当前姿势, 即当前配置的骨骼位置以及槽位、附件和活动皮肤. 当前姿势可以通过手动修改骨骼层次结构来计算得出, 更正常的做法是通过AnimationState应用动画来得到当前姿势.

AnimationState类负责跟踪哪些动画应该应用于Skeleton, 根据上一帧和当前渲染帧的间隔时间推进或混合(mix)这些动画, 并将动画应用到skeleton实例, 从而设置其当前姿势. AnimationState会查询AnimationStateData实例来获取动画间的混合时间, 若一对动画间没有混合时间, 则会采用默认混合时间.

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

SpineWidget

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

SpineWidget是一个StatefulWidget, 它负责加载和显示Spine Skeleton. 该部件至少需要知道从何处加载skeleton和atlas文件, 并且必须接收一个SpineWidgetController实例, 该实例负责修改小部件(widget)的状态, 比如设置动画或更改Skeleton的皮肤.

最简单的用例是, 在另一个小部件的build()方法中实例化SpineWidget:

dart
@override
Widget build(BuildContext context) {
   final controller = SpineWidgetController(onInitialized: (controller) {
    // Set the walk animation on track 0, let it loop
    controller.animationState.setAnimationByName(0, "walk", true);
   });

   return Scaffold(
    appBar: AppBar(title: const Text('Simple Animation')),
    body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller)
   );
}

实例化后, SpineWidget 将异步加载指定文件并根据文件构建底层核心类实例, 即AtlasSkeletonDataSkeletonAnimationStateDataAnimationState实例.

加载完成后将调用SpineWidgetController, 通过它来修改小部件的状态, 如设置一个或多个动画、操作骨骼层次结构或修改骨骼的皮肤. 请参阅下文 SpineWidgetController 的章节.

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

  • SpineWidget.fromAsset()从根包(bundle)或指定的包中加载文件.
  • SpineWidget.fromFile() 从文件系统加载文件.
  • SpineWidget.fromHttp() 从 URL 加载文件.
  • SpineWidget.fromDrawable()SkeletonDrawable构造一个小部件. 当skeleton数据需要在SpineWidget实例间预载、缓存和/或共享时请使用它. 请参阅下文的 "预载和共享Skeleton数据" 一节.

所有工厂方法均包含可选参数, 可进一步定义 Spine skeleton在小部件中的组合和放置方式, 以及小部件的尺寸:

  • fit, BoxFit, skeleton被置为适配小部件内的尺寸.
  • alignment, Alignment, 在小部件内对齐skeleton.
  • BoundsProvider, 将会计算适配和对齐时skeleton边界框尺寸. 默认情况下会使用骨骼的setup pose边界框. 请参阅SetupPoseBoundsRawBoundsSkinAndAnimationBounds 以获取更多信息.
  • sizedByBounds, 可定义是根据 BoundsProvider 计算出的边界框来调整小部件尺寸, 还是根据其父级小部件来调整尺寸.

SpineWidgetController

SpineWidget控制器控制着SpineWidget的skeleton动画和渲染方式. 该控制器提供了一组可选的回调作为构造函数参数, 这些参数会在 SpineWidget 生命周期的特定时刻被调用.

控制器通过返回Spine运行时API对象(如Atlas, SkeletonData, SkeletonAnimationState)的获取器公开skeleton状态, 可通过这些对象对状态进行操作. 更多信息请参阅Spine 运行时指南类代码.

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

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

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

计算了当前姿势后, 会调用可选的onAfterUpdateWorldTransforms()回调, 它能在渲染skeleton前进一步修改当前姿势. 这是手动放置骨骼的好时机.

SpineWidget渲染skeleton之前, 会调用可选的onBeforePaint()回调, 它能在Canvas上渲染背景或其他应位于skeleton后面的对象.

SpineWidgetCanvas渲染了skeleton的当前姿势后, 会调用可选的onAfterPaint()回调, 它能在skeleton之上渲染其他对象.

默认情况下, 小部件每帧都会更新和渲染skeleton. 可以使用 SpineWidgetController.pause() 方法暂停更新和渲染skeleton, 而SpineWidgetController.resume()方法可恢复skeleton的更新和渲染. SpineWidgetController.isPlaying() 获取器(getter)会报告当前的播放状态. 用例请参阅 example/lib/animation_state_events.dart 示例.

SkeletonDrawable

SkeletonDrawableSkeleton的加载、存储、更新和渲染及其关联的AnimationState封装到了一个简单易用的类里. 该类是实现自定义小部件的基础. SpineWidget通过一个SkeletonDrawable实例封装了它所呈现的skeleton的状态.

使用fromAsset()fromFile()fromHttp()方法可从文件资产构建SkeletonDrawable. 要在多个 SkeletonDrawable 实例间共享AtlasSkeletonData, 可通过构造函数实例化drawables类, 并向每个实例传入相同的atlas和skeleton数据.

SkeletonDrawable会公开SkeletonAnimationState, 以便查询、修改skeleton和播放skeleton动画. 它还公开了AtlasSkeletonData, skeleton和动画状态就是根据这些数据构造的.

要让skeleton动起来, 可通过 AnimationState API(如 AnimationState.setAnimation()AnimationState.addAnimation()) 在一个或多个轨道上队列动画来实现.

要更新动画状态、将其应用到skeleton并更新当前skeleton姿势, 请调用 SkeletonDrawable.update() 方法, 并为其提供一个以秒为单位的延迟时间来推进动画.

要渲染skeleton的当前姿势, 请使用渲染方法 SkeletonDrawable.render()SkeletonDrawable.renderToCanvas()SkeletonDrawable.renderToPictureRecorder()SkeletonDrawable.renderToPng()SkeletonDrawable.renderToRawImageData().

SkeletonDrawable存储在本地(native)堆上分配的对象. 若不再需要SkeletonDrawable, 则需通过调用SkeletonDrawable.dispose()手动销毁(dispose)本地对象, 否则将导致本地内存泄漏.

注意: 使用SpineWidget时不必手动销毁部件使用的 SkeletonDrawable. 小部件将在其销毁自身时自动销毁SkeletonDrawable.

应用动画

SpineWidget渲染的skeleton应用动画是靠SpineWidgetController回调中的AnimationState完成的.

注意: 请参阅《Spine 运行时指南》中的应用动画一章了解更多深入信息, 特别是关于动画轨道和队列动画的信息.

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

dart
final controller = SpineWidgetController(onInitialized: (controller) {
// Set the walk animation on track 0, let it loop
controller.animationState.setAnimationByName(0, "walk", true);
});

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

你也可以队列多个动画:

dart
controller.animationState.setAnimationByName(0, "walk", true);
controller.animationState.addAnimationByName(0, "jump", 2, false);
controller.animationState.addAnimationByName(0, "run", 0, true);

addAnimationByName()的第一个参数是轨道号. 第二个参数是动画名称. 第三个参数指定了延迟时间(以秒为单位), 延迟后该动画会替换该轨道的前一个动画. 最后一个参数定义了是否循环播放动画.

在上文的示例中, 首先播放的是"walk"(行走)动画. 2 秒后, 播放一次"jump"(跳跃)动画, 然后过渡到"run"(跑动)动画, 最后循环播放"run"动画.

从一个动画过渡到另一个动画时, AnimationState 会在一段时间内混合(mix)播放动画. 混合时长定义在 AnimationStateData 实例中, AnimationState将从中获取混合时长.

AnimationStateData实例也可通过控制器调用. 你可以设置默认混合时长, 也可以单独设置某对动画的混合时长:

dart
controller.animationStateData.setDefaultMix(0.2);
controller.animationStateData.setMixByName("walk", "jump", 0.1);

设置或添加动画时, 会返回一个 TrackEntry 对象, 通过该对象可以进一步定制动画的播放. 例如可以反转动画播放:

dart
final entry = controller.animationState.setAnimationByName(0, "walk", true);
entry.setReverse(true);

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

注意: 请勿在您使用它们的函数外保存 TrackEntry 实例. 轨道条目会在内部重复使用, 因此一旦其代表的动画完成播放, 轨道条目就会失效.

您可以在动画轨道上设置空动画或队列空动画, 以便将skeleton重置回setup pose:

dart
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.setToSetupPose():

dart
controller.skeleton.setToSetupPose();

这将把骨骼和槽位都重置为setup pose中的设置. 使用 Skeleton.setSlotsToSetupPose() 可只将槽位重置为setup pose设置.

AnimationState 事件

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

  • Start: 动画开始时触发.
  • Interrupted: 动画轨道被清空或设置了新动画时触发.
  • Completed: 动画播放完成了一个循环时触发.
  • Ended: 不再应用该动画时触发.
  • Disposed: 该动画的轨道条目被销毁时触发.
  • Event: 用户自定义事件触发时触发.

要接收事件, 可以注册一个 AnimationStateListener回调, 该回调可以是接收所有动画事件的AnimationState监听器, 也可以是订阅了队列中某个待播动画的TrackEntry监听器:

dart
let entry = controller.animationState.setAnimationByName(0, "walk", true);
entry.setListener((type, trackEntry, event) {
if (type == EventType.event) {
    print("User defined event: ${event?.getData().getName()}");
}
});

controller.animationState.setListener((type, trackEntry, event) {
print("Animation state event $type");
});

请参见 example/lib/animation_state_events.dart 示例.

皮肤

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

许多应用和游戏都允许用户用头发、眼睛、裤子或耳环、包包等配饰等多个部件(item)创建自定义形象. Spine中可通过皮肤混搭来实现该功能.

可以用其他皮肤创建自定义皮肤:

dart
final data = controller.skeletonData;
final skeleton = controller.skeleton;
final 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上.

注意: Skin封装了一个底层的 C++ 对象. 当它不再使用时需调用 Skin.dispose() 手动销毁它.

详见 example/lib/dress_up.dart示例, 它也演示了如何通过SkeletonDrawable来渲染皮肤的缩略图预览.

设置骨骼变换

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

在 Spine 编辑器中创建skeleton时, skeleton是定义于skeleton坐标系中的. 该坐标系可能与渲染skeleton的SpineWidget坐标系不一致. 因此在使用触控输入的SpineWidget坐标需要转换为skeleton坐标, 比如允许用户通过触控来移动骨骼这类场景.

SpineWidgetController提供了toSkeletonCoordinates()方法, 该方法接相对于skeleton所在SpineWidget的坐标偏移量(Offset), 并将其转换为skeleton坐标.

请参阅 example/lib/ik_following.dart 示例.

Flame集成(Integration)

/img/spine-runtimes-guide/spine-flutter/flame.png

spine-flutter 包含了一个示例, 演示了如何在 Flame Engine 中加载和渲染 Spine skeleton. 请参见源文件 example/lib/flame_example.dart .

该示例中的SpineComponent类是对Flame中PositionComponent类的扩展. SpineComponent类可以通过静态方法SpineComponent.fromAsset()或构造函数实例化.

当skeleton和atlas数据无需与其他组件共享时, 静态方法可用作快速、一次性的加载机制. 该示例包含一个名为SimpleFlameExampleFlameGame实现, 它演示了以这种简单方式可将Spine skeleton作为Flame游戏的一部分显示在屏幕上.

通过构造函数创建SpineComponent后, 就可以使用SkeletonDrawable对数据加载和共享进行更精细的控制. 例如你可以预加载skeleton数据和atlas, 然后在多个SpineComponent实例中共享. 这样既能提高内存使用率又能提高渲染性能, 因为数据是共享的, 渲染也可以合批处理. 有关示例, 请参阅名为PreloadAndShareSpineDataExampleFlameGame实现.

虽然 Flame 无法确知某个组件何时达到生命周期终点, 但SpineComponent会处理在其生命周期结束时需要释放的本地资源. 因此, 如果某个SpineComponent已不再使用, 你需要手动调用 SpineComponent.dispose(). 如果 SpineComponent 是通过 SkeletonDrawable 构建的, 你可能还需要如 PreloadAndShareSpineDataExample 示例中所示的那样, 手动地销毁由其构建的 SkeletonDataAtlas.

访问Spine运行时API

spine-flutter将几乎所有的Spine运行时API都映射为了Dart代码. 如Skeleton或者AnimationState这样由SpineWidgetControllerSkeletonDrawable返回的对象, 均为spine-cpp API到Dart的1:1直译. 因此你可以将Spine运行时指南中几乎所有通用操作以Dart代码实现.

由于 spine-cpp 与 Dart FFI 桥接的实现方式会产生一些限制:

  • 返回的数组(array)或映射(map)均为内部数组的副本. 修改它们不会有任何效果. 然而返回的 Float32ListInt32List 实例是底层原生内存值的封装, 因此可以用来修改底层原始(native)数据.
  • 不能直接创建、添加或删除骨骼、槽位和其他 Spine 对象.
  • 时间轴的 C++ 类层次结构没有暴露给Dart.