Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions content/ADR-285-continuous-tweens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
layout: adr
adr: 285
title: Continuous tweens

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By definition a tween is an interpolation, and naming a tween "continuous tween" is in conflict with "linear tweens", besides not reflecting the true nature of what the rest of the component does.

This ADR suits best a brand new component that may be called "Animation", "TransformAnimation" or similar as the semantics are different from a tween(not lerping/interpolating) and closer to CSS animations.

https://en.m.wikipedia.org/wiki/Inbetweening

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pravusjif @nearnshaw JIC you didn't see this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @menduz
sorry yes, we saw it
Yes it's technically true that a continuous tween is an oxymoron
But on the other hand, we felt it was a lot more discoverable to have this functionality in the Tween component. Writing Tween. and seeing all the options is way better than having to remember that a different component name exists for when you want this other behavior.
So we prioritized ease of use over technical correctness

The feature is already developed and in production BTW
https://docs.decentraland.org/creator/development-guide/sdk7/move-entities/#constant-rotation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nearnshaw the link 404

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

date: 2025-07-31
status: Living
type: RFC
spdx-license: CC0-1.0
authors:
- nearnshaw
---

# Abstract

This specification defines how to extend the Tween component to support continuous, infinite or time-bound motion types that move or rotate entities at a constant rate. These new modes include `RotateContinuous`, `MoveContinuous`, and `TextureMoveContinuous`. The motion is defined by a direction and a speed, rather than a start/end pair. The specification also outlines protocol changes, serialization details, and runtime behavior.

Currently, the Tween component supports only tweens with a start and end value, which is suitable for discrete, finite transitions (e.g., moving from position A to B or rotating from angle X to Y). However, many game elements — such as rotating collectibles, moving textures (e.g., waterfalls), or floating platforms — require continuous, direction-based motion at a fixed speed, not based on a start-end interpolation.

The current workaround (chaining multiple finite tweens) introduces complexity for creators and causes visual artifacts due to frame-precision issues and unnatural looping.

## Decision

We will introduce three new continuous tween modes to the Tween component:

- `RotateContinuous`
- `MoveContinuous`
- `TextureMoveContinuous`
Comment on lines +23 to +27

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d like to start a conversation about an alternative approach—similar to how Unity’s DOTween works.

The idea is to introduce the following new modes to the Tween component:

message MoveIncrement {
  decentraland.common.Vector3 value = 1;
}

message RotateIncrement {
  decentraland.common.Quaternion value = 1;
}

message ScaleIncrement {
  decentraland.common.Vector3 value = 1;
}

message TextureMoveIncrement {
  decentraland.common.Vector2 value = 1;
  optional TextureMovementType movement_type = 2; // default = TextureMovementType.TMT_OFFSET
}

In addition, we could introduce a new loop mode for the tween sequence: TextureLoop.INCREMENTAL.

Here’s how it would work:

Tween.create(entity, {
  mode: Tween.Mode.MoveIncrement({ value: Vector3.create(10, 10, 10), duration: 2.0 })
})

This would move the entity by 10 units along all axes over 2 seconds—executed once.

If used in a sequence with different loop modes:

  • RESTART: It moves the entity by 10 units, then immediately resets (subtracts the same amount), and starts over.
  • YOYO: It moves the entity by 10 units, then reverses by the same amount (like a ping-pong).
  • INCREMENTAL (new): It moves the entity by 10 units, and when it ends, it repeats the same increment again.

So, using RESTART and YOYO, you can achieve the same results as with the existing non-incremental modes (START/END), but instead of moving between fixed points, the effect is achieved through value addition and subtraction.

This would still work with the existing duration and easing_function parameters, and I believe it would be clear and intuitive for creators to use.

What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @kuruk-mm
I think this is a good addition, I see it being useful
It doesn't fix the original problem that I tried to solve with this ADR, which is to prevent glitches like these:

spinning.carpincho.1.mov
Screen.Recording.2025-07-31.at.11.01.52.AM.mov

