mcpField
A
animations-tweens

animations-tweens

github.com/dcl-regenesislabsDevelopment1738 installsUpdated 21w ago
Repository

About animations-tweens

animations-tweens is a Decentraland development skill for animating entities and GLTF models with Animator, Tween, TweenSequence, and custom systems. It explains when to use baked model clips, smooth property changes, chained motion, or per-frame control.

Key features

  1. 01Play and switch GLTF animation clips
  2. 02Tween position, rotation, and scale
  3. 03Chain tweens with TweenSequence
  4. 04Choose easing and loop modes
  5. 05Control morph target weights
  6. 06Build custom per-frame animation systems

Use cases

  1. 01Animate a character using GLTF clips
  2. 02Move or rotate a door between positions
  3. 03Create a looping patrol path
  4. 04Drive model facial expressions with morph targets

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/dcl-regenesislabs/opendcl --skill animations-tweens

After installing, type/animations-tweensin Claude Code to confirm it loaded.

Details

From the project README

Animations and Tweens in Decentraland

Authoring split

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.

When to Use Which Animation Approach

NeedApproachWhen
Play animation baked into a .glb modelAnimatorCharacter walks, door opens, flag waves — any animation created in Blender/Maya
Move/rotate/scale an entity smoothlyTweenSliding doors, floating platforms, growing objects — procedural A-to-B motion
Chain multiple animations in sequenceTweenSequencePatrol paths, multi-step doors, complex choreography
Continuous per-frame controlengine.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)

Declare 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)
}
Switching Animations
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
}

Tweens

Tween and TweenSequence are JSON-compatible. The mode is a $case-tagged union; positions/quaternions are plain object literals; easing is a numeric enum.

Move
// 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.

Rotate
// 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
  })
}
Scale
// 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 } }
      }
    }
  }
}

Tween Sequences (Chained Animations)

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 } } }
        }
      ]
    }
  }
}

Easing Functions

Inside main-entities.ts use the integer values (left). In src/index.ts you can reference EasingFunction.<NAME> directly.

valueenumshape
0EF_LINEARConstant speed
1EF_EASEINQUADQuadratic in
2EF_EASEOUTQUADQuadratic out
3EF_EASEQUADQuadratic in-out
4EF_EASEINSINESinusoidal in
5EF_EASEOUTSINESinusoidal out
6EF_EASESINESinusoidal in-out (smooth)
7EF_EASEINEXPOExponential in
8EF_EASEOUTEXPOExponential out
9EF_EASEEXPOExponential in-out
10EF_EASEINELASTICElastic in
11EF_EASEOUTELASTICElastic out
12EF_EASEELASTICElastic in-out
13EF_EASEINBOUNCEBounce in
14EF_EASEOUTBOUNCEBounce out
15EF_EASEBOUNCEBounce in-out
16EF_EASEINCUBICCubic in
17EF_EASEOUTCUBICCubic out
18EF_EASECUBICCubic in-out
19EF_EASEINQUARTQuartic in
20EF_EASEOUTQUARTQuartic out
21EF_EASEQUARTQuartic in-out
22EF_EASEINQUINTQuintic in
23EF_EASEOUTQUINTQuintic out
24EF_EASEQUINTQuintic in-out
25EF_EASEINCIRCCircular in
26EF_EASEOUTCIRCCircular out
27EF_EASECIRCCircular in-out
28EF_EASEINBACKOvershoot in
29EF_EASEOUTBACKOvershoot out
30EF_EASEBACKOvershoot in-out

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
)

// 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)).

Yoyo Loop Mode

TL_YOYO reverses the tween 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%
Morph Targets (Blend Shapes)

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%
}

Troubleshooting

ProblemCauseSolution
GLTF animation not playingWrong clip name in Animator.statesOpen the .glb in a viewer (e.g., Blender) to find exact clip names — they are case-sensitive
Animator component has no effectEntity missing GltfContainerAnimator only works on entities that have a loaded GLTF model
Tween doesn't moveStart and end positions are the sameVerify start and end values differ in Tween.Mode.Move()
Tween plays once then stopsNo TweenSequence with loopAdd TweenSequence.create(entity, { sequence: [], loop: TweenLoop.TL_YOYO }) for back-and-forth
Animation jitters or stuttersCreating new Tween every frameOnly 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 or physics-based animations
  • Tween durations are in milliseconds (1000 = 1 second)
  • Combine move + rotate tweens by applying them to parent/child entities
  • For looping: use TweenSequence with loop: TweenLoop.TL_RESTART

Comments

Markdown supported