关于 animations-tweens
animations-tweens 是面向 Decentraland 开发的动画技能,介绍如何用 Animator、Tween、TweenSequence 和自定义系统为实体及 GLTF 模型制作动画。它说明何时使用模型内置动画、平滑属性变化、串联动作或逐帧控制。
主要功能
- 01播放和切换 GLTF 动画片段
- 02补间位置、旋转和缩放
- 03用 TweenSequence 串联补间
- 04选择缓动和循环模式
- 05控制变形目标权重
- 06构建自定义逐帧动画系统
适用场景
- 01用 GLTF 片段为角色制作动画
- 02让门在两个位置间移动或旋转
- 03创建循环巡逻路径
- 04用变形目标驱动模型表情
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill animations-tweens安装后在 Claude Code 中输入/animations-tweens确认已加载。
详细介绍
Decentraland 中的动画和 Tween
编写代码的划分
Animator、Tween 和 TweenSequence 均支持在 main-entities.ts 中使用——在字面量中声明实体、其视觉组件和初始动画状态。在 src/index.ts 中通过 getMutable 在运行时切换动画片段或替换 tween。
如何选择动画方式
| 需求 | 方式 | 适用场景 |
|---|---|---|
| 播放烘焙在 .glb 模型中的动画 | Animator | 角色行走、门打开、旗帜飘动——任何在 Blender/Maya 中创建的动画 |
| 平滑地移动/旋转/缩放实体 | Tween | 滑动门、浮动平台、物体变大——程序化的 A 到 B 动作 |
| 按顺序串联多个动画 | TweenSequence | 巡逻路线、多步骤门、复杂编排 |
| 连续逐帧控制 | engine.addSystem() | 类似物理的运动、跟随目标、自定义缓动 |
决策流程:
- .glb 模型是否已有动画?→
Animator - 是否只需在两个值之间简单移动/旋转/缩放?→
Tween - 是否需要逐帧控制或自定义数学计算?→ 使用带
dt的系统
GLTF 动画(Animator)
在 main-entities.ts 中声明角色及其动画状态。states 数组是 Animator 结构的一部分,并且兼容 JSON。
// main-entities.ts
import type { Scene } from '@dcl/sdk/scene-types'
export const scene = {
character: {
components: {
Transform: { position: { x: 8, y: 0, z: 8 } },
GltfContainer: { src: 'models/character.glb' },
Animator: {
states: [
{ clip: 'idle', playing: true, loop: true, speed: 1 },
{ clip: 'walk', playing: false, loop: true, speed: 1 },
{ clip: 'attack', playing: false, loop: false, speed: 1.5 }
]
}
}
}
} satisfies Scene
在 src/index.ts 中于运行时触发并切换动画:
import { engine, Animator } from '@dcl/sdk/ecs'
export function main() {
const character = engine.getEntityOrNullByName('character')
if (!character) return
// Play a specific animation
Animator.playSingleAnimation(character, 'walk')
// Stop all animations
// Animator.stopAllAnimations(character)
}
切换动画
import { Entity, Animator } from '@dcl/sdk/ecs'
function playAnimation(entity: Entity, clipName: string) {
const animator = Animator.getMutable(entity)
for (const state of animator.states) state.playing = false
const state = animator.states.find(s => s.clip === clipName)
if (state) state.playing = true
}
Tween
Tween 和 TweenSequence 兼容 JSON。mode 是带 $case 标签的联合类型;位置/四元数是普通对象字面量;缓动是数字枚举。
移动
// main-entities.ts
sliding_box: {
components: {
Transform: { position: { x: 2, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
Tween: {
duration: 2000, // milliseconds
easingFunction: 6, // EasingFunction.EF_EASESINE
mode: {
$case: 'move',
move: { start: { x: 2, y: 1, z: 8 }, end: { x: 14, y: 1, z: 8 } }
}
}
}
}
常用缓动值:0 = EF_LINEAR、1 = EF_EASEINQUAD、2 = EF_EASEOUTQUAD、3 = EF_EASEQUAD、6 = EF_EASESINE。若要在运行时替换或停止 tween,请在 src/index.ts 中对指定实体使用 Tween.createOrReplace / Tween.deleteFrom。
旋转
// main-entities.ts — quaternions are { x, y, z, w } literals.
// 360° around Y end-state is { x: 0, y: 1, z: 0, w: 0 }
spinning_obj: {
components: {
Transform: { position: { x: 8, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
Tween: {
duration: 3000,
easingFunction: 0, // EF_LINEAR
mode: {
$case: 'rotate',
rotate: {
start: { x: 0, y: 0, z: 0, w: 1 },
end: { x: 0, y: 1, z: 0, w: 0 }
}
}
}
}
}
若要精确旋转指定角度而无需手动计算四元数,请勿在 main-entities.ts 中添加 tween,而是在 src/index.ts 中创建它,因为那里可以使用 Quaternion.fromEulerDegrees():
// src/index.ts — runtime tween authoring (helpers allowed)
import { engine, Tween, EasingFunction } from '@dcl/sdk/ecs'
import { Quaternion } from '@dcl/sdk/math'
export function main() {
const box = engine.getEntityOrNullByName('spinning_obj')
if (!box) return
Tween.createOrReplace(box, {
mode: Tween.Mode.Rotate({
start: Quaternion.fromEulerDegrees(0, 0, 0),
end: Quaternion.fromEulerDegrees(0, 360, 0)
}),
duration: 3000,
easingFunction: EasingFunction.EF_LINEAR
})
}
缩放
// main-entities.ts
grow_box: {
components: {
Transform: { position: { x: 8, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
Tween: {
duration: 1000,
easingFunction: 14, // EF_EASEOUTBOUNCE
mode: {
$case: 'scale',
scale: { start: { x: 1, y: 1, z: 1 }, end: { x: 2, y: 2, z: 2 } }
}
}
}
}
TweenSequence(串联动画)
串联多个 tween,使其依次播放。main-entities.ts 支持 TweenSequence:
// main-entities.ts
patrol_box: {
components: {
Transform: { position: { x: 2, y: 1, z: 8 } },
MeshRenderer: { mesh: { $case: 'box', box: { uvs: [] } } },
Tween: {
duration: 2000,
easingFunction: 6, // EF_EASESINE
mode: { $case: 'move', move: { start: { x: 2, y: 1, z: 8 }, end: { x: 14, y: 1, z: 8 } } }
},
TweenSequence: {
loop: 0, // TweenLoop.TL_RESTART (1 = TL_YOYO)
sequence: [
{
duration: 2000,
easingFunction: 6,
mode: { $case: 'move', move: { start: { x: 14, y: 1, z: 8 }, end: { x: 2, y: 1, z: 8 } } }
}
]
}
}
}
缓动函数
在 main-entities.ts 中使用整数值(左列)。在 src/index.ts 中可以直接引用 EasingFunction.<NAME>。
| value | enum | shape |
|---|---|---|
| 0 | EF_LINEAR | 匀速 |
| 1 | EF_EASEINQUAD | 二次缓入 |
| 2 | EF_EASEOUTQUAD | 二次缓出 |
| 3 | EF_EASEQUAD | 二次缓入缓出 |
| 4 | EF_EASEINSINE | 正弦缓入 |
| 5 | EF_EASEOUTSINE | 正弦缓出 |
| 6 | EF_EASESINE | 正弦缓入缓出(平滑) |
| 7 | EF_EASEINEXPO | 指数缓入 |
| 8 | EF_EASEOUTEXPO | 指数缓出 |
| 9 | EF_EASEEXPO | 指数缓入缓出 |
| 10 | EF_EASEINELASTIC | 弹性缓入 |
| 11 | EF_EASEOUTELASTIC | 弹性缓出 |
| 12 | EF_EASEELASTIC | 弹性缓入缓出 |
| 13 | EF_EASEINBOUNCE | 弹跳缓入 |
| 14 | EF_EASEOUTBOUNCE | 弹跳缓出 |
| 15 | EF_EASEBOUNCE | 弹跳缓入缓出 |
| 16 | EF_EASEINCUBIC | 三次缓入 |
| 17 | EF_EASEOUTCUBIC | 三次缓出 |
| 18 | EF_EASECUBIC | 三次缓入缓出 |
| 19 | EF_EASEINQUART | 四次缓入 |
| 20 | EF_EASEOUTQUART | 四次缓出 |
| 21 | EF_EASEQUART | 四次缓入缓出 |
| 22 | EF_EASEINQUINT | 五次缓入 |
| 23 | EF_EASEOUTQUINT | 五次缓出 |
| 24 | EF_EASEQUINT | 五次缓入缓出 |
| 25 | EF_EASEINCIRC | 圆形缓入 |
| 26 | EF_EASEOUTCIRC | 圆形缓出 |
| 27 | EF_EASECIRC | 圆形缓入缓出 |
| 28 | EF_EASEINBACK | 回弹缓入 |
| 29 | EF_EASEOUTBACK | 回弹缓出 |
| 30 | EF_EASEBACK | 回弹缓入缓出 |
自定义动画系统
对于复杂动画,请创建一个系统:
// Continuous rotation system
function spinSystem(dt: number) {
for (const [entity] of engine.getEntitiesWith(Transform, Spinner)) {
const transform = Transform.getMutable(entity)
const spinner = Spinner.get(entity)
// Rotate around Y axis
const currentRotation = Quaternion.toEulerAngles(transform.rotation)
transform.rotation = Quaternion.fromEulerDegrees(
currentRotation.x,
currentRotation.y + spinner.speed * dt,
currentRotation.z
)
}
}
engine.addSystem(spinSystem)
Tween 辅助方法
使用简写辅助方法,直接在实体上创建或替换 Tween 组件:
import { Tween, EasingFunction } from '@dcl/sdk/ecs'
// Move — signature: Tween.setMove(entity, start, end, duration, easingFunction?)
Tween.setMove(entity,
Vector3.create(0, 1, 0), Vector3.create(0, 3, 0),
1500, EasingFunction.EF_EASEINBOUNCE
)
// Rotate — signature: Tween.setRotate(entity, start, end, duration, easingFunction?)
Tween.setRotate(entity,
Quaternion.fromEulerDegrees(0, 0, 0), Quaternion.fromEulerDegrees(0, 180, 0),
2000, EasingFunction.EF_EASEOUTQUAD
)
// Scale — signature: Tween.setScale(entity, start, end, duration, easingFunction?)
Tween.setScale(entity,
Vector3.One(), Vector3.create(2, 2, 2),
1000, EasingFunction.EF_LINEAR
)
// Combined Move + Rotate + Scale in one Tween
Tween.setMoveRotateScale(entity, {
positionStart: Vector3.create(0, 0, 0), positionEnd: Vector3.create(0, 5, 0),
rotationStart: Quaternion.fromEulerDegrees(0, 0, 0), rotationEnd: Quaternion.fromEulerDegrees(0, 360, 0),
scaleStart: Vector3.One(), scaleEnd: Vector3.create(0.5, 0.5, 0.5)
}, 2000, EasingFunction.EF_EASEINOUTCUBIC)
// Continuous spin — applies a per-frame Quaternion delta. No end state.
Tween.setRotateContinuous(entity, Quaternion.fromEulerDegrees(0, -1, 0), 700)
// Continuous move — applies a per-frame Vector3 delta scaled by speed.
Tween.setMoveContinuous(entity, Vector3.create(0, 0, 1), 0.5)
*Continuous 变体不会结束——它们会持续应用固定增量,直到移除 Tween(Tween.deleteFrom(entity))。
Yoyo 循环模式
TL_YOYO 会在每次到达终点时反向播放 Tween,而不是重新开始:
TweenSequence.create(entity, {
sequence: [{ duration: 1000, ... }],
loop: TweenLoop.TL_YOYO
})
检测 Tween 完成
使用 tweenSystem.tweenCompleted() 检查 Tween 是否在本帧完成:
engine.addSystem(() => {
if (tweenSystem.tweenCompleted(entity)) {
console.log('Tween finished on', entity)
}
})
Animator 扩展功能
Animator 的其他功能:
// Get a specific clip to modify
const clip = Animator.getClip(entity, 'Walk')
// shouldReset: restart animation from beginning when re-triggered
Animator.playSingleAnimation(entity, 'Attack', true) // resets to start
// weight: blend between animations (0.0 to 1.0)
const anim = Animator.getMutable(entity)
anim.states[0].weight = 0.5 // blend walk at 50%
anim.states[1].weight = 0.5 // blend idle at 50%
变形目标(混合形状)
如果 GLTF 模型导出了变形目标(例如面部表情、形状混合),请通过每个 Animator.state 上的 weights 数组驱动它们。索引与导出变形目标的顺序一致。取值范围为 0–1。
const anim = Animator.getMutable(entity)
const state = anim.states.find(s => s.clip === 'Idle')
if (state) {
state.weights = state.weights ?? []
state.weights[0] = 0.8 // morph target 0 at 80%
state.weights[1] = 0.3 // morph target 1 at 30%
}
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| GLTF 动画未播放 | Animator.states 中的动画片段名称错误 | 在查看器(例如 Blender)中打开 .glb,以查找准确的动画片段名称——名称区分大小写 |
| Animator 组件不起作用 | 实体缺少 GltfContainer | Animator 仅适用于已加载 GLTF 模型的实体 |
| Tween 没有移动 | 起始位置和结束位置相同 | 检查 Tween.Mode.Move() 中的 start 和 end 值是否不同 |
| Tween 播放一次后停止 | 没有设置循环的 TweenSequence | 添加 TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_YOYO }),实现往返播放 |
| 动画抖动或卡顿 | 每帧都创建新的 Tween | 只创建一次 Tween,不要在系统中创建——使用 tweenSystem.tweenCompleted() 进行串联 |
需要用于动画的 3D 模型? 请参阅 add-3d-models 技能,了解如何加载包含动画片段的 GLTF 模型。
最佳实践
- 使用 Tween 实现简单的 A 到 B 动画(门、平台、UI 元素)
- 使用 Animator 播放烘焙到 GLTF 文件中的角色/模型动画
- 使用 Systems 实现持续动画或基于物理的动画
- Tween 时长以毫秒为单位(1000 = 1 秒)
- 将移动和旋转 Tween 应用于父/子实体,以组合两者
- 如需循环播放:使用
TweenSequence并设置loop: TweenLoop.TL_RESTART