mcpField
A
animations-tweens

animations-tweens

github.com/decentraland创意与媒体4281 次安装今天更新
代码仓库

关于 animations-tweens

animations-tweens 是面向 Decentraland 开发的技能,介绍如何选择和实现模型动画、程序化运动及逐帧控制,并说明相关组件的行为与注意事项。

主要功能

  1. 01播放内嵌的 GLTF 动画
  2. 02动画化移动、旋转和缩放
  3. 03使用 TweenSequence 串联动作
  4. 04通过系统逐帧控制运动
  5. 05检测动画播放完成

适用场景

  1. 01播放角色或物体动画片段
  2. 02制作门和移动平台动画
  3. 03编排巡逻或多阶段动作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/decentraland/sdk-skills --skill animations-tweens

安装后在 Claude Code 中输入/animations-tweens确认已加载。

附带文件references/animation-patterns.md

详细介绍

Decentraland 中的动画与补间

何时使用哪种动画方式

需求方式适用场景
播放烘焙到 .glb 模型中的动画Animator角色行走、门打开、旗帜飘动——任何在 Blender/Maya 中制作的动画
平滑地移动/旋转/缩放实体Tween滑动门、浮动平台、物体变大——程序化的 A 到 B 动作
按顺序串联多个动画TweenSequence巡逻路线、多步骤门、复杂编排
持续控制每一帧engine.addSystem()类似物理的运动、跟随目标、自定义缓动、由输入驱动的用户控制

判断流程:

  1. .glb 是否已经包含动画?→ Animator
  2. 是否只需在两个值之间移动/旋转/缩放?→ Tween
  3. 是否需要逐帧控制或自定义数学计算?→ 使用带 dt 的 System

GLTF 动画(Animator)

播放嵌入在 .glb 模型中的动画。支持骨骼动画、物体动画和形状键(变形目标)动画。形状键可用于面部表情、口型同步或形变。

使用 Animator.create(entity, { states: [{ clip: 'idle', playing: true, loop: true, speed: 1 }] }) 进行设置。使用 Animator.playSingleAnimation(entity, 'walk') 播放单个动画。使用 Animator.stopAllAnimations(entity) 停止所有动画。使用 Animator.getClip(entity, 'Walk') 获取动画片段。通过在多个状态上设置 weight(0.0-1.0)来混合动画。

注意事项:playSingleAnimation 对 states 中不存在的动画片段会静默地不执行任何操作(仅适用于以编程方式创建的 Animator)

**适用范围:**这_仅_适用于完全通过代码创建 Animator 并以编程方式调用 Animator.playSingleAnimation(entity, clipName) 的情况。在许多实际工作流中,动画片段列表已经为你填充——请参阅下方的“何时无需手动注册动画片段”。

具体来说(已验证——@dcl/ecs/dist-cjs/components/extended/Animator.js 第 35-46 行):当 clipName 不在实体的 Animator.states 数组中时,playSingleAnimation(entity, clipName) 会返回 false,且不执行任何操作。内部的 getClipAndAnimator 辅助函数会返回空状态,随后 playSingleAnimation 会通过 if (!animator || !state) return false 退出。不会显示控制台警告。

因此,如果你在 TypeScript 中自行构建 Animator,并打算使用动画片段名称调用 playSingleAnimation,该动画片段必须已经存在于 states[] 中:

Animator.create(ghost, {
  states: [
    { clip: "Idle_001", playing: true, loop: true },
    { clip: "Idle_002", playing: false, loop: true },
    { clip: "Appear", playing: false, loop: false },
    { clip: "Death", playing: false, loop: false },
    { clip: "Hit", playing: false, loop: false },
  ],
});

如果你希望获得 SDK6 那种“无需设置即可播放任意动画片段”的便捷体验,可以封装 playSingleAnimation,并延迟添加缺失的状态:

function playAnim(entity: Entity, clip: string, loop = false) {
  const a = Animator.getMutableOrNull(entity);
  if (!a) return;
  if (!a.states.find((s) => s.clip === clip)) {
    a.states.push({
      clip,
      playing: false,
      loop,
      shouldReset: true,
      speed: 1,
      weight: 1,
    });
  }
  Animator.playSingleAnimation(entity, clip, true);
  // playSingleAnimation already pauses all other states
}
无需手动注册动画片段的情况

