コンテンツにスキップ

Spine 骨骼动画

このコンテンツはまだ日本語訳がありません。

<spine> 节点用来在场景中显示 Spine 骨骼动画模型,支持多轨动画、混合过渡、皮肤与附件切换、暂停与时间缩放,以及点击命中测试。

Spine Runtimes 有自己的授权条款,它的派生产物不能按本引擎的 MPL-2.0 授权,因此插件作为独立二进制单独交付。

本页面只描述接入方式,不构成 Spine 授权。发布游戏时,请自行确认你的使用方式符合 Spine 的许可条款,包括随游戏分发插件、商用发行等情形。示例模型(如 Spineboy)另有素材使用条款,不能直接用于发行。

参考:

通过社区渠道(QQ群或 Discord)联系作者获取插件:

见社区页面。

能力 说明
渲染 多页图集、网格变形、遮罩裁剪、四种混合模式、双色染色
动画 多轨播放、替换与排队、循环、过渡混合、从中间位置开始
状态 皮肤切换、附件切换、回到初始姿势
播放 暂停、时间缩放、物理重置
交互 按下自动命中、手动命中查询
事件 播放状态、动画内嵌事件、加载与错误

把插件动态库放到引擎可执行文件同目录:

平台 文件名
Windows moyu_spine_plugin.dll
Linux libmoyu_spine_plugin.so
macOS libmoyu_spine_plugin.dylib
Android libmoyu_spine_plugin.so

也可以用环境变量 MOYU_SPINE_PLUGIN 指向插件文件。设置该变量后,插件只从这个路径加载。

Spine 模型由骨架文件与图集文件两部分组成,图集再引用页图:

assets/
└── spine/
└── hero/
├── hero.json # 或 hero.skel
├── hero.atlas
├── hero.png # 页图,文件名由 .atlas 指定
└── hero2.png # 多页时更多

src 填骨架文件相对 assets/ 的路径,atlas 填图集文件路径。

不填 atlas 时,引擎按「骨架同目录、同主名、扩展名换成 .atlas」推断。很多模型的导出不符合这个规律(骨架叫 hero-pro.json,图集却叫 hero.atlas),建议显式填写。

.json 与 .skel 两种骨架格式都支持,画面一致。.skel 体积更小、加载更快,推荐用于发行。

图集里每个页都必须声明有效的 size,缺少时会加载失败。

import { useRef } from 'react';
import type { Node } from '@momoyu-ink/kit';
function Character() {
const ref = useRef<Node>(null);
return (
<spine
ref={ref}
src="spine/hero/hero.json"
atlas="spine/hero/hero.atlas"
x={960}
y={540}
scale={0.8}
onReady={(event) => {
console.log(`${event.animations.length} 个动画,${event.skins.length} 个皮肤`);
}}
/>
);
}

节点的尺寸取自模型的原始几何范围,通常远大于舞台,需要用 scale 缩放。这个尺寸不会随动画或换装变化。

以下命令示例都基于上面这个 ref,并假定模型已经加载完成。

属性 类型 默认值 说明
src string | null — 骨架文件路径,相对 assets/。设为 null 卸载模型
atlas string — 图集文件路径;留空时按骨架路径推断
skin string 空 皮肤名;留空使用模型的默认皮肤
timeScale number 1 时间缩放,0.5 为半速
paused boolean false 暂停推进时间
physics 'none' | 'pose' | 'update' 'update' 物理模式
hitArea boolean false 按下时自动做命中测试并触发 onHit

paused 停止推进时间,但不会冻结画面:暂停期间换皮肤、换附件、换姿势都会立即生效,命中测试也照常工作。

timeScale 与 paused 互相独立,只改变播放速度。

值 含义
'none' 既不更新也不应用,保持当前状态
'pose' 应用当前状态,但不按时间推进
'update' 按时间推进并应用(默认)

改变 src 会卸载旧模型并加载新的,动画、皮肤、附件等状态全部重置,加载完成后重新触发 onReady。

旧模型上正在播放的动画会各收到一次 onTrackDispose。

通过节点 ref 的 executeCommand 调用:

