Unityで超便利なTweenアセット「DOTween」が好き

ゲーム開発ではいろいろなアニメーションが必要になる。

  • キャラクターモーション
  • バトルエフェクト
  • UIアニメーション

基本的にはデザイナーさんに作ってもらうけど、ちょっとしたアニメーションはスクリプトで組んでしまうことも多い。
そういう時に便利なのがDOTween!

特徴

ドキュメントが丁寧

http://dotween.demigiant.com/documentation.php

パフォーマンスに気を使ってる

似たようなアニメーションはiTweenでも行えるが、DOTweenの方がパフォーマンスが良い。
パフォーマンス比較

スクリプトからのアクセスが容易

基本、なんらかのクラスの拡張メソッドで定義されている。

transform.DOMove(Vector3.one, 1);
image.DOColor(Color.red, 1);

また、戻り値にはTweenerクラスが返されるので、以下のように設定を繋げて書ける。

transform.DOMove(Vector3.one, 1).SetDelay(1).SetEase(Ease.Linear).SetLoops(-1, LoopType.Yoyo);

transformを1秒待って(Delay)から直線的な動き(Linear)でヨーヨーのようなループ運動(Loop)させる。

Tweenのチェインが可能

例えば「移動してから」→「大きくなって」→「回転させる」というアニメーションを組みたい場合、

transform.DOMove(Vector3.one, 1);
transform.DOScale(2, 1);
transform.DORotate(new Vector3(0, 0, 180), 1);

とすると全部のTweenが同時に実行されてしまう。一連のTweenを繋げて(チェインして)行いたい場合、

var tweener1 = transform.DOMove(Vector3.one, 1);
var tweener2 = transform.DOScale(2, 1);
var tweener3 = transform.DORotate(new Vector3(0, 0, 180), 1);
DOTween.Sequence().Append(tweener1).Append(tweener2).Append(tweener3);

という風にSequenceに登録してやれば可能。これが本当に便利!

動き方の種類が豊富

Easeで動き方を変えられるが、種類が豊富(加速・減速させたり、バウンドさせたり)。
[Unity]DoTweenのEaseいろいろあってよくわかんない…な人へ[DoTween]
easing_demo
イージングの基本

無料版もあってオープンソース

内部実装を見ると勉強になる(キャッシュの仕組みとか)。 https://github.com/Demigiant/dotween
License

読み方

「ドゥーツウィーン」ではなく「ドッツウィーン」(ドットで繋げていくTweenだから?)。

使い方

自分がよく使うものをまとめた。

Global settings

DOTweenクラスのstatic変数にグローバル設定がある。

// デフォルトのEaseをLinearに変更
DOTween.defaultEaseType = Ease.Linear;

ほとんどはそのままでいいが、デフォルトのEaseは直線的になってた方がいいので変更。
Tools > Demigiant > DOTween Utility Panel > Preferencesからも変更可能(こっちの方がプログラムを書かなくていいので良い)。

Tween
基本の形
// アニメーション後、次の処理へ
transform.DOMove(Vector3.one, 1).SetDelay(1).SetEase(Ease.OutQuad).OnComplete(() =>
{
    // 次の処理
});

// 相対移動(endValueは絶対座標。現在位置からの移動量を指定したい場合はRelativeをtrueに)
transform.position = new Vector3(1, 0, 0);
transform.DOMoveX(endValue: 1, duration: 1).SetRelative(true);

// ループアニメーション
transform.DOScale(2, 1).SetLoops(-1, LoopType.Yoyo);

// 再生中か
var tweener = transform.DOMoveX(1, 1);
if(tweener.IsPlaying()) Debug.Log("再生中");

// 停止
tweener.Kill();
移動
// transform.position
transform.DOMove(Vector3.one, 1);
transform.DOMoveX(1, 1);
transform.DOMoveY(1, 1);
transform.DOMoveZ(1, 1);

// transform.localPosition
transform.DOLocalMove(Vector3.one, 1);
transform.DOLocalMoveX(1, 1);
transform.DOLocalMoveY(1, 1);
transform.DOLocalMoveZ(1, 1);