几种常见工作流会自动填充动画片段列表——以下情况不适用上述规则:

  • 未附加 Animator 的 GltfContainer。 渲染器会自行自动播放 GLB 中的一个动画片段(观察到的情况:文件中烘焙的第一个/默认动画片段)。我们在一个万圣节场景中亲自确认过:小幽灵没有 Animator,渲染器直接从 GLB 自动播放了 die。如果模型只需在生成时就开始播放动画,并且运行时不需要切换动画片段,可以使用这种方式。
  • 在 Creator Hub Inspector 中放置。 将资源拖入 Inspector 时,生成的组合体会带有一个 Animator,其 states 数组已包含 GLB 中的每个动画片段。使用 Inspector 的创作者不会看到注册步骤,因为编辑器会自动完成。
  • 资源包/智能物品。 这些内容的 states 已由资源包的作者填充。

「必须预先注册」的问题只会出现在**通过代码创建 + playSingleAnimation**的路径中。如果模型在没有任何代码侧 Animator.create 调用的情况下播放动画,那么它走的就是上述自动播放路径——这是预期行为。

相关说明:如果 GltfContainer 模型生成时播放了_错误_的动画片段(例如,本应待机的 NPC 却摆出死亡姿势),说明实体没有 Animator,而渲染器选择了你不想播放的动画片段。添加一个 Animator.create,并将所需的默认动画片段设为 playing: true,即可接管控制。

将动画模型停在第 1 帧(初始关闭的门/坟墓/盖子)

这是将 SDK6 项目移植到 SDK7 时的常见问题:门/坟墓/棺材的 GLB 中,「关闭」姿势是打开/触发动画片段的第 0 帧。在 SDK7 下,渲染器会在加载时自动播放一个动画片段并停留在最后一帧,因此模型生成时会处于打开状态,而不是关闭状态。

**已验证的修复方法(用户已在场景中确认):**保持某个状态处于 playing: true,但将 speed: 0 且 loop: false 冻结,这样它会停在第 0 帧——即关闭/静止姿势——同时仍是受控且处于播放状态的状态:

// Hold `clip` at its first frame (closed pose), under your control.
export function holdFirstFrame(entity: Entity, clip: string) {
  if (!Animator.has(entity)) Animator.create(entity, { states: [] })
  const a = Animator.getMutable(entity)
  if (!a.states.some((s) => s.clip === clip)) a.states = [...a.states, { clip }]
  Animator.stopAllAnimations(entity)
  const state = Animator.getClipOrNull(entity, clip)
  if (state) { state.playing = true; state.loop = false; state.speed = 0 }
}

然后,要打开时,将同一动画片段设为 speed: 1, playing: true(调用常规的 playSingleAnimation / playClip);要关闭时,则播放反向/关闭动画片段。这是观察到的行为和有效的修复方法;SDK 中没有记录渲染器自动播放哪个动画片段的具体内部机制——请将其视为渲染器行为,并针对每个模型进行验证。

最佳实践:在逐帧调用方中调用的切换动画片段辅助函数应尽早返回

适用范围: 当你编写自己的切换动画片段辅助函数,直接修改 Animator.states(s.playing、s.loop、s.shouldReset),且该函数由逐帧调用方触发时(例如 engine.addSystem 更新、每个 tick 都会触发的 inputSystem/射线检测回调,或任何逐帧运行的代码路径),本建议适用。编写此类辅助函数最常见的原因,是移植使用延迟注册动画片段或 noLoop + revertToIdle 语义的 SDK6 场景([[migrate-sdk6-to-sdk7]])。

为什么这很重要。 一个简单的辅助函数看起来可能是这样:

function playClip(entity: Entity, name: string, loop: boolean) {
  const a = Animator.getMutableOrNull(entity);
  if (!a) return;
  for (const s of a.states) {
    if (s.clip === name) {
      s.playing = true;
      s.loop = loop;
      s.shouldReset = true; // <-- rewritten every tick if helper is called per-frame
    } else {
      s.playing = false;
    }
  }
}

如果每帧都调用它,每个 tick 都会调用一次 Animator.getMutableOrNull(),从而在 CRDT 层将实体标记为脏数据。随后,ECS 会将组件序列化为字节,并与上次发送的快照进行比较(lww-element-set-component-definition.ts)。由于每帧的值都相同,CRDT 抑制层会静默丢弃更新——动画不会被冻结。不过,逐帧序列化和字节比较会带来不必要的开销,应当避免。