ref.current?.executeCommand({ subCommand: 'setAnimation', track: 0, animation: 'idle' });
// 替换轨道上的动画
ref.current?.executeCommand({
subCommand: 'setAnimation',
track: 0,
animation: 'walk',
loop: true,
mix: 0.2,
time: 0,
});
// 排队:当前动画结束后接着播
ref.current?.executeCommand({
subCommand: 'addAnimation',
track: 0,
animation: 'jump',
loop: false,
mix: 0.15,
});
// 清空轨道;mix > 0 时淡出
ref.current?.executeCommand({ subCommand: 'clearTrack', track: 0, mix: 0.3 });
// 清空全部轨道
ref.current?.executeCommand({ subCommand: 'clearTracks' });
参数 类型 默认值 说明
track number 必填 轨道序号,从 0 开始
animation string — 动画名,取自 onReady 的 animations
loop boolean false 是否循环
mix number -1 过渡时长(秒);负值使用默认值
delay number 0 延迟(秒)
time number 0 起始播放位置(秒),仅 setAnimation 有效

setAnimation 替换轨道上原有的动画,addAnimation 则在后面排队。

track 是必填的。第二条动画要用 track: 1,否则会和第一条落在同一个轨道上互相替换。

setAnimation 返回 { entry, epoch },标识这一次播放,可用于识别对应的事件:

const id = ref.current?.executeCommand({
subCommand: 'setAnimation',
track: 0,
animation: 'jump',
});
// 之后在事件里比对
event.id.entry === id.entry && event.id.epoch === id.epoch

两个字段都是 bigint。

// 所有动画切换的默认过渡时长
ref.current?.executeCommand({ subCommand: 'setDefaultMix', duration: 0.2 });
// 指定某一对动画之间的过渡时长,优先于默认值
ref.current?.executeCommand({ subCommand: 'setMix', from: 'idle', to: 'walk', duration: 0.3 });

这两个命令只改配置,不影响已经在播放的过渡。

皮肤用 skin 属性切换:

<spine src="spine/hero/hero.json" skin="damaged" />

换皮肤时,各槽位恢复到该皮肤默认的附件组合,之后当前动画继续生效。皮肤名可在 onReady 的 skins 中查到。

附件用命令切换:

// 换成另一个附件;空名表示移除
ref.current?.executeCommand({ subCommand: 'setAttachment', slot: 'weapon', name: 'sword' });
// 清空所有轨道并回到初始姿势
ref.current?.executeCommand({ subCommand: 'resetToSetupPose' });
// 让物理立即回到稳定状态
ref.current?.executeCommand({ subCommand: 'resetPhysics' });

resetToSetupPose 会同时清空所有轨道。

const name = ref.current?.executeCommand({
subCommand: 'hitTest',
x: 200,
y: 300,
});
// 返回命中的区域名称,未命中返回 null

x / y 使用节点局部坐标,与指针事件的 offsetX / offsetY 一致。

事件 载荷 触发时机
onReady 模型信息 模型解析完成
onTrackStart { track, id, animation } 动画开始播放
onTrackComplete { track, id, animation } 这一轮播放到终点
onTrackEnd { track, id, animation } 动画停止应用
onTrackDispose { track, id, animation } 播放被回收
onTrackEvent { track, id, animation, name, intValue, floatValue, stringValue, audioPath, volume, balance } 动画内嵌事件
onHit { epoch, name, x, y } 按下时命中区域(需开启 hitArea)
onError { code, message } 插件不可用、加载失败或运行出错
你想知道 用哪个事件
这一轮播完了 onTrackComplete
这次播放彻底结束了 onTrackDispose

onTrackComplete 在每轮到达终点时触发,循环动画每轮都会触发一次。

onTrackDispose 在播放被回收时触发:被新动画替换、被 clearTrack 清掉、或模型被卸载时。

Spine 工程里可以给动画插入事件,播放到对应时间点时通过 onTrackEvent 上报:

<spine
src="spine/hero/hero.json"
onTrackEvent={(event) => {
if (event.name === 'footstep') playSound('step.opus');
if (event.audioPath) playSound(event.audioPath, { volume: event.volume });
}}
/>

name 是事件名。intValue / floatValue / stringValue / audioPath / volume / balance 对应工程里该事件的字段,未填写的会沿用事件定义中的值,volume 默认为 1。

字段 内容
dataVersion 骨架导出的版本字符串
bounds 模型的原始几何范围 { x, y, width, height }
animations 动画清单:名称与时长(秒)
skins 皮肤名清单,可直接用于 skin 属性
pages 图集页清单:路径、尺寸、是否已预乘
boundingBoxes 命中区域名称清单

