animations-tweens

Installation
SKILL.md

Animations and Tweens in Decentraland

When to Use Which Animation Approach

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:

  1. Does the .glb model already have the animation? → Animator
  2. Is it a simple move/rotate/scale between two values? → Tween
  3. Do you need frame-by-frame control or custom math? → System with dt

GLTF Animations (Animator)

Play animations embedded in .glb models:

import { engine, Transform, GltfContainer, Animator } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const character = engine.addEntity()
Transform.create(character, { position: Vector3.create(8, 0, 8) })
GltfContainer.create(character, { src: 'models/character.glb' })

// Set up animation states
Animator.create(character, {
  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 }
  ]
})

// Play a specific animation
Animator.playSingleAnimation(character, 'walk')

// Stop all animations
Animator.stopAllAnimations(character)

Switching Animations

function playAnimation(entity: Entity, clipName: string) {
  const animator = Animator.getMutable(entity)
  // Stop all
  for (const state of animator.states) {
    state.playing = false
  }
  // Play the desired one
  const state = animator.states.find(s => s.clip === clipName)
  if (state) {
    state.playing = true
  }
}

Tweens (Code-Based Animation)

Animate entity properties smoothly over time:

Move

import { engine, Transform, Tween, EasingFunction } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'

const box = engine.addEntity()
Transform.create(box, { position: Vector3.create(2, 1, 8) })

Tween.create(box, {
  mode: Tween.Mode.Move({
    start: Vector3.create(2, 1, 8),
    end: Vector3.create(14, 1, 8)
  }),
  duration: 2000,  // milliseconds
  easingFunction: EasingFunction.EF_EASESINE
})

Rotate

Tween.create(box, {
  mode: Tween.Mode.Rotate({
    start: Quaternion.fromEulerDegrees(0, 0, 0),
    end: Quaternion.fromEulerDegrees(0, 360, 0)
  }),
  duration: 3000,
  easingFunction: EasingFunction.EF_LINEAR
})

You can also use tweens for a continuous rotation:

Tween.setRotateContinuous(myEntity, 
	Quaternion.fromEulerDegrees(0, -1, 0), 
	700
)

Scale

Tween.create(box, {
  mode: Tween.Mode.Scale({
    start: Vector3.create(1, 1, 1),
    end: Vector3.create(2, 2, 2)
  }),
  duration: 1000,
  easingFunction: EasingFunction.EF_EASEOUTBOUNCE
})

### Multiple transformations

If an entity needs to tween in any combination of position, scale, or rotation, you can achieve multiple simultaneous changes using Tween.setMoveRotateScale. An entity can only have one Tween compoent at a time.

Tween.setMoveRotateScale(mrsEntity, {
	position: { start: Vector3.create(14, 1, 2), end: Vector3.create(14, 3, 2) },
	rotation: { start: Quaternion.fromEulerDegrees(0, 0, 0), end: Quaternion.fromEulerDegrees(0, 180, 90) },
	scale: { start: Vector3.One(), end: Vector3.create(2, 0.5, 2) },
	duration: 2000
})

Tween Sequences (Chained Animations)

Chain multiple tweens to play one after another:

import { TweenSequence, TweenLoop } from '@dcl/sdk/ecs'

// First tween
Tween.create(box, {
  mode: Tween.Mode.Move({
    start: Vector3.create(2, 1, 8),
    end: Vector3.create(14, 1, 8)
  }),
  duration: 2000,
  easingFunction: EasingFunction.EF_EASESINE
})

// Chain sequence
TweenSequence.create(box, {
  sequence: [
    // Second: move back
    {
      mode: Tween.Mode.Move({
        start: Vector3.create(14, 1, 8),
        end: Vector3.create(2, 1, 8)
      }),
      duration: 2000,
      easingFunction: EasingFunction.EF_EASESINE
    }
  ],
  loop: TweenLoop.TL_RESTART // Loop the entire sequence
})

Easing Functions

Available easing functions from EasingFunction:

  • EF_LINEAR — Constant speed
  • EF_EASEINQUAD / EF_EASEOUTQUAD / EF_EASEQUAD — Quadratic
  • EF_EASEINSINE / EF_EASEOUTSINE / EF_EASESINE — Sinusoidal (smooth)
  • EF_EASEINEXPO / EF_EASEOUTEXPO / EF_EASEEXPO — Exponential
  • EF_EASEINELASTIC / EF_EASEOUTELASTIC / EF_EASEELASTIC — Elastic bounce
  • EF_EASEOUTBOUNCE / EF_EASEINBOUNCE / EF_EASEBOUNCE — Bounce effect
  • EF_EASEINBACK / EF_EASEOUTBACK / EF_EASEBACK — Overshoot
  • EF_EASEINCUBIC / EF_EASEOUTCUBIC / EF_EASECUBIC — Cubic
  • EF_EASEINQUART / EF_EASEOUTQUART / EF_EASEQUART — Quartic
  • EF_EASEINQUINT / EF_EASEOUTQUINT / EF_EASEQUINT — Quintic
  • EF_EASEINCIRC / EF_EASEOUTCIRC / EF_EASECIRC — Circular

Custom Animation Systems

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)

Tween Helper Methods

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
)

Continuous Tweens

Tween.setMoveContinuous and Tween.setRotateContinuous keep moving/rotating by a relative delta every cycle — no explicit start/end needed. Use for conveyor belts, idle spinning objects, or looping motion:

// Move by (0, 0, 1) every 2 seconds, forever
Tween.setMoveContinuous(entity, Vector3.create(0, 0, 1), 2000)

// Rotate 90° around Y every 2 seconds, forever
Tween.setRotateContinuous(entity, Quaternion.fromEulerDegrees(0, 90, 0), 2000)

Texture Scrolling

Animate a material's texture UV offset — useful for waterfalls, conveyor belts, scrolling signs:

import { Vector2 } from '@dcl/sdk/math'

// From UV (0,0) to (1,0) over 2 seconds
Tween.setTextureMove(entity, Vector2.create(0, 0), Vector2.create(1, 0), 2000)

// Continuous scroll — shift UV by (0, 1) every 2 seconds, forever
Tween.setTextureMoveContinuous(entity, Vector2.create(0, 1), 2000)

Pause / Reset a Tween

Mutate the Tween component to pause playback or scrub to a specific time:

const tween = Tween.getMutable(entity)
tween.playing = false   // pause
tween.currentTime = 0   // reset to beginning
tween.playing = true    // resume

Yoyo Loop Mode

TL_YOYO reverses the tween sequence at each end instead of restarting:

TweenSequence.create(entity, {
  sequence: [{ duration: 1000, ... }],
  loop: TweenLoop.TL_YOYO
})

Detecting Tween Completion

Use tweenSystem.tweenCompleted() to check if a tween finished this frame:

engine.addSystem(() => {
  if (tweenSystem.tweenCompleted(entity)) {
    console.log('Tween finished on', entity)
  }
})

Animator Extras

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%

Troubleshooting

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.

Best Practices

  • Use Tweens for simple A-to-B animations (doors, platforms, UI elements)
  • Use Animator for character/model animations baked into GLTF files
  • Use Systems for continuous user control or physics-based animations
  • Tween durations are in milliseconds (1000 = 1 second)
  • For looping: use TweenSequence with loop: TweenLoop.TL_RESTART
Related skills

More from decentraland/sdk-skills

Installs
3
GitHub Stars
3
First Seen
Apr 13, 2026