本章节为大家介绍如何在代码中使用 Galacean Spine 运行时。
无论是通过编辑器导出的项目,或者 procode 项目,都需要通过安装 @galacean/engine-spine (即Galacean Spine 运行时) 来实现 Spine 动画的加载与渲染。
npm install @galacean/engine-spine --save
安装成功后,需要在代码中引入
import { SpineAnimationRenderer } from "@galacean/engine-spine";
安装并导入 @galacean/engine-spine
后,编辑器的 resourceManager 才能识别并加载 Spine 动画资产。
Galacean spine 加载器既能加载编辑器上传的资产,也能过加载自定义上传的资产。
导出编辑器项目后,已添加至场景中的 Spine 动画,会在加载场景文件时,自动完成加载
:
// 加载场景文件时,已添加至场景中的 Spine 动画会自行完成加载
await engine.resourceManager.load({
url: projectInfo.url,
type: AssetType.Project,
})
若未添加至场景中,则需要在代码中手动加载,步骤如下:
注意:Uploadd Assets to CDN
选项,如果勾选,则是通过 cdn 链接来加载动画;如果未勾选,则是通过本地文件相对路径加载动画。
下载项目到本地后,打开 project.json 文件,找到 url 属性并打开。
如果上一步勾选了Uploadd Assets to CDN
选项,那么可以在 json 文件中找到 spine 资产链接:
如果上一步未勾选Uploadd Assets to CDN
选项,可以在本地 public 文件夹
中找到 spine 资产。加载时,使用相对路径作为链接即可。
得到 spine 的骨骼文件资产链接后,需要使用 resourceManager 进行加载。手动加载时,添加 Spine 至场景中,需要创建一个新的实体并添加 SpineAnimationRenderer 组件,代码如下:
import { SpineAnimationRenderer } from '@galacean/engine-spine';
// 加载并得到 spine 资源
const spineResource = await engine.resourceManager.load(
{
url: 'https://galacean.raptor.json', // 或者是文件的相对路径 url: '../public/raptor.json"'
type: 'spine', // 必须指定加载器类型为 spine
},
);
// 创建一个新的实体
const spineEntity = new Entity(engine);
// 添加 SpineAnimationRenderer 组件
const spine = spineEntity.addComponent(SpineAnimationRenderer);
// 设置动画资源
spine.resource = spineResource;
// 添加至场景
root.addChild(spineEntity);
如果你的 Spine 资产未通过 Galacean 编辑器进行上传,而是通过三方平台上传至 CDN,同样能够通过 Galacean Spine 运行时加载器进行加载。
const resource = await engine.resourceManager.load(
{
url: 'https://your.spineboy.json', // 自定义上传的资产
type: 'spine', // 必须指定加载器类型为 spine
},
);
加载自定义上传的资产时:
当传递参数为 url 时,需要保证 atlas 和 texture 资源与骨骼文件在相同目录下
,即:
https://your.spineboy.json
https://your.spineboy.atlas
https://your.spineboy.png
三个文件相同目录
当传递参数为 urls (多链接)时,则无需满足相同目录的条件:
const resource = await engine.resourceManager.load(
{
urls: [
'https://your.spineboy.json',
'https://ahother-path1.spineboy.altas',
'https://ahother-path2.spineboy.png',
], // 自定义上传的资产
type: 'spine',// 必须指定加载器类型为 spine
},
);
const resource = await engine.resourceManager.load(
{
urls: [
'https://your.spineboyjson',
'https://ahother-path1.spineboyatlas',
'https://ahother-path2.spineboypng',
], // 自定义上传的资产
type: 'spine',
fileExtensions: [
'json', // 指定第一个文件为 json 后缀
'atlas', // 指定第二个文件为 atlas 后缀
'png', // // 指定第三个文件为 atlas 后缀
]
},
);
加载完毕后,需要手动创建实体,并添加 SpineAnimationRenderer 组件:
import { SpineAnimationRenderer } from '@galacean/engine-spine';
const spineResource = await engine.resourceManager.load(
{
url: 'https://your.spineboy.json', // 自定义上传的资产
type: 'spine',
},
);
// 创建实体
const spineEntity = new Entity(engine);
// 添加 SpineAnimationRenderer 组件
const spine = spineEntity.addComponent(SpineAnimationRenderer);
// 设置动画资源
spine.resource = spineResource;
// 添加至场景
root.addChild(spineEntity);
在前一个章节中,为大家介绍了编辑器中 SpineAnimationRenderer 组件的配置项。 本小节会更加详细介绍在代码中如何使用 SpineAnimationRenderer 组件的各个 API。
SpineAnimationRenderer 组件继承于 Renderer,除了暴露 Renderer 的通用方法外,还提供了以下属性:
属性 | 解释 |
---|---|
resource | Spine 动画资源。设置了资源后,SpineAnimationRenderer 组件会读取资源数据,并渲染出 Spine 动画 |
setting | 渲染设置。用于控制开启裁减和调整图层间隔 |
defaultState | 默认状态。与编辑器的配置项对应,用于设置默认状态下 Spine 动画的动画,皮肤,缩放 |
state | 动画状态对象。用于进行更加复杂动画控制,如:队列播放,循环控制等 |
skeleton | 骨架对象。用于进行更加复杂的骨架操作,如:附件替换,换肤等 |
下面是更详细的使用介绍:
首先是资源的设置。SpineAnimationRenderer 组件需要设置资源后,才能完成 Spine动画的渲染。在上一个章节,「加载资产并添加至场景」中,已经为大家展示了设置资产的方式:
import { SpineAnimationRenderer } from '@galacean/engine-spine';
const spineResource = await engine.resourceManager.load(
{
url: 'https://your.spineboy.json',
type: 'spine',
},
);
const spineEntity = new Entity(engine);
const spine = spineEntity.addComponent(SpineAnimationRenderer);
spine.resource = spineResource; // 设置 Spine 资产
root.addChild(spineEntity);
在脚本中,你可以通过以下方式修改 Spine 的渲染设置,一般情况下,使用默认值即可。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
spine.setting.zSpacing = 0.01; // 设置图层间隔
spine.setting.useClipping = true; // 开启或关闭裁减,默认开启
}
}
在脚本中,你可以通过以下方式修改 Spine 动画的默认状态:
class YourAmazingScript {
onStart() {
const spineResource = await engine.resourceManager.load(
{
url: 'https://your.spineboy.json',
type: 'spine',
},
);
const spineEntity = new Entity(engine);
const spine = spineEntity.addComponent(SpineAnimationRenderer);
spine.defaultState.animationName = 'your-default-animation-name'; // 默认播放的动画名称
spine.defaultState.loop = true; // 默认播放的动画是否循环
spine.defaultState.skinName = 'default'; // 默认皮肤名称
spine.defaultState.scale = 0.02; // 默认缩放
spine.resource = spineResource; // 设置资源
rootEntity.addChild(spineEntity); // 添加至场景,此时组件激活
}
}
注意:默认状态仅在 SpineAnimationRenderer 组件激活和资源设置时生效。动态修改动画、皮肤、缩放请使用 state 与 skeleton 属性中的方法(见下面的章节)。
在脚本中,你能够通过以下方式获取到 AnimationState 对象,使用 AnimationState 对象能够实现更加复杂的动画操作。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { state } = spine; // AnimationState 对象
}
}
首先,我们来介绍一下最常用的 API:setAnimation
state.setAnimation(0, 'animationName', true)
setAnimation 函数接受三个参数:
后两个参数很好理解,第一个参数则包含了 Spine 动画的一个概念:Track (轨道)
Spine 动画在播放时,需要指定一个动画轨道。借助动画轨道,Spine 能够分层应用动画,每一个轨道都能够存储动画与播放参数,轨道的编号从 0 开始累加。在动画应用后,Spine 会从低轨道到高轨道依次应用动画,高轨道上的动画将会覆盖低轨道上的动画。
动画轨道有很多用途,例如,轨道 0 可以有行走、奔跑、游泳或其他动画,轨道 1 可以有一个只为手臂和开枪设置了关键帧的射击动画。此外,为高层轨道设置TrackEntry alpha可使其与下面的轨道混合。例如,轨道 0 可以有一个行走动画,轨道 1 可以有一个跛行动画。当玩家受伤时,增加轨道 1 的alpha值,跛行就会加重。
调用 setAnimation 方法后,会立即切换当前轨道的动画。如果你需要动画切换时有过渡效果,需要设置过渡的持续时间。可以通过 AnimationStateData 的 API 来进行设置:
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { state } = spine; // AnimationState 对象
const { data } = state; // AnimationStateData 对象
data.defaultMix = 0.2; // 设置默认过渡持续时间
data.setMix('animationA', 'animationB', 0.3); // 设置两个指定动画的过渡持续时间
}
}
Spine 还提供了 addAnimation 方法来实现动画的队列播放:
state.setAnimation(0, 'animationA', false); // 在轨道 0 播放动画 A
state.addAnimation(0, 'animationB', true, 0); // 在动画 A 之后后,添加动画 B,并循环播放
addAnimation 接受 4 个参数:
前三个参数很好理解,这里解释一下第四个参数: delay 代表了前一个动画的持续时间。
当 delay > 0 时(假设 delay 为 1),前一个动画会在播放 1 秒后,切换到下一个动画。如下图所示:
如果动画 A 的时长小于 1 秒,则会根据是否设置了循环播放:循环播放直至 1 秒,或者播放完毕后,保持在动画播放完毕的状态直至 1 秒。
当 delay = 0 时,下一个动画会在前一个动画播放完毕后播放,如下图所示:
假设动画 A 的时长为 1 秒,过渡持续时间为 0.2 秒,当 delay 设置为 0 时,动画 B 会从 1 - 0.2 也就是 0.8 秒开始过渡到动画 B。
当 delay < 0 时,上一个动画未播放完毕前,下一个动画就会开始播放,如下图所示: 同样假设动画 A 的时长为 1 秒,过渡持续时间为 0.2 秒,动画 B 则会从 0.6 秒开始过渡到动画 B。
除了 addAnimation 外,还能够通过 addEmptyAnimation 方法添加空动画。空动画能够让动画回到初始状态。
addEmptyAnimation 接受三个参数:TrackIndex,mixDuration 和 delay。TrackIndex 和 delay 参数与 addAnimation 一样。 mixDuration 是过渡持续时间,动画会在 mixDuration 时间内逐渐回到初始状态。如下图所示(右侧棕色区域即是空动画),
setAnimation 和 addAnimation 方法都会返回一个对象:TrackEntry。TrackEntry 提供了更多的参数来进行动画控制。 例如:
更多参数可以参考 TrackEntry 官方文档
当调用 AnimationState API 进行动画控制时,会触发如上图所示的事件。 在新的动画开始播放时,会触发 Start 事件,当动画在动画队列中移除或者中断时,会触发 End 事件。当动画播放完毕时,无论是否循环,都会触发 Complete 事件。
全部的事件以及详细解释请参考:Spine 动画事件官方文档
这些事件能够通过 AnimationState.addListener 进行监听。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { state } = spine; // AnimationState 对象
state.addListener({
start: (entry: TrackEntry) => {
// call back function
},
complete: (entry: TrackEntry) => {
// call back function
},
end: (entry: TrackEntry) => {
// call back function
},
interrupt: (entry: TrackEntry) => {
// call back function
},
dispose: (entry: TrackEntry) => {
// call back function
},
event: (entry: TrackEntry) => {
// call back function
},
})
}
}
在脚本中,你能够通过以下方式获取到 Skeleton 对象,来访问骨骼、插槽、附件等,并进行骨架操作。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { skeleton } = spine; // Skeleton 对象
}
}
下面是一些常用的操作:
通过 Skeleton API 能够修改 Spine 骨骼的位置,比较常见的应用是:可以通过设置 IK 的目标骨骼,来实现瞄准/跟随效果。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { skeleton } = spine; // Skeleton 对象
const bone = skeleton.findBone('aim-target');
bone.x = targetX;
bone.y = targetY;
}
}
注意:由于应用动画会修改骨骼位置,所以如果 Spine 在播放动画, 那么骨骼位置的修改需要在应用动画之后,也就是在脚本的 onLateUpdate 生命周期中进行操作。
通过 Skeleton API 能够替换插槽内的附件。通过切换附件,能够实现局部换装的效果。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { skeleton } = spine; // Skeleton 对象
// 根据名称查找插槽
const slot = skeleton.findSlot('slotName');
// 按名称从骨架皮肤或默认皮肤获取附件
const attachment = skeleton.getAttachment(slot.index, 'attachmentName');
// 设置插槽附件
slot.attachment = attachment;
// 或者由骨架setAttachment方法来设置插槽附件
skeleton.setAttachment('slotName', 'attachmentName');
}
}
注意:由于应用动画会修改插槽内的附件,所以如果 Spine 在播放动画,那么附件更换的操作需要在应用动画之后,也就是在脚本的 onLateUpdate 生命周期中进行操作。
换肤
通过 Skeleton 的 setSkin API 能够根据皮肤名称实现整体换肤。
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { skeleton } = spine; // Skeleton 对象
// 根据皮肤名称设置皮肤
skeleton.setSkinByName("full-skins/girl");
// 回到初始位置(必须调用,否则渲染可能出现错乱)
skeleton.setSlotsToSetupPose();
}
}
混搭
在 Spine 编辑器中,设计师可以为每一个外观和装备准备皮肤,然后在运行时把他们组合成一个新的皮肤。下面的代码展示了如果通过 addSkin 来添加选定的皮肤的:
class YourAmazingScript {
onStart() {
const spine = this.entity.getComponent(SpineAnimationRenderer);
const { skeleton } = spine; // Skeleton 对象
const mixAndMatchSkin = new spine.Skin("custom-girl");
mixAndMatchSkin.addSkin(skeletonData.findSkin("skin-base"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("nose/short"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyelids/girly"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("eyes/violet"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("hair/brown"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("clothes/hoodie-orange"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("legs/pants-jeans"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/bag"));
mixAndMatchSkin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
this.skeleton.setSkin(mixAndMatchSkin);
}
}
代码中皮肤的名称来自 mix-and-match 示例。
下一个章节会给大家展示全部的 Spine 示例