// rectTransform.anchoredPosition
rectTransform.DOAnchorPos(Vector2.one, 1);
rectTransform.DOAnchorPosX(1, 1);
rectTransform.DOAnchorPosY(1, 1);
スケール
// transform.localScale
transform.DOScale(new Vector3(2, 2, 2), 1);
transform.DOScale(2, 1);
transform.DOScaleX(2, 1);
transform.DOScaleY(2, 1);
transform.DOScaleZ(2, 1);

// rectTransform.sizeDelta
rectTransform.DOSizeDelta(Vector2.one, 1);
回転
// transform.rotation
transform.DORotate(new Vector3(0, 0, 180), 1, RotateMode.Fast);
transform.DORotateQuaternion(Quaternion.Euler(0, 0, 180), 1);

// transform.localRotation
transform.DOLocalRotate(new Vector3(0, 0, 180), 1, RotateMode.Fast);
transform.DOLocalRotateQuaternion(Quaternion.Euler(0, 0, 180), 1);
色/フェイド
// material.color
material.DOColor(Color.red, 1);
material.DOFade(0, 1);

// spriteRenderer.color
spriteRenderer.DOColor(Color.red, 1);
spriteRenderer.DOFade(0, 1);

// image.color
image.DOColor(Color.red, 1);
image.DOFade(0, 1);

// text.color
text.DOColor(Color.red, 1);
text.DOFade(0, 1);
面白い動き
// シェイク(一定時間のランダムな動き)
var duration   = 1f;    // 時間
var strength   = 1f;    // 力
var vibrato    = 10;    // 揺れ度合い
var randomness = 90f;   // 揺れのランダム度合い(0で一定方向のみの揺れになる)
var snapping   = false; // 値を整数に変換するか
var fadeOut    = true;  // 揺れが終わりに向かうにつれ段々小さくなっていくか(falseだとピタッと止まる)
transform.DOShakePosition(duration, strength, vibrato, randomness, snapping, fadeOut);
transform.DOShakeScale(duration, strength, vibrato, randomness, fadeOut);
transform.DOShakeRotation(duration, strength, vibrato, randomness, fadeOut);

// パンチ(ボクシングでジャブをもらった時のような動き)
var punch      = Vector3.one; // 力
var duration   = 1f;          // 時間
var vibrato    = 10;          // 揺れ度合い
var elasticity = 1f;          // 弾力
var snapping   = false;       // 値を整数に変換するか
transform.DOPunchPosition(punch, duration, vibrato, elasticity, snapping);
transform.DOPunchScale(punch, duration, vibrato, elasticity);
transform.DOPunchRotation(punch, duration, vibrato, elasticity);

// ジャンプ(You can fly!!)
var endValue  = new Vector3(500, 0); // 最終地点
var jumpPower = 300;                 // 力
var numJumps  = 2;                   // 最終地点までにジャンプする回数
var duration  = 2;                   // 時間
var snapping  = false;               // 値を整数に変換するか
transform.DOJump(endValue, jumpPower, numJumps, duration, snapping);
transform.DOLocalJump(endValue, jumpPower, numJumps, duration, snapping);
Sequence
// Append(順番に処理)
// 1 → 2 → 3 → 4
var sq1 = DOTween.Sequence()
                 .Append(transform.DOMoveX(1, 1))              // 1
                 .AppendInterval(0.5f)                         // 2
                 .Append(transform.DOMoveY(1, 1))              // 3
                 .AppendCallback(() => Debug.Log("Complete")); // 4

// Insert(途中に加える。並列処理)
// 567
var sq2 = DOTween.Sequence()
                 .Insert(0,    transform.DOMoveX(1, 1))              // 5
                 .Insert(1.5f, transform.DOMoveY(1, 1))              // 6
                 .InsertCallback(2.5f, () => Debug.Log("Complete")); // 7

// Prepend(先頭に加える。優先処理)
// Join(直前の処理に繋げる)
// 89 → 1 → 2 → 3 → 4
if(isTutorial)
{
    // チュートリアルでは最初に特別アニメーションを再生
    sq1.Prepend(transform.DOScale(2, 1))                     // 8
       .Join(transform.DORotate(new Vector3(0, 0, 180), 1)); // 9
}