关于 Animator.playSingleAnimation 的说明。 根据 @dcl/ecs/src/components/extended/Animator.ts 的验证,playSingleAnimation 不是幂等操作——它会在循环中将每个状态都设为 playing=false, shouldReset=true,然后将目标状态设为 playing=true, shouldReset=<arg>。逐帧调用它会造成同样不必要的序列化开销。应优先在一次性状态切换中调用 playSingleAnimation(例如 onPointerDown、状态机边沿),而不是在系统 tick 中调用。

修复方法——跟踪上次应用的动画片段,并尽早返回:

type Anim = { entity: Entity; lastClip?: string };

function playClip(a: Anim, name: string, loop: boolean) {
  if (a.lastClip === name) return; // already applied — skip redundant serialization
  const an = Animator.getMutableOrNull(a.entity);
  if (!an) return;
  for (const s of an.states) {
    if (s.clip === name) {
      s.playing = true;
      s.loop = loop;
      s.shouldReset = true;
    } else {
      s.playing = false;
    }
  }
  a.lastClip = name;
}

尽早返回可避免在动画片段未改变的帧中调用 getMutableOrNull(),从而消除逐帧序列化开销。只有实际切换动画片段时才会发送 CRDT 增量。

移植 SDK6 时的细节——noLoop + revertToIdle。 SDK6 风格的辅助函数接收 (clipName, noLoop, durationSec),并通过 timers.setTimeout(来自 @dcl/sdk/ecs——绝不能使用原生 JS 的 setTimeout)安排计时器,以切回 idle 动画片段。它必须:

  • 再次调用时若动画片段未变(例如玩家一直按住光束,每帧都会请求播放 “Hit”),只刷新计时器(清除并重新安排),使输入持续按住期间动画片段可以不断重播。不要再次修改 Animator.states。
  • 切回 idle 的计时器触发时,在使用 idle 动画片段再次调用辅助函数之前清除 lastClip——否则,如果前一个 tick 中 lastClip 已等于 idle 名称,尽早返回会阻止应用 idle 动画片段。

何时优先使用 Animator.playSingleAnimation。 它是标准写法:一次操作即可暂停所有其他状态,并接受一个 resetCursor 参数(默认为 true)。它不是幂等的,但如果触发条件是一次性事件,这就是合适的调用位置。只有移植需要延迟注册动画片段的 SDK6 代码(参见前面的 PITFALL),或需要实现 noLoop + revertToIdle 语义时,才需要使用自定义辅助函数模式。

检测动画完成

非循环动画片段结束时,引擎会将该状态的 playing 重新设为 false——无需通过 setTimeout(clipLength) 猜测结束时间。使用只读的 Animator.get() 轮询,并检测 true → false 的边沿:

let wasPlaying = false
engine.addSystem(() => {
	const state = Animator.get(entity).states.find((s) => s.clip === 'Bite')
	const isPlaying = state?.playing ?? false
	if (wasPlaying && !isPlaying) {
		console.log('Bite finished')
		Animator.playSingleAnimation(entity, 'Walk') // chain the next clip
	}
	wasPlaying = isPlaying
})
  • 绝不要使用 Animator.getClip() 或 getMutable() 轮询——二者都会返回可变引用,并在每帧将组件标记为脏数据(会产生与上方 PITFALL 相同的序列化开销)。Animator.get() 是唯一适合逐帧读取的安全方法。
  • 只有当动画片段自行结束时,引擎才会切换该标志。循环动画片段不会切换该标志,speed: 0 的状态永远不会结束,而场景端调用 stopAllAnimations() 属于你自己的写入(如果需要区分,请自行跟踪)。
  • 将该标志视为一种通知,而非保证:绝不要让游戏逻辑依赖它而完全阻塞。如果信号遗漏会导致场景卡死,请使用基于动画片段时长设置的 timers.setTimeout 作为备用方案。

Tweens(基于代码的动画)

随时间平滑地为实体属性添加动画。使用 Tween.create(entity, { mode: Tween.Mode.Move/Rotate/Scale({start, end}), duration, easingFunction }) 创建。时长单位为毫秒(1000 = 1 秒)。一个实体同一时间只能有一个 Tween 组件。

辅助方法(直接创建或替换 Tween):

  • Tween.setMove(entity, start, end, duration, easing?)
  • Tween.setRotate(entity, start, end, duration, easing?)
  • Tween.setScale(entity, start, end, duration, easing?)
  • Tween.setMoveRotateScale(entity, { position?, rotation?, scale?, duration }) — 同时执行

