skills/animations-tweens/SKILL.md
Animate objects in Decentraland scenes. Play GLTF model animations with Animator, create procedural motion with Tween (move/rotate/scale), and chain sequences with TweenSequence. Use when the user wants to animate, move, rotate, spin, slide, bob, or create motion effects. Do NOT use for audio/video playback (see audio-video).
npx skillsauth add dcl-regenesislabs/opendcl animations-tweensInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Animator, Tween, and TweenSequence are all supported in main-entities.ts — declare the entity, its visual components, and its initial animation state in the literal. Switch clips or replace tweens at runtime in src/index.ts via getMutable.
| Need | Approach | When |
|------|----------|------|
| Play animation baked into a .glb model | Animator | Character walks, door opens, flag waves — any animation created in Blender/Maya |
| Move/rotate/scale an entity smoothly | Tween | Sliding doors, floating platforms, growing objects — procedural A-to-B motion |
| Chain multiple animations in sequence | TweenSequence | Patrol paths, multi-step doors, complex choreography |
| Continuous per-frame control | engine.addSystem() | Physics-like motion, following a target, custom easing |
Decision flow:
AnimatorTweendtDeclare the character and its animation states in main-entities.ts. The states array is part of the Animator shape and is JSON-compatible.
// 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
Trigger and switch animations at runtime in 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 and TweenSequence are JSON-compatible. The mode is a $case-tagged union; positions/quaternions are plain object literals; easing is a numeric enum.
// 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 } }
}
}
}
}
Common easing values: 0 = EF_LINEAR, 1 = EF_EASEINQUAD, 2 = EF_EASEOUTQUAD, 3 = EF_EASEQUAD, 6 = EF_EASESINE. To replace or stop a tween at runtime, use Tween.createOrReplace / Tween.deleteFrom on the named entity in src/index.ts.
// 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 }
}
}
}
}
}
For exact-degree rotations without hand-computing quaternions, leave the tween out of main-entities.ts and create it at runtime in src/index.ts where Quaternion.fromEulerDegrees() is available:
// 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 } }
}
}
}
}
Chain multiple tweens to play one after another. TweenSequence is supported in main-entities.ts:
// 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 } } }
}
]
}
}
}
Inside main-entities.ts use the integer values (left). In src/index.ts you can reference EasingFunction.<NAME> directly.
| value | enum | shape | |---|---|---| | 0 | EF_LINEAR | Constant speed | | 1 | EF_EASEINQUAD | Quadratic in | | 2 | EF_EASEOUTQUAD | Quadratic out | | 3 | EF_EASEQUAD | Quadratic in-out | | 4 | EF_EASEINSINE | Sinusoidal in | | 5 | EF_EASEOUTSINE | Sinusoidal out | | 6 | EF_EASESINE | Sinusoidal in-out (smooth) | | 7 | EF_EASEINEXPO | Exponential in | | 8 | EF_EASEOUTEXPO | Exponential out | | 9 | EF_EASEEXPO | Exponential in-out | | 10 | EF_EASEINELASTIC | Elastic in | | 11 | EF_EASEOUTELASTIC | Elastic out | | 12 | EF_EASEELASTIC | Elastic in-out | | 13 | EF_EASEINBOUNCE | Bounce in | | 14 | EF_EASEOUTBOUNCE | Bounce out | | 15 | EF_EASEBOUNCE | Bounce in-out | | 16 | EF_EASEINCUBIC | Cubic in | | 17 | EF_EASEOUTCUBIC | Cubic out | | 18 | EF_EASECUBIC | Cubic in-out | | 19 | EF_EASEINQUART | Quartic in | | 20 | EF_EASEOUTQUART | Quartic out | | 21 | EF_EASEQUART | Quartic in-out | | 22 | EF_EASEINQUINT | Quintic in | | 23 | EF_EASEOUTQUINT | Quintic out | | 24 | EF_EASEQUINT | Quintic in-out | | 25 | EF_EASEINCIRC | Circular in | | 26 | EF_EASEOUTCIRC | Circular out | | 27 | EF_EASECIRC | Circular in-out | | 28 | EF_EASEINBACK | Overshoot in | | 29 | EF_EASEOUTBACK | Overshoot out | | 30 | EF_EASEBACK | Overshoot in-out |
For complex animations, create a system:
// 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)
Use shorthand helpers that create or replace the Tween component directly on the entity:
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 variants don't end — they apply a constant delta until the Tween is removed (Tween.deleteFrom(entity)).
TL_YOYO reverses the tween at each end instead of restarting:
TweenSequence.create(entity, {
sequence: [{ duration: 1000, ... }],
loop: TweenLoop.TL_YOYO
})
Use tweenSystem.tweenCompleted() to check if a tween finished this frame:
engine.addSystem(() => {
if (tweenSystem.tweenCompleted(entity)) {
console.log('Tween finished on', entity)
}
})
Additional Animator features:
// 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%
If a GLTF model exports morph targets (e.g., facial expressions, shape blends), drive them via the weights array on each Animator.state. Indices match the order morph targets were exported. Values range 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%
}
| Problem | Cause | Solution |
|---------|-------|----------|
| GLTF animation not playing | Wrong clip name in Animator.states | Open the .glb in a viewer (e.g., Blender) to find exact clip names — they are case-sensitive |
| Animator component has no effect | Entity missing GltfContainer | Animator only works on entities that have a loaded GLTF model |
| Tween doesn't move | Start and end positions are the same | Verify start and end values differ in Tween.Mode.Move() |
| Tween plays once then stops | No TweenSequence with loop | Add TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_YOYO }) for back-and-forth |
| Animation jitters or stutters | Creating new Tween every frame | Only create Tween once, not inside a system — use tweenSystem.tweenCompleted() to chain |
Need 3D models to animate? See the add-3d-models skill for loading GLTF models that contain animation clips.
TweenSequence with loop: TweenLoop.TL_RESTARTdevelopment
Capture screenshots of the running Decentraland preview to verify scene changes visually. Covers camera movement, interaction actions, and visual debugging. Use when the preview is running and you need to check what the scene looks like, debug visual issues, or verify layout. Do NOT use for code changes (make changes first, then screenshot).
development
Cross-cutting runtime APIs for Decentraland SDK7 scenes. Use when the user needs async operations (executeTask), HTTP requests (fetch, signedFetch), WebSocket connections, timers, realm/scene detection, restricted actions (movePlayerTo, teleportTo, triggerEmote, openExternalUrl), portable experiences, or the testing framework. Do NOT use for UI (see build-ui), multiplayer sync (see multiplayer-sync), or avatar/player data (see player-avatar).
development
Apply physics forces to the player in Decentraland scenes. Impulses (one-shot pushes), knockback (push away from a point with falloff), continuous forces (wind tunnels, anti-gravity, lift, levitation, hover), timed forces, and repulsion fields. Use when the user wants launch pads, knockback on hit, wind zones, gravity fields, jumps, lifting/floating the player, pushing the player up/sideways/back, hover effects, or any scene-applied force on the player. THIS is also the right skill when an agent's first instinct is to mutate `Transform` on `engine.PlayerEntity` to move/lift/push the player — that does NOT work (the player Transform is engine-controlled and read-only); use the Physics API instead. Do NOT use for player movement speed (see player-avatar AvatarLocomotionSettings) or platform movement (see animations-tweens).
data-ai
Player and avatar system in Decentraland. Read player position/profile, customize appearance (AvatarBase), trigger emotes (triggerEmote/triggerSceneEmote), read equipped wearables (AvatarEquippedData), attach objects to players (AvatarAttach), create NPC avatars (AvatarShape), avatar modifier areas, and locomotion settings. Use when the user wants player data, emotes, wearables, NPC avatars, avatar attachments, or movement speed changes. Do NOT use for wallet/blockchain interactions (see nft-blockchain).