onReady 表示模型已经解析完成,可以开始发命令,但贴图可能还在加载,此时节点不绘制任何内容。贴图加载失败会另外触发 onError。

executeCommand 参数有误时(例如动画名不存在)会直接抛出错误,需要在调用处捕获。

命中测试与命中事件使用节点局部坐标,原点在节点左上角、y 向下。

把指针事件直接绑在 <spine> 上时,offsetX / offsetY 就是节点局部坐标:

<spine
ref={ref}
src="spine/hero/hero.json"
atlas="spine/hero/hero.atlas"
hitArea
onClick={(event) => {
ref.current?.executeCommand({
subCommand: 'hitTest',
x: event.offsetX,
y: event.offsetY,
});
}}
/>

事件从子节点冒泡上来、或手上只有舞台坐标时,需要先换算。节点没有旋转、祖先没有额外变换时:

const localX = (stageX - globalNodeX) / globalScale;
const localY = (stageY - globalNodeY) / globalScale;

节点带旋转或嵌套变换时,请按全局变换求逆换算。

命中测试只认当前启用的区域。很多模型的命中区槽位在初始状态下是空的(没有挂附件),这时模型里就没有这个判定区,hitTest 会返回空。

这一般是有意设计的:动画常用附件时间线来控制判定区何时开启,比如只有抬手时才允许点中手部。

如果需要在默认状态下就能点中,用 setAttachment 把判定区挂上去:

ref.current?.executeCommand({ subCommand: 'setAttachment', slot: 'head-bb', name: 'head' });

槽位名与附件名来自模型工程,需要向美术确认。

模型的命中区与动画彼此独立,不存在「点这个区域就播那个动画」的配置。拿到区域名后播哪个动画,由你的代码决定。

现象 可能原因 处理
节点空白,收到 code: 5 插件缺失,或版本不匹配 检查动态库位置与 MOYU_SPINE_PLUGIN;补上文件后重启引擎
收到 code: 3 骨架或图集读取失败 检查 src / atlas 路径,确认 assets/ 下文件齐全
收到 code: 4 骨架或图集解析失败 确认模型由 4.3 版编辑器导出、文件未损坏
提示找不到图集 没填 atlas,推断出的路径不对 显式填写 atlas
收到 code: 4 且提示页缺少尺寸 图集里有页没声明 size 用官方工具重新导出
模型太大或位置不对 节点尺寸取自模型原始范围 调整 scale 与 x / y
切换动画没有反应 动画名不对,或报错被吞掉了 用 onReady 的 animations 校对名称;在调用处 try / catch
叠加的第二条动画顶掉了第一条 两次用了同一个 track 第二条改用 track: 1
等动画播完一直没有反应 用 onTrackDispose 等非循环动画 改用 onTrackComplete
改对了 atlas 仍提示文件不存在 加载失败后不会自动重试 先把 src 设成 null,再设回原值
命中测试总是返回空 模型当前没有启用任何命中区 见上方「命中测试可能什么都点不到」
换皮肤后附件不对 换皮肤会恢复该皮肤的默认附件 换肤后重新调用 setAttachment

常见错误码:

码 含义
1 参数无效,如动画名或皮肤名不存在
2 插件与引擎版本不匹配
3 文件读取失败
4 解析失败
5 插件不可用,或模型超出支持范围
平台 支持
Windows 支持
Linux 支持
macOS 支持
Android 支持
iOS 未验证
Web 不支持

Web 平台没有动态库可加载,节点会保持空白并通过 onError 上报一次。iOS 尚未验证。

  • 仅支持 Spine 4.3 导出的模型;
  • 无 Web 实现,iOS 未验证;
  • 物理的三种模式目前表现相同;
  • 图集页需为 RGBA8888,且每页必须声明 size;
  • 不提供骨骼级编辑(如运行时调整单根骨骼),也不支持混合皮肤组合、运行时重新打包图集;
  • 命中测试按包围盒判定,不是像素级精确命中。

模型的原始几何范围在加载时按默认皮肤的初始姿势测量一次,之后不再变化:

  1. 动画把角色移出这个范围很远时,可能被引擎判定为不可见;
  2. 放进 <filter> 时,伸出范围的部分(比如抬手的手、飘动的头发)会被裁掉;
  3. 换成几何范围更大的皮肤,布局尺寸也不会跟着变。

需要完整保留时,把 <filter> 移到不含裁剪的层级,或让美术放宽模型的包围范围。