跳转到内容

Spine 动画

SpineAnimation 组件播放 Spine 骨骼动画。在场景编辑器中将它和 Transform 一起添加到实体上。

属性

属性类型默认值说明
skeletonPathstring''骨骼 JSON 文件路径
atlasPathstring''图集文件路径
skinstring''当前皮肤名
animationstring''当前动画名
timeScalenumber1.0播放速度倍率
loopbooleantrue是否循环
playingbooleantrue是否正在播放
flipXbooleanfalse水平翻转
flipYbooleanfalse垂直翻转
colorColor{r:1, g:1, b:1, a:1}着色 RGBA
layernumber0渲染顺序
skeletonScalenumber1.0骨骼缩放因子
materialnumber0材质 ID
enabledbooleantrue是否渲染骨骼动画

设置步骤

  1. 将 Spine 导出文件(.json + .atlas + 图片)放到项目的 assets/ 文件夹
  2. 在场景编辑器中:创建实体 → 添加 TransformSpineAnimation
  3. 设置 skeletonPathatlasPath 指向 Spine 文件
  4. 设置 animation 为要播放的动画名

控制动画

在系统中查询 SpineAnimation 组件来运行时切换动画:

import { defineSystem, addSystem, Res, Input, Query, Mut, SpineAnimation } from 'esengine';
import { Player } from './components';
addSystem(defineSystem(
[Res(Input), Query(Mut(SpineAnimation), Player)],
(input, query) => {
for (const [entity, spine] of query) {
if (input.isKeyPressed('Space')) {
spine.animation = 'jump';
spine.loop = false;
} else if (input.isKeyDown('KeyD') || input.isKeyDown('KeyA')) {
spine.animation = 'run';
spine.loop = true;
} else {
spine.animation = 'idle';
spine.loop = true;
}
}
}
));

皮肤

切换皮肤改变角色外观:

spine.skin = 'warrior';

播放控制

spine.playing = false; // 暂停
spine.playing = true; // 恢复
spine.timeScale = 2.0; // 两倍速
spine.timeScale = 0.5; // 半速

翻转

spine.flipX = true; // 面朝左
spine.flipX = false; // 面朝右(默认)

颜色着色

spine.color = { r: 1, g: 0, b: 0, a: 1 }; // 红色
spine.color = { r: 1, g: 1, b: 1, a: 0.5 }; // 50% 透明

示例:角色控制器

定义 CharacterState 组件,在编辑器中与 SpineAnimation 一起挂载到实体上:

import {
defineComponent, defineSystem, addSystem,
Res, Input, Time, Query, Mut, Transform, SpineAnimation
} from 'esengine';
const CharacterState = defineComponent('CharacterState', {
speed: 200,
currentAnim: 'idle'
});
addSystem(defineSystem(
[Res(Input), Res(Time), Query(Mut(Transform), Mut(SpineAnimation), Mut(CharacterState))],
(input, time, query) => {
for (const [entity, transform, spine, state] of query) {
let moving = false;
if (input.isKeyDown('KeyD')) {
transform.position.x += state.speed * time.delta;
spine.flipX = false;
moving = true;
}
if (input.isKeyDown('KeyA')) {
transform.position.x -= state.speed * time.delta;
spine.flipX = true;
moving = true;
}
const newAnim = moving ? 'run' : 'idle';
if (state.currentAnim !== newAnim) {
state.currentAnim = newAnim;
spine.animation = newAnim;
spine.loop = true;
}
}
}
));

动画混合

通过 SpineManager 配置动画之间的交叉淡入淡出:

import { SpineManager } from 'esengine/spine';
const manager = app.getResource(SpineManager);
// 设置所有过渡的默认混合时长
manager.setDefaultMix(entity, 0.2);
// 设置特定过渡对的混合时长
manager.setMixDuration(entity, 'idle', 'run', 0.15);
manager.setMixDuration(entity, 'run', 'idle', 0.3);

附件控制

在运行时显示/隐藏插槽和替换附件:

const manager = app.getResource(SpineManager);
// 隐藏插槽
manager.setAttachment(entity, 'weapon', '');
// 替换为其他附件
manager.setAttachment(entity, 'weapon', 'sword');

IK 约束

配置 Spine 骨架上的反向动力学约束,可通过代码设置 IK 目标位置:

const manager = app.getResource(SpineManager);
// mix: 0.0 = 无效果, 1.0 = 完全 IK
manager.setIKTarget(entity, 'aim-ik', targetX, targetY, 1.0);

插槽颜色

为单个 Spine 插槽着色:

const manager = app.getResource(SpineManager);
manager.setSlotColor(entity, 'body', 1, 0.5, 0.5, 1);

多版本架构

Estella 同时支持 Spine 3.84.14.2 版本。引擎会自动从骨骼数据中检测 Spine 版本并选择对应的运行时后端。

import type { SpineVersion } from 'esengine/spine';
// SpineVersion = '3.8' | '4.1' | '4.2'

工作原理

  • 加载 Spine 资源时,引擎从骨骼文件头中检测版本
  • 自动选择匹配的后端:C++ native(优先)或 WASM fallback
  • 无需修改代码——同一个 SpineAnimation 组件适用于所有版本

SpineManager 运行时 API

对于高级用例,SpineManager 提供运行时查询能力:

import { SpineManager } from 'esengine/spine';
// 从原始数据检测版本(加载前)
const version = SpineManager.detectVersion(skelBinaryData);
const versionJson = SpineManager.detectVersionJson(skelJsonString);
// 查询已加载实体
const manager = app.getResource(SpineManager);
const ver = manager.getEntityVersion(entity); // '3.8' | '4.1' | '4.2'
const anims = manager.getAnimations(entity); // ['idle', 'run', 'jump']
const skins = manager.getSkins(entity); // ['default', 'warrior']
const bounds = manager.getBounds(entity); // { x, y, width, height }

动画查询与事件

v0.12.0 新增。

动画查询

通过 SpineManager 资源在运行时查询动画状态:

import { SpineManager } from 'esengine/spine';
const manager = app.getResource(SpineManager);
// 列出实体的所有可用动画 / 皮肤
const anims = manager.getAnimations(entity); // ['idle', 'run', 'jump']
const skins = manager.getSkins(entity); // ['default', 'warrior']
// 获取轴对齐包围盒
const bounds = manager.getBounds(entity); // { x, y, width, height }

事件回调

Spine 每帧会发出生命周期事件和自定义事件。SpineEvents 资源会将它们收集到一个只读数组中,可在任何系统中消费:

import { defineSystem, addSystem, Res } from 'esengine';
import { SpineEvents } from 'esengine/spine';
import type { SpineEvent } from 'esengine/spine';
addSystem(defineSystem(
[Res(SpineEvents)],
(spineEvents) => {
for (const evt of spineEvents.events) {
switch (evt.type) {
case 'complete':
console.log(`${evt.animationName} 在 track ${evt.track} 上播放完成`);
break;
case 'event':
// 在 Spine 编辑器中定义的自定义事件
console.log(`自定义事件: ${evt.eventName}`,
evt.intValue, evt.floatValue, evt.stringValue);
break;
}
}
}
));

事件类型:startinterruptendcompleteevent(自定义)。

每个 SpineEvent 包含:

字段类型说明
entityEntity触发事件的实体
typeSpineEventType上述事件类型之一
tracknumber动画轨道索引
animationNamestring动画名
eventNamestring?自定义事件名(仅 event 类型)
intValuenumber?整数载荷(仅 event 类型)
floatValuenumber?浮点载荷(仅 event 类型)
stringValuestring?字符串载荷(仅 event 类型)

约束控制

在运行时查询和修改 IK、变换、路径约束:

const manager = app.getResource(SpineManager);
// 列出实体上的所有约束
const constraints = manager.listConstraints(entity);
// { ik: ['aim-ik'], transform: ['hip-transform'], path: ['path-follow'] }
// IK:设置目标位置和混合强度
manager.setIKTarget(entity, 'aim-ik', targetX, targetY, 1.0);
// 变换约束:读取和修改混合值
const tmix = manager.getTransformConstraintMix(entity, 'hip-transform');
// { mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY }
manager.setTransformConstraintMix(entity, 'hip-transform', {
...tmix!,
mixRotate: 0.5,
});
// 路径约束:读取和修改混合值
const pmix = manager.getPathConstraintMix(entity, 'path-follow');
// { position, spacing, mixRotate, mixX, mixY }
manager.setPathConstraintMix(entity, 'path-follow', {
...pmix!,
mixRotate: 0.8,
});

多纹理批处理

使用多个图集页的 Spine 骨骼会被渲染器自动批处理。无需额外配置 — C++ 后端透明地处理多纹理情况,即使是复杂的多图集角色也能保持较低的 draw call。

资源清理

Spine 资源会在所属实体被销毁时自动清理。引擎通过 onDespawn 调用 SpineManager.removeEntity(),释放底层 WASM 内存。销毁实体时无需手动释放 Spine 资源。