mcpField
A
animations-tweens

animations-tweens

github.com/dcl-regenesislabs开发1738 次安装21 周前更新
代码仓库

关于 animations-tweens

animations-tweens 是面向 Decentraland 开发的动画技能,介绍如何用 Animator、Tween、TweenSequence 和自定义系统为实体及 GLTF 模型制作动画。它说明何时使用模型内置动画、平滑属性变化、串联动作或逐帧控制。

主要功能

  1. 01播放和切换 GLTF 动画片段
  2. 02补间位置、旋转和缩放
  3. 03用 TweenSequence 串联补间
  4. 04选择缓动和循环模式
  5. 05控制变形目标权重
  6. 06构建自定义逐帧动画系统

适用场景

  1. 01用 GLTF 片段为角色制作动画
  2. 02让门在两个位置间移动或旋转
  3. 03创建循环巡逻路径
  4. 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()类似物理的运动、跟随目标、自定义缓动

决策流程:

  1. .glb 模型是否已有动画?→ Animator
  2. 是否只需在两个值之间简单移动/旋转/缩放?→ Tween
  3. 是否需要逐帧控制或自定义数学计算?→ 使用带 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>。

valueenumshape
0EF_LINEAR匀速
1EF_EASEINQUAD二次缓入
2EF_EASEOUTQUAD二次缓出
3EF_EASEQUAD二次缓入缓出
4EF_EASEINSINE正弦缓入
5EF_EASEOUTSINE正弦缓出
6EF_EASESINE正弦缓入缓出(平滑)
7EF_EASEINEXPO指数缓入
8EF_EASEOUTEXPO指数缓出
9EF_EASEEXPO指数缓入缓出
10EF_EASEINELASTIC弹性缓入
11EF_EASEOUTELASTIC弹性缓出
12EF_EASEELASTIC弹性缓入缓出
13EF_EASEINBOUNCE弹跳缓入
14EF_EASEOUTBOUNCE弹跳缓出
15EF_EASEBOUNCE弹跳缓入缓出
16EF_EASEINCUBIC三次缓入
17EF_EASEOUTCUBIC三次缓出
18EF_EASECUBIC三次缓入缓出
19EF_EASEINQUART四次缓入
20EF_EASEOUTQUART四次缓出
21EF_EASEQUART四次缓入缓出
22EF_EASEINQUINT五次缓入
23EF_EASEOUTQUINT五次缓出
24EF_EASEQUINT五次缓入缓出
25EF_EASEINCIRC圆形缓入
26EF_EASEOUTCIRC圆形缓出
27EF_EASECIRC圆形缓入缓出
28EF_EASEINBACK回弹缓入
29EF_EASEOUTBACK回弹缓出
30EF_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 组件不起作用实体缺少 GltfContainerAnimator 仅适用于已加载 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

评论

支持 Markdown