Chapter 4: Tweening and Easing — Smooth Animations in Unity3D

Game DevelopmentMath for Unity3DJune 201835 min read

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 = 0 returns A
  • t = 0.5 returns the midpoint between A and B
  • t = 1 returns 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

  1. Download DOTween from the Unity Asset Store (search "DOTween") or from dotween.demigiant.com.
  2. Import the package into your Unity project.
  3. 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).
  4. Add using DG.Tweening; at the top of any script that uses DOTween.
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 MuseumCamera script
  • Exhibit_1, Exhibit_2, Exhibit_3 — 3D objects with Exhibit scripts 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 AnimationCurve in 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 when Time.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, and Mathf.Lerp for simple interpolation
  • Apply easing functions to the t parameter to shape the motion curve
  • Use AnimationCurve when 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
Unity3DTweeningEasingLerpDOTweenAnimationC#

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