animations-tweens
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:
- Does the .glb model already have the animation? →
Animator - Is it a simple move/rotate/scale between two values? →
Tween - 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 speedEF_EASEINQUAD/EF_EASEOUTQUAD/EF_EASEQUAD— QuadraticEF_EASEINSINE/EF_EASEOUTSINE/EF_EASESINE— Sinusoidal (smooth)EF_EASEINEXPO/EF_EASEOUTEXPO/EF_EASEEXPO— ExponentialEF_EASEINELASTIC/EF_EASEOUTELASTIC/EF_EASEELASTIC— Elastic bounceEF_EASEOUTBOUNCE/EF_EASEINBOUNCE/EF_EASEBOUNCE— Bounce effectEF_EASEINBACK/EF_EASEOUTBACK/EF_EASEBACK— OvershootEF_EASEINCUBIC/EF_EASEOUTCUBIC/EF_EASECUBIC— CubicEF_EASEINQUART/EF_EASEOUTQUART/EF_EASEQUART— QuarticEF_EASEINQUINT/EF_EASEOUTQUINT/EF_EASEQUINT— QuinticEF_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
TweenSequencewithloop: TweenLoop.TL_RESTART
More from decentraland/sdk-skills
decentraland-sdk-skills
Build, extend, and deploy Decentraland SDK7 scenes. Contains agent behavioral guidelines, the composite-first rule, and an index of all topic skills with reference files. Install with a single command — no flags needed.
20build-ui
Build 2D screen-space UI for Decentraland scenes using React-ECS (JSX). Create HUDs, menus, health bars, scoreboards, dialogs, buttons, inputs, and dropdowns. Use when the user wants screen overlays, on-screen UI, HUD elements, menus, or form inputs. Do NOT use for 3D in-world text (see advanced-rendering) or clickable 3D objects (see add-interactivity).
3advanced-input
System-level input polling and player movement control in Decentraland. Covers inputSystem (isTriggered/isPressed for held keys, WASD polling), InputModifier (freeze/restrict player movement), PointerLock (cursor capture detection), PrimaryPointerInfo (cursor screen coords and world ray), and number-key action bar patterns. Use when the user wants continuous key polling, WASD-controlled entities, to freeze the player during a cutscene, FPS-style cursor lock, or multi-key combo patterns. For event-driven clicks and hover on entities see add-interactivity.
3nft-blockchain
NFT display and blockchain interaction in Decentraland. NftShape (framed NFT artwork), wallet checks (getPlayer, isGuest), signedFetch (authenticated requests), smart contract interaction (eth-connect, createEthereumProvider), and RPC calls. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, crypto, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
3player-physics
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), timed forces, and repulsion fields. Use when the user wants launch pads, knockback on hit, wind zones, gravity fields, or any scene-applied force on the player. Do NOT use for player movement speed (see player-avatar) or platform movement (see animations-tweens).
3camera-control
Control camera behavior in Decentraland scenes. CameraMode detection (first/third person, onChange listener), CameraModeArea (force a mode inside a box), VirtualCamera (cinematic scripted cameras with Speed/Time transitions and lookAtEntity), MainCamera (activate/deactivate virtual cameras), and camera vs collider interactions (CL_PHYSICS + CL_POINTER). Use when the user wants camera control, cutscenes, cinematic views, forced camera modes, or camera tracking. Do NOT use for input restriction during cutscenes (see advanced-input for InputModifier) or cursor lock detection (see advanced-input for PointerLock).
3