Spine 动画
SpineAnimation 组件播放 Spine 骨骼动画。在场景编辑器中将它和 Transform 一起添加到实体上。
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
skeletonPath | string | '' | 骨骼 JSON 文件路径 |
atlasPath | string | '' | 图集文件路径 |
skin | string | '' | 当前皮肤名 |
animation | string | '' | 当前动画名 |
timeScale | number | 1.0 | 播放速度倍率 |
loop | boolean | true | 是否循环 |
playing | boolean | true | 是否正在播放 |
flipX | boolean | false | 水平翻转 |
flipY | boolean | false | 垂直翻转 |
color | Color | {r:1, g:1, b:1, a:1} | 着色 RGBA |
layer | number | 0 | 渲染顺序 |
skeletonScale | number | 1.0 | 骨骼缩放因子 |
material | number | 0 | 材质 ID |
enabled | boolean | true | 是否渲染骨骼动画 |
设置步骤
- 将 Spine 导出文件(
.json+.atlas+ 图片)放到项目的assets/文件夹 - 在场景编辑器中:创建实体 → 添加
Transform和SpineAnimation - 设置
skeletonPath和atlasPath指向 Spine 文件 - 设置
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 = 完全 IKmanager.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.8、4.1 和 4.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; } } }));事件类型:start、interrupt、end、complete、event(自定义)。
每个 SpineEvent 包含:
| 字段 | 类型 | 说明 |
|---|---|---|
entity | Entity | 触发事件的实体 |
type | SpineEventType | 上述事件类型之一 |
track | number | 动画轨道索引 |
animationName | string | 动画名 |
eventName | string? | 自定义事件名(仅 event 类型) |
intValue | number? | 整数载荷(仅 event 类型) |
floatValue | number? | 浮点载荷(仅 event 类型) |
stringValue | string? | 字符串载荷(仅 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 资源。