Unless we also move the interpretation of the TweenSequence to the engine, which is something that we should do eventually, but at least short term I don't think we have the bandwidth for it in the Foundation.

Both approaches could coexist I think, we don't need to pick just one or the other.

@kuruk-mm kuruk-mm Aug 7, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ADR should not address that issue. It is focused on introducing new functionality (incremental/continuous tweens).

To resolve the sequence issue, we should push TweenSequence into the engine.


These modes enable perpetual, direction-based motion at a fixed speed, optionally for a finite duration. They improve usability and output quality by delegating the motion to engine-level logic, ensuring smooth, jitter-free behavior.

## RotateContinuous


The `RotateContinuous` mode allows to rotate an entity at a constant speed in a given direction. The rotation is defined by a direction and a speed.

The `RotateContinuous` mode will have the following fields:

- `direction`: _Quaternion_, the direction of the rotation
- `speed`: _number_, the speed of the rotation in radians per second
- `duration`: _number_, the duration of the rotation in milliseconds
- `easingFunction`: _EasingFunction_ (optional), the easing function to use for the rotation

Note: The `duration` field is optional. If it is not specified, the rotation will be continuous and infinite. The `easingFunction` field will only be used if the `duration` field is specified.


## MoveContinuous

The `MoveContinuous` mode allows to move an entity at a constant speed in a given direction. The movement is defined by a direction and a speed.

The `MoveContinuous` mode will have the following fields:

- `direction`: _Vector3_, the direction of the movement
- `speed`: _number_, the speed of the movement in meters per second
- `duration`: _number_, the duration of the movement in milliseconds
- `easingFunction`: _EasingFunction_ (optional), the easing function to use for the movement

Note: The `duration` field is optional. If it is not specified, the movement will be continuous and infinite. The `easingFunction` field will only be used if the `duration` field is specified.


## TextureMoveContinuous

The `TextureMoveContinuous` mode allows to move the texture of a material at a constant speed in a given direction. The movement is defined by a direction and a speed.

The `TextureMoveContinuous` mode will have the following fields:

- `direction`: _Vector2_, the direction of the movement
- `speed`: _number_, the speed of the movement expressed as a percentage of the texture size per second
- `duration`: _number_, the duration of the movement in milliseconds
- `easingFunction`: _EasingFunction_ (optional), the easing function to use for the movement

Note: The `duration` field is optional. If it is not specified, the movement will be continuous and infinite. The `easingFunction` field will only be used if the `duration` field is specified.


## Easing function to be optional

Currently the `EasingFunction` field is required for all tweens. We will make it optional for all tweens, including the already existing ones. Setting this field is a nuissance for creators, since it involves importing an enum reference which is not always obvious. The vast majority of the time, creators will want to use the default value, which is `EasingFunction.EF_LINEAR`.

## Serialization

```yaml

```

```protobuf

```

## Semantics

### Example


Simple continuos rotation:

```ts
const myEntity = engine.addEntity()

MeshRenderer.setCube(myEntity)

Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})

Tween.create(myEntity, {
mode: Tween.Mode.RotateContinuous({
direction: Quaternion.create(0, 0, 0, 1),
speed: 10,
}),
})
```

Simple continuous movement:
```ts
const myEntity = engine.addEntity()

MeshRenderer.setCube(myEntity)

Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})

Tween.create(myEntity, {
mode: Tween.Mode.MoveContinuous({
direction: Vector3.create(0, 0, 1),
speed: 10,
}),
})
```

Simple continuous texture movement:
```ts
const myEntity = engine.addEntity()

MeshRenderer.setPlane(myEntity)

Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})

Material.setPbrMaterial(myEntity, {
texture: Material.Texture.Common({
src: 'materials/water.png',
wrapMode: TextureWrapMode.TWM_REPEAT,
}),
})

Tween.create(myEntity, {
mode: Tween.Mode.TextureMoveContinuous({
direction: Vector2.create(0, 1),
speed: 0.1,
}),
})

TweenSequence.create(myEntity, { sequence: [], loop: TweenLoop.TL_RESTART })
```
Loading