连续补间(以恒定速度移动/旋转)。第三个参数是速度(每秒单位数,沿方向向量应用),而不是时长。可选的最后一个参数 duration 是停止前的时长,单位为毫秒——0 或省略表示无限运行:

  • Tween.setMoveContinuous(entity, direction: Vector3, speed: number, duration?: number) — 速度单位为米/秒
  • Tween.setRotateContinuous(entity, direction: Quaternion, speed: number, duration?: number) — speed 的单位为度/秒(完成一整圈 360° 旋转需要 360/speed 秒)。direction 四元数仅提供旋转轴(其虚部 x,y,z,保留符号);旋转角度/幅度会被忽略——Quaternion.fromEulerDegrees(0, 45, 0) 和 (0, 90, 0) 都会绕 +Y 轴旋转,速率仅由 speed 决定。负 speed 会反转方向。
  • Tween.setTextureMoveContinuous(entity, direction: Vector2, speed: number, movementType?: TextureMovementType, duration?: number) — 速度单位为 UV 单位/秒

纹理滚动(用于瀑布、传送带的 UV 动画):

  • Tween.setTextureMove(entity, start: Vector2, end: Vector2, duration, movementType?: TextureMovementType, easing?) — movementType 是第 5 个参数,easing 是第 6 个参数

控制:Tween.getMutable(entity).playing = false(暂停),.currentTime = 0(重置)。读取 Tween.getMutableOrNull(entity) 并切换 .playing,即可控制正在运行的连续补间(已在 78,-4-tweens 中验证)。使用 Tween.deleteFrom(entity) 完全移除 Tween;Tween.has(entity) 检查是否存在。

运行时重新触发/替换:如果实体可能已有 Tween,请使用 Tween.createOrReplace(entity, {...})(以及 TweenSequence.createOrReplace),而不是 create——例如仍在移动的平台再次触发时。在配置中传入 currentTime: 0,以便在补间进行到一半时从头开始(已在 77,-5-tweens-moving-platforms 中验证)。

从当前位置移动/在移动途中更改目标:将 Transform.get(entity).position 作为 start 传入——补间活动期间,引擎每帧都会将实体插值后的 Transform 写回场景,因此读取到的是移动途中的实时位置。这样,Tween.setMove(entity, Transform.get(entity).position, target, duration, easing) 就会让实体从当前位置开始移动;即使之前的补间仍在运行,再次调用此方法也会平滑地将实体改向新目标,不会突然跳变或瞬移(set* 辅助方法使用 createOrReplace,即使值相同也会重新触发)(已在 79,-4-tween-following-cube 中验证)。

注意事项——Move 中省略 start 并不表示“当前位置”:未设置的 start 会被视为 (0,0,0)——实体会先瞬移到场景原点,再开始移动。使用过时的硬编码 start 同样会导致实体在移动开始前明显瞬移到该位置。务必提供 start;如果要表示“从当前位置开始”,请读取 Transform.get(entity).position(已在 79,-4-tween-following-cube 中验证)。 注意 — 对于不断变化的目标,应使用 setMoveContinuous,而不是反复调用 setMove:每帧重新创建一个 Move 补间来追逐移动目标(跟随玩家、追踪逃跑的实体),会让实体明显卡顿。Transform.get(entity).position 是渲染器最近写回 CRDT 的位置,因此会比实体的实际位置滞后约 1–3 帧;新 start 一到达,渲染器就会立即应用它,导致实体每次重新瞄准时都向后跳。修正一次不会被察觉,每秒修正约 30 次就会产生抖动;提高重新瞄准阈值只会让卡顿更明显,因为问题在于替换的频率。请改用 Tween.setMoveContinuous(entity, direction, speed):连续模式会从渲染器自身的实时变换获取起点,因此在运动中替换它们不会造成跳动。注意:它没有目标位置(请在距离检查时用 Tween.deleteFrom 停止它,或传入可选的停止后 duration),并且只有在方向发生明显变化时才应重新瞄准,否则每帧都会发送一次 CRDT 更新。离散式重新设定目标(点击、路径点)仍可使用 setMove(已在 79,-4-tween-following-cube 中验证,该示例会实时切换两种模式)。

Tween.Mode.MoveRotateScale({...}) 和 setMoveRotateScale 接受部分变换 — 只需提供所需的轴(position / rotation / scale);至少需要提供一个。未提供的轴保持不变(已通过 SDK 源码和 78,-4-tweens 验证)。自移动应用 v1.15.0 起支持移动端(文档提交 5f212e3);在较早的移动端版本中,它不会执行动画。

