spine-phaser 运行时文档

Licensing

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

入门篇

spine-phaser 运行时基于spine-ts core实现, 它是对渲染器无关的 Spine Runtimes 核心 API 的 TypeScript 实现. spine-phaser 运行时支持了Phaser 3所支持的全部平台, 支持用Canvas APIs进行渲染和用WebGL进行渲染.

使用 Phaser WebGL 渲染器时, spine-phaser 运行时支持全部Spine功能. 而使用 Phaser Canvas 渲染器时, spine-phaser 运行时不支持网格tint blackblend模式.

安装

要在 Phaser 项目中使用 spine-phaser 运行时, 必须首先引入(include)其源代码.

原生JavaScript

在原生 JavaScript 中, 请使用 script 标记来引入来自 unpkg 的 spine-phaser 运行时:

<script src="https://unpkg.com/@esotericsoftware/spine-phaser@4.1.*/dist/iife/spine-phaser.js"></script>

注意: 务必确保 spine-phaser 的major.minor版本与Spine编辑器的major.minor版本一致. 请参阅同步版本了解更多.

下一步, 将 Spine 场景插件添加到 Phaser 游戏配置中:

const config = {
...
plugins: {
    scene: [
       { key: "spine.SpinePlugin", plugin: spine.SpinePlugin, mapping: "spine" }
    ]
}
}
new Phaser.Game(config);

至此项目中的所有场景均可使用 spine-phaser 运行时了.

请查看 basic-vanilla-js-example.html, 了解完整的示例.

spine-phaser 软件包还提供了用于调试的源映射文件, 以及 spine-phaser 的精简版, 只需在 unpkg URL 中将.js文件后缀替换为.min.js即可引入.

若要在本地使用, 请按照 spine-ts README.md 中的说明构建 spine-phaser.js.

使用NPM或Yarn

使用 NPM 或 Yarn 进行依赖管理时, 如常添加 spine-phaser 即可:

npm install @esotericsoftware/spine-phaser@~4.1.0

注意: 请确保 spine-phaser 的 major.minor版本与您要导出的 Spine 编辑器的 major.minor版本一致. 请参阅同步版本了解更多.

下一步是把 Spine 场景插件添加到 Phaser 游戏配置中:

import Phaser from "phaser"
import {SpinePlugin} from "@esotericsoftware/spine-phaser"

const config = {
...
plugins: {
    scene: [
       { key: "spine.SpinePlugin", plugin: SpinePlugin, mapping: "spine" }
    ]
}
}
new Phaser.Game(config);

现在项目中的所有场景现在都可以使用 spine-phaser 运行时了.

请访问 spine-phaser esbuild/TypeScript 项目 以获取最简示例.

本模块包还包含源映射文件以及 d.ts 类型声明(typings)文件, 以帮助调试和开发.

示例

spine-phaser 运行时包含多个示例项目, 用于演示其功能.

要在本地运行这些示例请按以下步骤操作:

  1. 在操作系统上安装 Git 和 Node.js.
  2. 克隆 spine-runtimes 仓库: git clone https://github.com/esotericsoftware/spine-runtimes
  3. 在终端中导航到 spine-runtimes/spine-ts 目录, 然后执行 npm install & npm run dev.

该命令会构建 spine-phaser 运行时, 然后打开浏览器, 显示所有基于 spine-ts 运行时的示例项目列表.

点击你感兴趣的 spine-phaser 示例, 可在 spine-runtimes/spine-ts/spine-phaser/example文件夹中查看对应项目的代码.

更新 spine-phaser 运行时

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

要更新原生JavaScript中的 spine-phaser 运行时, 请更改src 属性或 script 标记中的版本字符串, 它们用于从 unpkg 获取 spine-phaser.

要在使用 NPM 或 Yarn 管理依赖关系时更新 spine-phaser 运行时, 直接更改package.json文件中的版本号字符串即可.