// Sequence自体も登録可能!
// 89 → 1 → 2 → 3 → 4 → 567
DOTween.Sequence().Append(sq1).Append(sq2);
To
// 一定時間、繰り返し処理を行う
var progress = 0f;
DOTween.To(
    ()    => progress, // getter(現在値を取得)
    value =>           // setter(更新値を設定)
    {
        // 1秒間、0~1の値がくる
        progress = value;
        Debug.Log($"{(int)(progress * 100)}%");
    },
    1,  // endValue(最終値)
    1); // duration(時間)

// こうすればボックス化を避けられる
DOTween.To(
    value => // setter(更新値を設定)
    {
        Debug.Log($"{(int)(value * 100)}%");
    }, 
    0,  // startValue(開始値)
    1,  // endValue(最終値)
    1); // duration(時間)

// 1秒で現在色を赤色に変化させる
DOTween.To(() => image.color, c => image.color = c, Color.red, 1);

細かい処理を行いたい場合はToを使えば解決!(余談だが、他のTweenも内部的にはToで実装されてる)
とても便利なToだけど、ラムダ式にローカル変数を使う場合はボックス化のコストが掛かることに注意(progressみたいな使い方)。
Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方

実践

以前リリースしたアプリのダイアログ演出(PlayAnimationの中でDOTweenを使用してる)。
イメージとしてはiPhoneの標準ダイアログにほんの少しアレンジを加えた感じ♪

f:id:okamura0510:20190228160701g:plain:w512

https://github.com/okamura0510/DOTweenFavorite

using UnityEngine;

public class Demo : MonoBehaviour
{
    public void OpenDialog()
    {
        Dialog.Create().SetMessage("君の夢が").Open(() => Debug.Log("完了しまうま"));
    }
}
using UnityEngine;
using UnityEngine.UI;
using System;
using DG.Tweening;
using static DG.Tweening.Ease;

public class Dialog : MonoBehaviour
{
    [SerializeField] Image background;
    [SerializeField] RectTransform window;
    [SerializeField] Text message;
    [SerializeField] Text buttonText;
    Graphic[] graphics;
    Action onClose;

    public bool IsShowing    { get; private set; }
    public string Message    => message.text;
    public string ButtonText => buttonText.text;

    public static Dialog Create()
    {
        var d      = Instantiate(Resources.Load<Dialog>(nameof(Dialog)));
        d.graphics = d.GetComponentsInChildren<Graphic>();
        return d;
    }

    public Dialog SetMessage(string message)
    {
        this.message.text = message;
        return this;
    }

    public Dialog SetButtonText(string buttonText)
    {
        this.buttonText.text = buttonText;
        return this;
    }

    public Dialog Open(Action onClose = null)
    {
        this.onClose = onClose;
        IsShowing    = true;
        PlayAnimation();
        return this;
    }

    public Dialog Close()
    {
        IsShowing = false;
        PlayAnimation();
        return this;
    }

    public void OnButtonClick() => Close();

    void PlayAnimation()
    {
        if(IsShowing)
        {
            DOTween.Sequence()
                   .Append(window.DOScale(1, 0.3f).SetEase(OutBack))
                   .Append(background.DOFade(0.5f, 0.1f).SetEase(Linear));
        }
        else
        {
            background.DOFade(0, 0.3f).SetEase(OutCubic);
        }

        var start    = IsShowing ? 0f : 1f;
        var end      = IsShowing ? 1f : 0f;
        var duration = 0.3f;
        var ease     = IsShowing ? InCubic : OutCubic;
        var tweener  = DOTween.To(a =>
        {
            foreach(var g in graphics)
            {
                if(g == background) continue;

                var color = g.color;
                color.a   = a;
                g.color   = color;
            }
        }, 
        start, end, duration).SetEase(ease);

        if(!IsShowing)
        {
            tweener.OnComplete(() =>
            {
                onClose?.Invoke();
                Destroy(gameObject);
            });
        }
    }
}

最後に

個人的にAssetは最低限しか入れない自分が、DOTweenだけはどのプロジェクトでも入れてる。
使いやすくて、設計がしっかりしてて、パフォーマンスも良くて、とても完成度が高いAssetだと思う☆