纹理移动类型(setTextureMove / setTextureMoveContinuous,以及 Tween.Mode.TextureMove):movementType 参数可以是 TextureMovementType.TMT_OFFSET(默认值,=0 — 滚动 UV 偏移)或 TextureMovementType.TMT_TILING(=1 — 为平铺设置动画)。从 @dcl/sdk/ecs 导入 TextureMovementType。若要让连续滚动无缝平铺,纹理必须使用 wrapMode: TextureWrapMode.TWM_REPEAT(已在 0,3-texture-movement、78,-4-tweens 中验证)。

Tween 序列(串联动画)

使用 TweenSequence.create(entity, { sequence: [...tweenConfigs], loop }) 进行串联。循环模式:TweenLoop.TL_RESTART(=0,从头循环)、TweenLoop.TL_YOYO(=1,到达每个端点时反向)。

⚠️ 移动端客户端尚未实现 TweenSequence — 序列补间不会执行动画。 这不是计时或缓动效果不同:实体完全不会移动。普通 Tween 在移动端可用(包括自移动应用 v1.15.0 起支持的 MoveRotateScale);缺失的是序列包装器,目前没有预计完成时间(文档提交 5f212e3,于 2026 年 10 月审查 / v1.15.0)。这会影响手机上最常见的两种用途:循环移动平台和巡逻路径。如果场景面向移动端:

  • 不要使用 TweenSequence 来构建通行所需的几何结构。 如果穿越缺口的唯一方式是移动平台,那么该场景在移动端将无法通行。
  • 改为通过系统驱动重复运动和多段运动:每次运行一个普通 Tween,并在 tweenSystem.tweenCompleted(entity) 时启动下一段。此方式兼容移动端,只需几行代码。
  • 装饰性序列(漂浮的云朵、空闲时的上下浮动)会退化为静态物件 — 可以接受,但请确认场景在它们静止时仍然清晰易懂。

这也包括下方的空 sequence 循环惯用写法,它本身也只是 TweenSequence 的一种用法 [根据“尚未实现”推断;文档未单独说明空序列的情况 — 如果场景依赖此功能,请在设备上验证]。

模式:空序列会循环基础 Tween

对已有普通 Tween 的实体调用 TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_YOYO }),会让基础 tween 本身循环并往返播放,无需序列步骤。这是让单个 setMove/setRotate/setScale/setTextureMove 永久重复播放的惯用方式(已在 77,-5-tweens-moving-platforms、0,3-texture-movement 中验证)。往返运动(平台上下浮动)使用 TL_YOYO,每个周期硬重置(滚动纹理、单向旋转)则使用 TL_RESTART。

对于多路点路径,基础 Tween 是第一段,sequence[] 保存其余部分;每个步骤的 start/end 必须从前一段的终点接续(已在 77,-5-tweens-moving-platforms 的 platform4 中验证)。

检测 Tween 是否完成

在 engine.addSystem() 中使用 tweenSystem.tweenCompleted(entity),检查 tween 是否在本帧完成。

缓动函数

EasingFunction 提供:EF_LINEAR、EF_EASEINQUAD/EASEOUTQUAD/EASEQUAD、EF_EASEINSINE/EASEOUTSINE/EASESINE、EF_EASEINEXPO/EASEOUTEXPO/EASEEXPO、EF_EASEINELASTIC/EASEOUTELASTIC/EASEELASTIC、EF_EASEOUTBOUNCE/EASEINBOUNCE/EASEBOUNCE、EF_EASEINBACK/EASEOUTBACK/EASEBACK、EF_EASEINCUBIC/EASEOUTCUBIC/EASECUBIC、EF_EASEINQUART/EASEOUTQUART/EASEQUART、EF_EASEINQUINT/EASEOUTQUINT/EASEQUINT、EF_EASEINCIRC/EASEOUTCIRC/EASECIRC。

自定义动画系统

对于复杂动画,使用 engine.addSystem((dt) => { ... }) 创建系统,并在每一帧修改 Transform.getMutable(entity)。使用自定义组件(例如 Spinner)标记需要动画的实体。

故障排除