注意: 如果更改了 spine-phaser 软件包的major.minor版本, 就必须用同major.minor版本的Spine 编辑器重新导出 Spine skeleton!更多信息请参阅同步版本.

Using spine-phaser 使用 spine-phaser

使用 Phaser WebGL 渲染器时, spine-phaser 运行时支持全部Spine功能. 而使用 Phaser Canvas 渲染器时, spine-phaser 运行时不支持网格tint blackblend模式.

管理美术资产

为spine-phaser导出资产

https://esotericsoftware.com/img/spine-runtimes-guide/spine-ue4/export.png

请按Spine用户指南中的步骤操作:

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

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

https://esotericsoftware.com/img/spine-runtimes-guide/spine-ue4/exported-files.png

  1. skeleton-name.jsonskeleton-name.skel, 其中包含skeleton和动画数据(JSON 或二进制格式).
  2. skeleton-name.atlas, 其包含了texture atlas信息.
  3. 一个或多个 .png 文件, 每个文件表示了texture atlas中的一页.

注意: 与导出的JSON格式相比, 导出的二进制skeleton体积更小, 加载速度也更快.

在服务器提供这些文件的访问时, 请确保服务器配置了正确的 MIME 类型.

更新Spine资产

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

请确保 spine-phaser 的 major.minor版本与您要导出的 Spine 编辑器的 major.minor版本一致. 请参阅同步版本了解更多.

核心类

spine-phaser API的构建基于通用的TypeScript spine-core 运行时, 该运行时提供了与平台无关的核心类和算法, 用于加载、查询、修改 Spine skeleton并将其动画化.

在此将简要讨论日常使用 spine-phaser 时将涉及的重要核心类. 请同时查阅 Spine 运行时指南 详细了解 Spine 运行时架构、核心类和 API 使用方法.

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

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

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

AnimationState类负责跟踪应应用于skeleton的动画, 根据上一帧和当前渲染帧之间的时间提前和mix这些动画, 并将动画应用于skeleton实例, 从而设置其当前姿势. AnimationState会查询AnimationStateData实例以获取动画间的mix时间, 如果一对动画之间没有设置mix时间, 则会使用默认mix时间.

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

Spine场景插件

spine-phaser 场景插件添加了将导出的.json.skel.atlas文件(预)加载进场景的 LoaderPlugin (Scene.load) 的功能. 它提供了访问原始skeleton数据和texture atlas的获取器(getter).

该场景插件用函数扩展了场景的 GameObjectFactory (Scene.add)GameObjectCreator (Scene.make)类, 这样就能直接从加载的skeleton数据和atlas文件创建SpineGameObject实例.

加载Spine资产

Spine资产, 如skeleton数据.json/.skel文件或.atlas文件, 是通过SpinePlugin附加到Phaser SceneLoaderPlugin (Scene.load)类的附加函数加载的.

  • spineBinary(key: string, url: string, xhrSettings: Phaser.Loader.XHRSettingsObject): 从url加载二进制skeleton.skel文件, 然后通过key访问. XHRSettingsObject为可选参数.
  • spineJson(key: string, url: string, xhrSettings: Phaser.Loader.XHRSettingsObject): 从url加载 JSON skeleton.json文件, 然后通过key访问. XHRSettingsObject为可选参数.
  • spineAtlas(key: string, url: string, premultipliedAlpha: boolean, xhrSettings: Phaser.Loader.XHRSettingsObject):从url加载texture atlas的.atlas文件及其相关的.png 页文件, 然后通过key访问. premultipliedAlpha为可选参数, 用于指示atlas是否使用了premultiplied-alpha. 默认使用存储在 .atlas 文件中的值. XHRSettingsObject为可选参数.

假设您已将skeleton数据导出成名为 skeleton.skel 的二进制skeleton文件, 并将图集导出成名为 skeleton.atlas 的文件, 同时导出了一个对应的 skeleton.png 文件, 那么可以在场景的 preload() 函数中以如下方式加载资产:

function preload() {
this.load.spineBinary("skeleton-data", "path/to/skeleton.skel");
this.load.spineAtlas("skeleton-atlas", "path/to/skeleton.atlas");
}

preload()函数从skeleton.skel文件加载SkeletonData, 并将其缓存在skeleton-data键下. 它还会从skeleton.atlas文件加载TextureAtlas, 并从相应的skeleton.png文件加载texture. Atlas缓存在键 skeleton-atlas 中. 每个texture atlas页的图像均采用透明加载机制, 无需显式加载.

预加载完成后, 可通过 Scene.spine.getAtlas(atlasKey) 来访问 TextureAtlas . 同样, 也可以通过 Scene.spine.getSkeletonData(dataKey, atlasKey) 访问原始的 SkeletonData . 请注意第二个参数: SkeletonData 实例只能在同时持有该skeleton数据的atlas时创建.

Skeleton数据和atlas本身不能进行动画或渲染, 而是使用一个从Skeleton数据和atlas构建的SpineGameObject. SpineGameObject实例可以共享相同的skeleton数据和atlas.

创建SpineGameObject实例

当加载了skeleton数据和相应的atlas, 就可以创建一个SpineGameObject, 并可通过被SpinePlugin添加到场景的GameObjectFactory (Scene.add) 类和GameObjectCreator (Scene.make)类中的spine()函数, 将SpineGameObject添加到当前场景中. 可以在场景的 create() 函数中使用它们:

function create() {
// Create a SpineGameObject through the GameObjectFactory and add it to the scene
const spineObject = this.add.spine(400, 500, "skeleton-data", "skeleton-atlas");

// Create a SpineGameObject through the GameObjectCreator. It is not automatically
// added to the scene.
const spineObject2 = this.make.spine({
    x: 200, y: 500,
    dataKey: "skeleton-data", atlasKey: "skeleton-atlas"
});

// Manually add the game object to the scene
this.add.existing(spineObject2);
}

GameObjectFactory上的spine()函数将对象位置、skeleton数据的键值(key)和atlas的键值作为参数. 该函数将自动把对象加入场景.

GameObjectCreator上的spine()函数将SpineGameObjectConfig作为参数, 该参数还指定了位置、数据的键值和atals的键值. 但该函数不会自动将对象加入场景.

默认情况下, SpineGameObject的尺寸取决于其setup pose的边界框尺寸. 向任意spine()函数传入一个可选的 SpineGameObjectBoundsProvider参数即可更改该行为.

边框提供器(bounds provider)会计算 Spine 游戏对象(game object)的边框尺寸. 默认使用的是 SetupPoseBoundsProvider, 它会根据skeleton在setup pose中的边界来计算边界框尺寸.

另一个边框提供器是SkinsAndAnimationBoundsProvider, 它会根据给定皮肤和动画的最大边界来计算边界框尺寸.

你也可以通过实现 SpineGameObjectBoundsProvider接口来向spine()函数传入自定义的边框提供器.

SpineGameObject

SpineGameObject是一种 Phaser GameObject, 它将存储、更新和渲染Skeleton及其相关AnimationState的功能捆绑在一起. 如上节所述, SpineGameObject实例基于skeleton数据和atlas创建. SkeletonAnimationState可分别通过skeletonanimationState字段访问.

SpineGameObject 每帧都会::

  • 更新AnimationState
  • AnimationState 应用于 Skeleton
  • 更新Skeleton的世界变换, 得出新姿势
  • 以当前姿势渲染Skeleton

应用动画

通过AnimationState可将动画应用于由SpineGameObject呈现的skeleton.

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

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

spineObject.animationState.setAnimation(0, "walk", true);

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

你可以将多个动画队列起来:

spineObject.animationState.setAnimation(0, "walk", true);
spineObject.animationState.addAnimation(0, "jump", 2, false);
spineObject.animationState.addAnimation(0, "run", 0, true);

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

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

从一个动画过渡到另一个动画时, AnimationState 会在某段时间内mix(混合)动画, 该时间称为mix时间. 这些mix时间定义在一个 AnimationStateData 实例中, 而AnimationState会从中获取mix时间.

