What Is Tweening?
The word "tween" comes from "in-between" — a term borrowed from traditional animation, where junior animators were responsible for drawing the frames between the key poses drawn by senior animators. In game development and interactive media, tweening is the process of automatically generating intermediate values between a start state and an end state over a period of time.
A tween animates a property — a position, a rotation, a scale, a color, or any numeric value — from point A to point B. The simplest tween is a straight linear interpolation. More expressive tweens use easing functions to shape the motion curve, giving animations personality: a bouncy UI button, a smooth camera glide, a snappy card flip.
Unity provides several native tools for tweening, and the community library DOTween is the de-facto standard for more advanced needs. This chapter covers all of them.
Native Tweening with Lerp()
Linear Interpolation (Lerp) is the mathematical foundation of all tweening. Given a start value A, an end value B, and a parameter t between 0 and 1, Lerp returns the value that is fraction t of the way from A to B.
t = 0returns At = 0.5returns the midpoint between A and Bt = 1returns B
Unity provides Lerp on the types you use most often:
Vector3.Lerp(a, b, t)Quaternion.Lerp(from, to, t)Mathf.Lerp(a, b, t)Color.Lerp(a, b, t)
Basic Linear Movement
using UnityEngine;
public class LinearMover : MonoBehaviour
{
public Vector3 startPosition;
public Vector3 endPosition;
public float duration = 2f;
private float _elapsed = 0f;
void Start()
{
startPosition = transform.position;
}
void Update()
{
if (_elapsed < duration)
{
_elapsed += Time.deltaTime;
float t = Mathf.Clamp01(_elapsed / duration); // normalize to 0..1
transform.position = Vector3.Lerp(startPosition, endPosition, t);
}
}
}
The "Lazy" Lerp Pattern
There is a popular shortcut where instead of tracking elapsed time, you lerp from the current position toward the target each frame. This naturally produces an ease-out effect because the distance shrinks each frame and the step size shrinks with it.
public Transform target;
public float speed = 5f;
void Update()
{
// Each frame, move 'speed * Time.deltaTime' fraction of the remaining distance
transform.position = Vector3.Lerp(transform.position, target.position, speed * Time.deltaTime);
}
This pattern is extremely common because it is short and produces a pleasing deceleration for free. However, it has a subtle problem: the motion is frame-rate dependent. At 30 FPS vs. 120 FPS the deceleration curve looks different. For precise animation timing, the elapsed-time pattern is more reliable. Use the lazy pattern for visual polish where exact timing does not matter (camera follow, smooth UI hover effects).
Easing Functions
A pure linear Lerp produces mechanical, robotic motion. Real-world objects accelerate and decelerate — they do not start instantly at full speed or stop dead. Easing functions transform the linear parameter t into a curved value, shaping the speed profile of the animation.
The three fundamental families are:
- EaseIn: slow start, fast end. The object accelerates — like a car pulling away.
- EaseOut: fast start, slow end. The object decelerates — like a car braking to a stop.
- EaseInOut: slow start, slow end, fast middle. The most natural-looking motion for most UI animations.
Common mathematical curves include Quadratic (²), Cubic (³), Quartic (⁴), Quintic (⁵), Sine, Exponential, Circular, Back (slight overshoot), Elastic (spring oscillation), and Bounce.
Using an Easings Helper Class
Robert Penner's easing equations are the industry standard. You can find many C# implementations online — add an Easings.cs static class to your project and call its methods to transform t before passing it to Lerp:
// Example Easings.cs (excerpt — add the full class to your project)
public static class Easings
{
// Cubic ease in: accelerates from zero
public static float EaseInCubic(float t)
{
return t * t * t;
}
// Cubic ease out: decelerates to zero
public static float EaseOutCubic(float t)
{
float f = t - 1f;
return f * f * f + 1f;
}
// Cubic ease in-out
public static float EaseInOutCubic(float t)
{
if (t < 0.5f)
return 4f * t * t * t;
else
{
float f = (2f * t) - 2f;
return 0.5f * f * f * f + 1f;
}
}
// Elastic ease out: spring-like overshoot
public static float EaseOutElastic(float t)
{
float p = 0.3f;
return Mathf.Pow(2f, -10f * t) * Mathf.Sin((t - p / 4f) * (2f * Mathf.PI) / p) + 1f;
}
// Bounce ease out
public static float EaseOutBounce(float t)
{
if (t < 1f / 2.75f)
return 7.5625f * t * t;
else if (t < 2f / 2.75f)
{
t -= 1.5f / 2.75f;
return 7.5625f * t * t + 0.75f;
}
else if (t < 2.5f / 2.75f)
{
t -= 2.25f / 2.75f;
return 7.5625f * t * t + 0.9375f;
}
else
{
t -= 2.625f / 2.75f;
return 7.5625f * t * t + 0.984375f;
}
}
}
Applying an Easing to a Lerp
using UnityEngine;
public class EasedMover : MonoBehaviour
{
public Vector3 startPosition;
public Vector3 endPosition;
public float duration = 1.5f;
private float _elapsed = 0f;
void Start()
{
startPosition = transform.position;
}
void Update()
{
if (_elapsed < duration)
{
_elapsed += Time.deltaTime;
float tLinear = Mathf.Clamp01(_elapsed / duration);
// Apply easing to the linear t
float tEased = Easings.EaseInOutCubic(tLinear);
transform.position = Vector3.Lerp(startPosition, endPosition, tEased);
}
}
}
The pattern is always the same: compute a linear t, transform it through an easing function, then use the result as the Lerp parameter. You can swap the easing function without touching any other code.
The Elastic Problem
Elastic and Bounce easing functions can produce t values outside the range 0 to 1 — the overshoot is intentional, it is what creates the spring or bounce appearance. However, if your Lerp endpoints are hard limits (for example, you must not move past a wall), an unclamped t can cause the object to visually pass through the boundary.
Two solutions:
// Solution 1: clamp t after easing (kills the elastic effect past boundaries)
float tEased = Easings.EaseOutElastic(tLinear);
tEased = Mathf.Clamp01(tEased);
transform.position = Vector3.Lerp(start, end, tEased);
// Solution 2: use DOTween (handles all edge cases internally)
transform.DOMove(endPosition, duration).SetEase(Ease.OutElastic);
For most UI animations (buttons, panels, icons), the elastic overshoot looks great and the positions involved have no physical constraints — so you can use elastic easings freely. For gameplay objects with collision boundaries, either clamp or switch to DOTween.
Lerp and AnimationCurve
Unity's AnimationCurve class lets you define a custom curve using a visual curve editor in the Inspector. This is extremely powerful for designers and artists who want to fine-tune animation timing without writing code.
Setting Up an AnimationCurve
using UnityEngine;
public class AnimationCurveMover : MonoBehaviour
{
public Vector3 startPosition;
public Vector3 endPosition;
public float duration = 1f;
// Edit this curve visually in the Inspector
public AnimationCurve curve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
private float _elapsed = 0f;
void Start()
{
startPosition = transform.position;
}
void Update()
{
if (_elapsed < duration)
{
_elapsed += Time.deltaTime;
float normalizedTime = Mathf.Clamp01(_elapsed / duration); // 0..1
// Sample the curve at the normalized time
float curveValue = curve.Evaluate(normalizedTime);
// curveValue is now shaped by the curve (can go outside 0..1 if designer wants overshoots)
transform.position = Vector3.Lerp(startPosition, endPosition, curveValue);
}
}
}
The curve's horizontal axis represents normalized time (0 to 1) and the vertical axis represents the output value sent to Lerp. In the Inspector, you can drag keyframes, adjust tangents, and create complex multi-stage animations. This puts timing control in the hands of non-programmers.
Common AnimationCurve Presets
// Built-in preset curves:
AnimationCurve linear = AnimationCurve.Linear(0f, 0f, 1f, 1f);
AnimationCurve easeInOut = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
// Create a custom curve programmatically:
AnimationCurve bouncy = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(0.7f, 1.1f), // overshoot
new Keyframe(0.85f, 0.9f), // bounce back
new Keyframe(1f, 1f) // settle
);
Installing DOTween
DOTween is the most popular tweening library for Unity, created by Daniele Giardini (Demigiant). It is available in two versions: DOTween (free) and DOTween Pro (paid, adds visual animation tool). For all the features covered in this chapter, the free version is sufficient.
Installation Steps
- Download DOTween from the Unity Asset Store (search "DOTween") or from
dotween.demigiant.com. - Import the package into your Unity project.
- Unity will prompt you to run the DOTween Setup Wizard — do this. It generates the necessary assembly files and configures DOTween for your project settings (including safe mode and logging preferences).
- Add
using DG.Tweening;at the top of any script that uses DOTween.
Initialization (Optional but Recommended)
using UnityEngine;
using DG.Tweening;
public class GameManager : MonoBehaviour
{
void Awake()
{
// Optional: configure DOTween globally once at startup
DOTween.Init(
recycleAllByDefault: true, // reuse tween objects for performance
useSafeMode: true, // catch errors internally
logBehaviour: LogBehaviour.ErrorsOnly
);
DOTween.defaultEaseType = Ease.OutQuad; // set the default ease
}
}
How to Use DOTween
DOTween extends Unity's built-in types with extension methods. You call a tween method directly on a Transform, Rigidbody, Material, or other component — no boilerplate needed.
Basic Transform Tweens
using UnityEngine;
using DG.Tweening;
public class DOTweenExamples : MonoBehaviour
{
public Vector3 targetPosition = new Vector3(5f, 0f, 0f);
public Vector3 targetRotation = new Vector3(0f, 180f, 0f);
public Vector3 targetScale = new Vector3(2f, 2f, 2f);
void Start()
{
// Move to world position in 1 second
transform.DOMove(targetPosition, 1f);
// Rotate to Euler angles in 1 second
transform.DORotate(targetRotation, 1f);
// Scale to target scale in 0.5 seconds
transform.DOScale(targetScale, 0.5f);
// Local space variants
transform.DOLocalMove(new Vector3(0f, 2f, 0f), 1f);
transform.DOLocalRotate(new Vector3(45f, 0f, 0f), 1f);
}
}
Method Chaining
DOTween is designed for chaining. Every tween returns a Tweener object you can configure with additional method calls:
transform
.DOMove(new Vector3(5f, 0f, 0f), 1.5f)
.SetEase(Ease.OutBounce) // apply a bounce easing
.SetDelay(0.5f) // wait 0.5s before starting
.SetLoops(3, LoopType.Yoyo) // repeat 3 times, alternating direction
.OnStart(() => Debug.Log("Tween started"))
.OnComplete(() => Debug.Log("Tween finished"));
Common Ease Types in DOTween
// Smooth deceleration (most common for UI):
.SetEase(Ease.OutQuad)
.SetEase(Ease.OutCubic)
.SetEase(Ease.OutExpo)
// Spring and overshoot:
.SetEase(Ease.OutBack)
.SetEase(Ease.OutElastic)
.SetEase(Ease.OutBounce)
// Linear (no easing):
.SetEase(Ease.Linear)
// Custom AnimationCurve:
AnimationCurve myCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
.SetEase(myCurve)
Animating Other Properties
// Animate a Material color
Renderer rend = GetComponent<Renderer>();
rend.material.DOColor(Color.red, 1f);
// Animate a CanvasGroup alpha (UI fade)
CanvasGroup canvasGroup = GetComponent<CanvasGroup>();
canvasGroup.DOFade(0f, 0.5f); // fade out
// Animate a Light intensity
Light light = GetComponent<Light>();
light.DOIntensity(0f, 1f);
// Animate any float with a generic tween
float currentValue = 0f;
DOTween.To(
() => currentValue, // getter
x => currentValue = x, // setter
100f, // end value
2f // duration
).OnUpdate(() => Debug.Log(currentValue));
Sequences
A DOTween Sequence lets you chain multiple tweens in order, run some in parallel, and insert delays or callbacks — all without nested coroutines.
void PlayIntroAnimation()
{
Sequence seq = DOTween.Sequence();
// Step 1: fade in (0.3 seconds)
seq.Append(canvasGroup.DOFade(1f, 0.3f));
// Step 2 (runs alongside step 1): slide in from left
seq.Join(transform.DOLocalMove(Vector3.zero, 0.3f).SetEase(Ease.OutBack));
// Step 3: wait a moment
seq.AppendInterval(0.5f);
// Step 4: scale up slightly and back
seq.Append(transform.DOScale(1.05f, 0.1f));
seq.Append(transform.DOScale(1f, 0.1f));
// Callback when all done
seq.OnComplete(() => Debug.Log("Intro done"));
seq.Play();
}
The From() Variant
By default, DOTween tweens to the target value. Adding .From() reverses the direction — the tween animates from the target value back to the current value. This is useful for spawning effects where you want an object to "arrive" from a position:
// Object appears to fly in from above
transform.DOLocalMove(transform.localPosition, 1f)
.From(transform.localPosition + Vector3.up * 5f)
.SetEase(Ease.OutCubic);
// Button scales up when it appears (starts small, grows to full size)
transform.DOScale(Vector3.one, 0.4f)
.From(Vector3.zero)
.SetEase(Ease.OutBack);
Killing Tweens
Always kill tweens on an object before starting a new one on the same property, especially if the tween can be triggered repeatedly (e.g., by user input). Failing to do so causes multiple tweens to fight over the same property.
// Kill all tweens on this transform, then start a new one
transform.DOKill();
transform.DOMove(newTarget, 0.5f).SetEase(Ease.OutQuad);
// Or complete the current tween immediately before killing
transform.DOComplete(); // jump to end value
transform.DOMove(newTarget, 0.5f);
// Kill a specific tween by storing the reference
Tweener myTween = transform.DOMove(target, 1f);
// ... later:
myTween.Kill();
Tweening in Coroutines
DOTween provides .WaitForCompletion() for use in coroutines, allowing you to sequence animations using Unity's coroutine system:
IEnumerator PlaySequencedAnimation()
{
yield return transform.DOMove(pointA, 1f).WaitForCompletion();
yield return transform.DORotate(new Vector3(0, 90, 0), 0.5f).WaitForCompletion();
yield return new WaitForSeconds(0.2f);
yield return transform.DOMove(pointB, 1f).SetEase(Ease.InOutCubic).WaitForCompletion();
Debug.Log("All done");
}
Exercise: Virtual Museum
Build an interactive virtual museum where clicking on an exhibit smoothly moves and rotates the camera to view that exhibit up close, then fades in a UI panel with information about it. Clicking again returns the camera to its default position.
Scene Setup
- MainCamera — the scene camera with the
MuseumCamerascript - Exhibit_1, Exhibit_2, Exhibit_3 — 3D objects with
Exhibitscripts and colliders - ViewPoint_1, ViewPoint_2, ViewPoint_3 — empty GameObjects marking camera positions for each exhibit
- InfoPanel — a Canvas / CanvasGroup for the UI overlay
Exhibit.cs
using UnityEngine;
public class Exhibit : MonoBehaviour
{
public Transform cameraViewPoint; // where the camera should go for this exhibit
public string exhibitTitle = "Exhibit Name";
[TextArea] public string exhibitDescription = "Description...";
void OnMouseDown()
{
MuseumCamera.Instance.FocusOn(this);
}
}
MuseumCamera.cs
using UnityEngine;
using DG.Tweening;
public class MuseumCamera : MonoBehaviour
{
public static MuseumCamera Instance;
[Header("References")]
public CanvasGroup infoPanel;
public TMPro.TMP_Text infoTitle;
public TMPro.TMP_Text infoDescription;
[Header("Default View")]
public Transform defaultViewPoint;
[Header("Timing")]
public float moveDuration = 1.2f;
public float fadeDuration = 0.4f;
private bool _isFocused = false;
private Sequence _activeSequence;
void Awake()
{
Instance = this;
infoPanel.alpha = 0f;
infoPanel.interactable = false;
}
public void FocusOn(Exhibit exhibit)
{
// If already focused, return to default first
if (_isFocused)
{
ReturnToDefault();
return;
}
_isFocused = true;
Transform viewPoint = exhibit.cameraViewPoint;
// Kill any running tween sequence
_activeSequence?.Kill();
_activeSequence = DOTween.Sequence();
// Fade out info panel (in case one is visible)
_activeSequence.Append(infoPanel.DOFade(0f, fadeDuration));
// Move and rotate camera to the exhibit view point
_activeSequence.Append(
transform.DOMove(viewPoint.position, moveDuration).SetEase(Ease.InOutCubic)
);
_activeSequence.Join(
transform.DORotateQuaternion(viewPoint.rotation, moveDuration).SetEase(Ease.InOutCubic)
);
// Update UI content, then fade it in
_activeSequence.AppendCallback(() =>
{
infoTitle.text = exhibit.exhibitTitle;
infoDescription.text = exhibit.exhibitDescription;
infoPanel.interactable = true;
});
_activeSequence.Append(infoPanel.DOFade(1f, fadeDuration));
}
public void ReturnToDefault()
{
_isFocused = false;
_activeSequence?.Kill();
_activeSequence = DOTween.Sequence();
// Fade out info panel
_activeSequence.Append(infoPanel.DOFade(0f, fadeDuration));
_activeSequence.AppendCallback(() => infoPanel.interactable = false);
// Return camera to default position
_activeSequence.Append(
transform.DOMove(defaultViewPoint.position, moveDuration).SetEase(Ease.InOutCubic)
);
_activeSequence.Join(
transform.DORotateQuaternion(defaultViewPoint.rotation, moveDuration).SetEase(Ease.InOutCubic)
);
}
}
This example demonstrates several key DOTween patterns working together: sequences, parallel joins, callbacks, interactable toggling, and ease selection. The result is a polished, professional camera transition that would take far more code to implement with coroutines and manual interpolation.
Choosing the Right Tool
Here is a quick decision guide for which tweening approach to use in a given situation:
- Simple one-off smooth movement with no timing requirements — use the lazy Lerp pattern (
Vector3.Lerp(current, target, speed * Time.deltaTime)) - Precisely timed animation with standard easing — use an easing helper class with tracked elapsed time
- Designer-configurable animation timing — use
AnimationCurvein the Inspector - Complex sequences, callbacks, Material/Light/UI animations, elastic/bounce effects — use DOTween
- Character or gameplay animations keyed to specific poses — use Unity's Animator component with Animation Clips (out of scope for this chapter)
Performance Notes
Tweening in DOTween is very efficient — it recycles tween objects internally when configured with recycleAllByDefault: true. However, keep these points in mind:
- Always call
DOKill()on objects that are destroyed or disabled to prevent callbacks from running on null references. - Avoid creating hundreds of simultaneous tweens. DOTween handles many tweens well, but thousands per frame can accumulate GC pressure.
- Use
SetUpdate(true)on tweens that should run even whenTime.timeScale = 0(e.g., pause menu animations). - Prefer tweening material property blocks over tweening shared materials to avoid modifying assets during play mode.
Summary
Tweening is one of the highest-impact skills in game UI and interactive experience development. A well-eased animation communicates intent, confirms actions, and gives your product a polished, professional feel that distinguishes it from amateur work.
- Use
Vector3.Lerp,Quaternion.Lerp, andMathf.Lerpfor simple interpolation - Apply easing functions to the
tparameter to shape the motion curve - Use
AnimationCurvewhen designers need visual control over timing - Install DOTween for sequences, callbacks, chaining, and animating any property with minimal code
- Kill tweens before starting new ones on the same property
Need help with Unity3D development?
I'm a senior developer with 16+ years experience, including AAA projects at Ubisoft. Let's discuss how I can help with your game or interactive project.
Start a Conversation