问题原因解决方案
GLTF 动画不播放动画片段名称错误在查看器中检查准确的动画片段名称(区分大小写)
playSingleAnimation 不起作用,返回 false对于通过代码创建的 Animator,动画片段名称不在其 Animator.states 中在调用 Animator.create 时将动画片段添加到 states[],或使用上面的延迟添加封装。仅适用于以编程方式创建 Animator 的情况——Inspector / 资源包中的 Animator 已预先填充
模型生成时自动播放了意外的动画没有 Animator 组件——GltfContainer 会自动播放 .glb 中的一个动画片段(这与不含动画片段的 Inspector 场景无需手动注册即可播放动画的机制相同)添加 Animator.create,并将预期的默认动画片段设为 playing: true,以接管控制
门 / 墓穴 / 盖子生成时处于打开状态渲染器会自动播放动画片段并停留在其最后一帧(打开状态);模型的闭合姿势是该动画片段的第 0 帧使用 playing: true, speed: 0, loop: false 状态停留在第 0 帧——参见上文“让动画模型停留在第一帧”
不必要的逐帧序列化开销动画片段切换辅助函数每个 tick 都会以相同的值调用 getMutableOrNull(自定义辅助函数,或在系统 / 逐帧输入回调中调用 playSingleAnimation)记录上次应用的动画片段,并在未改变时提前返回;或者只在状态转换时调用辅助函数——参见上文最佳实践“提前返回的动画片段切换辅助函数”
Animator 没有效果缺少 GltfContainerAnimator 仅适用于已加载 GLTF 模型的实体
需要知道非循环动画片段何时结束通过 timers.setTimeout(clipLength) 猜测通过只读的 Animator.get() 检测 playing 变为 false——参见上文“检测动画完成”
Tween 没有移动起点和终点相同在 Tween.Mode.Move() 中确认值不同
Move tween 开始时实体发生瞬移start 与实体当前位置不一致——省略 start 时为 (0,0,0)(场景原点),硬编码 / 过时的 start 则是你写入的位置将 Transform.get(entity).position 作为 start 传入——即使上一个 tween 正在播放,它也始终是实时位置
Tween 播放一次后停止没有循环添加 TweenSequence,并设置 loop: TweenLoop.TL_YOYO
动画抖动每帧都创建 Tween只创建一次 Tween,不要在系统中创建
玩家看到场景时,开场动画 / tween 已经播放完毕它在场景启动时就开始播放,并在 Explorer 的加载屏幕后面播放完了等待 EngineInfo.sceneHidden 变为 false(加载屏幕淡出)后再启动——参见 scene-runtime 技能
有关完整代码示例(Animator 设置、所有补间类型、序列、辅助函数、纹理滚动),请参阅 {baseDir}/references/animation-patterns.md。

示例场景

Engine 团队的测试场景(权威 API 用法):

  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/73,-8-animator-tests-shark — 使用两个状态和 weight 的 Animator;点击时循环调用 playSingleAnimation('swim') / ('bite'),并通过同时设置 getClip('bite').playing = true 演示并发混合。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/73,-9-animation-finish — 动画完成检测:非循环的 bite 播放结束后会将 playing 设为 false(通过只读的 Animator.get() 观察),随后衔接播放 swim;证明循环片段永远不会报告播放结束。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/78,-4-tweens — 展示所有有限模式(setMove/setRotate/setScale/setTextureMove/setMoveRotateScale)、连续模式、createOrReplace、deleteFrom、通过 getMutableOrNull().playing 切换暂停,以及混合模式的 TweenSequence。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/77,-5-tweens-moving-platforms — 使用 Tween.setMove 和空序列 TL_YOYO 制作上下浮动的平台,通过 sequence[] 实现多路点路径,并通过 TriggerArea 使用 createOrReplace 重新触发。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/79,-4-tween-following-cube — 一个追逐玩家的立方体,以及一个可以实时切换两种模式的踏板:setMoveContinuous(平滑,默认模式)和每次重新瞄准时基于 Transform.get(entity).position 重新创建 setMove 补间(可见抖动)。两种模式共用相同的重新瞄准触发器和速度,因此差异只归因于补间模式本身。还展示了浮动标签上使用 BM_Y 的 Billboard。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,3-texture-movement — 使用 TMT_OFFSET 与 TMT_TILING 的 setTextureMove、空序列循环/往返,以及 TWM_REPEAT 平铺。
  • https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/10,0-move-to-test — 自定义的逐帧旋转系统(Quaternion.multiply + fromAngleAxis(dt*speed, Up())),使用 Spinner组件标记实体。(其中的movePlayerTo` 属于受限操作,而非补间。)

评论

支持 Markdown