也可通过 SpineGameObject 获取 AnimationStateData 实例. 你既可以设置一个默认的mix时间, 也可以单独设置某对动画的mix时间:

spineObject.animationStateData.setDefaultMix = 0.2;
spineObject.animationStateData.setMix("walk", "jump", 0.1);

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

const entry = spineObject.animationState.setAnimation(0, "walk", true);
entry.reverse = true;

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

注意: 小心不要函数作用域外保留 TrackEntry 实例. 轨道条目会在内部重复使用, 因此一旦发生轨道条目的 销毁(dispose)事件, 轨道条目就会失效.

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

spineObject.animationState.setEmptyAnimation(0, 0.5);
spineObject.animationState.addEmptyAnimation(0, 0.5, 0.5);

setEmptyAnimation()的第一个参数指定轨道号. 第二个参数指定mix持续时间(以秒为单位), 这段时间可以mix掉之前的动画并过渡到空动画.

addEmptyAnimation()的第一个参数指定轨道号. 第二个参数指定mix持续时间(以秒为单位). 第三个参数则是延迟时间(以秒为单位), 轨道上的上一个动画在延迟后将mix到空动画.

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

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

spineObject.skeleton.setToSetupPose();

这将把skeleton和槽位都重置为setup pose. 使用 Skeleton.setSlotsToSetupPose() 则仅重置槽位为setup pose.

AnimationState事件

AnimationState 将在动画播放的生命周期中触发事件. 你可以监听这些事件来按需响应. Spine Runtimes API 定义了以下几种事件类型:

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

要接收事件, 你可以在AnimationState上注册一个AnimationStateListener回调函数接收所有动画的事件, 也可以注册到TrackEntry上来监听队列中某个动画的事件:

spineObject.animationState.addListener({
   start: (entry) => log(`Started animation ${entry.animation.name}`),
   interrupt: (entry) => log(`Interrupted animation ${entry.animation.name}`),
   end: (entry) => log(`Ended animation ${entry.animation.name}`),
   dispose: (entry) => log(`Disposed animation ${entry.animation.name}`),
   complete: (entry) => log(`Completed animation ${entry.animation.name}`),
   event: (entry, event) => log(`Custom event for ${entry.animation.name}: ${event.data.name}`)         
})

trackEntry.listener = {
   event: (entry, event) => log(`Custom event for ${entry.animation.name}: ${event.data.name}`)
}

参见 events-example.html 了解更多示例.

皮肤

许多应用程序和游戏都允许用户用头发、眼睛、裤子或耳环、包包等配饰等部件来创建自定义形象. 而用Spine可以通过皮肤混搭来实现这一功能.

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

const skeletonData = spineObject.skeleton.data;
const skin = new spine.Skin("custom");
skin.addSkin(skeletonData.findSkin("skin-base"));
skin.addSkin(skeletonData.findSkin("nose/short"));
skin.addSkin(skeletonData.findSkin("eyelids/girly"));
skin.addSkin(skeletonData.findSkin("eyes/violet"));
skin.addSkin(skeletonData.findSkin("hair/brown"));
skin.addSkin(skeletonData.findSkin("clothes/hoodie-orange"));
skin.addSkin(skeletonData.findSkin("legs/pants-jeans"));
skin.addSkin(skeletonData.findSkin("accessories/bag"));
skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));      
spineObject.skeleton.setSkin(skin);
spineObject.skeleton.setToSetupPose();

使用Skin()构造函数创建自定义皮肤.

然后从skeleton中获取SkeletonData. 再通过SkeletonData.findSkin()按名称查找皮肤.

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

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

完整示例代码请参见 mix-and-match-example.html.

设置骨骼变换

在Spine Editor中创建skeleton时, skeleton定义skeleton世界坐标系下----即所谓的"skeleton坐标系". 该坐标系可能与 Phaser 的坐标系不一致. 因此SpineGameObject 的鼠标和触摸操作的坐标值需要转换到skeleton坐标系----比如用户可能需要通过触控来移动骨骼.

SpineGameObject提供了一个方法phaserWorldCoordinatesToBone(point: { x: number, y: number}, bone: Bone), 该方法接收一个相对于 SpineGameObject 的坐标点, 并将其转换到相对于指定骨骼的骨骼坐标系中.

反过来, 也就是从骨骼坐标系转换到 Phaser 坐标系, 则可以通过这句: SpineGameObject.skeletonToPhaserWorldCoordinates(point: { x: number, y: number}) 来实现.

完整示例代码请参见 control-bones-example.html.

访问Spine运行时API

spine-phaser 通过SpineGameObject属性skeletonanimationStateDataanimationState公开了整个spine-ts core API. 详细信息请参阅这些类的JS文档以和通用的Spine 运行时指南.

与Phaser Spine Plugin的差异

Phaser 提供了自己的 Spine Plugin, 它基于我们的 spine-ts WebGL 运行时. 我们在编写正式版 spine-phaser 运行时时已尽力与 Phaser 提供的 API 保持一致. 下文将探讨 Phaser 插件与我们 spine-phaser 官方运行时 API 间的一些差异点.

从 Spine 4.1 开始, 我们将像维护其他 Spine 运行时一样维护 spine-phaser 运行时. 这意味着每当发布新的 Spine 编辑器版本时, 您都将及时获得更新, 您还将获得下一个 Spine 编辑器版本的测试版运行时, 并及时收到所有最新的改进和错误修复. Phaser 维护者可能无法提供这样的保证, 因为 维护 Spine 插件并非其首要任务.

在 API 方面, 有一些差异应该注意.

加载skeleton和atlas数据

使用Phaser Spine插件加载skeleton及其atlas只需一步:

javascript
this.load.spine('spineboy', 'spineboy.json`, [ `spineboy.atlas` ], true);

而spine-phaser运行时中的atlas数据和skeleton数据需分开加载.

this.load.spineBinary("spineboy-data", "spineboy.skel");
this.load.spineAtlas("spineboy-atlas", "spineboy.atlas");

这样就可以在不同的skeleton中共享同一个图集. spine-phaser 运行时还支持加载 JSON 和二进制skeleton数据, 而 Phaser Spine 插件只能加载 JSON skeleton数据. 最后, 加载器方法会自动推断skeleton atlas是否使用了pre-multiplied alpha.

spine-phaser skeleton数据加载器不支持从单个 JSON 文件加载多个spine skeleton, 然而 Phaser Spine 插件支持这一功能.

创建 SpineGameObject 实例

使用 Phaser Spine 插件创建 SpineGameObject 时只会引用一个已加载资产, 并提供了许多额外的可选参数.

javascript
const spineboy = this.add.spine(400, 600, 'spineboy', 'idle', true);

spine-phaser 运行时需要指定skeleton数据和atlas的键值.

javascript
const spineboy = this.add.spine(400, 500, 'spineboy-data', "spineboy-atlas");
spineboy.animationState.setAnimation(0, "idle", true)

如上文所述, 你可以提供一个 SkeletonBoundsProvider 实例作为第 5 个参数. 但你不能通过此方法配置任何其他属性. 相反你可以直接访问 SpineGameObject 的方法和属性, 按照自己的喜好进行配置.

Spine容器

Phaser Spine 插件有一个特殊的容器类, 名为 SpineContainer. 可用于批量渲染其内含的多个 SpineGameObject 实例并据此提高性能.

而该功能对于 spine-phaser 运行时就显得毫无必要, 因为渲染器会在可能的情况下自动批处理后续的 SpineGameObject 实例, 而无关其容器父级为何.

示例项目移植

为了进一步简化从 Phaser Spine 插件到官方 spine-phaser 运行时的过渡过程, 我们已将大部分 Phaser Spine 插件示例 移植到了 spine-phaser 运行时上.