DOTween入門してみた　～1日目～

# はじめに
いい感じにアニメーションできるという噂を耳にしDOTweenに入門してみました。
尚2日目以降の記事の予定はありません。
DOTweenとは何か、入手方法、基本的な機能などは、わかりやすく解説してくれている記事がたくさんあるのでこの記事では触れません。
よさげな参考文献は一番下に記載しておきます。

# 動作環境
・Windows10 Home
・Unity2019.4.0f1
・DOTween 1.2.335

# Sequenceの無限ループさせても、gameObjectが消えるときにSequenceをKillさせたい

SequenceのSetloop(-1)により無限ループにさせると、gameObjectがdestroyされてもSequenceは実行され続けます。
そして実行を止めると「Tweenさせるオブジェクトがないよ」と警告が...
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/977d86c2-b86c-7e84-b09a-675736f517ab.png)

この警告はDOTweenがいい感じに処理してくれているもので、設定からsafeModeのチェックを外すと、警告だったものが大量のエラーになるはずです。
↓safeModeをONにしたまま、警告を消すこともできます。
https://kan-kikuchi.hatenablog.com/entry/DOTween_Safe_Mode

しかし、できれば警告すら起きないような実装にしたい...と思いまして。

## SetLink(gameObject)を利用する ※2020/07/03追記
もうこれでいいです。
SetLink(gameObject)で、gameObjectがDestroyされるときに自動でKillしてくれます。

以下蛇足の3つの方法です。

### Sequenceのフィールドを用意し、OnDestroyでKill

この実装の欠点。フィールドとしてSequenceを記憶しなければならないこと。


```cs
private Sequence currentSequence;

private void OnDestroy()
{
    this.currentSequence.Kill();
}
```

### Sequence.OnUpdateでdestroyされていたらKill


```cs
Sequence sequence = DOTween.Sequence();
sequence.
    OnUpdate(() =>
    {
        if (this == null) sequence.Kill();
    })
    .SetLoops(-1)
    /*以下sequenceの設定をしていく*/
    .Play();
```

よし。これで無駄なフィールドを用意しなくて済みます。
動かしてみると...ちゃんとSequenceがKillされていることが確認できます。
しかし、先ほど同様警告が(今回はひとつだけ)出てしまいます。
safeModeの力を借りれば、この実装でもいいかもしれませんね。


### OnDestroyで呼ぶUnityEventに代入
Sequenceというフィールドを用意するから邪魔なのであって、OnDestroyの際に呼ぶ処理を用意したら、邪魔にならないはず。

結局フィールドを用意することに変わりないのですが...。

```cs
private UnityEvent onDestroy = new UnityEvent();

//どこかの関数内-------------------
Sequence sequence = DOTween.Sequence();
sequence.
    .SetLoops(-1)
    /*以下sequenceの設定をしていく*/
    .Play();
this.onDestroy.AddListener(()=>sequence.Kill());
//--------------------------------

private void OnDestroy()
{
    this.onDestroy.Invoke();
}
```


### 余談(?) Sequenceではなく、直接呼んでも警告でた
DOTweenの記事をいろいろと漁っていたのですが、「SequenceでSetLoops(-1)の無限ループをすると、手動でKillしないといけない」みたいな話が多く、「問題解決のためには、Sequenceを使わないで、SetLoops(-1)にしよう」のような話も多くありました。
しかしですね、例えば以下のような処理をしたとしても、safeMode外したらバリバリエラー出ます...。
状態としてはOnUpdateでKillした3つ目の実装と同じですかね。

```cs
this.transform.DOBlendableRotateBy(this.transform.right * 200f, 3f, RotateMode.WorldAxisAdd)
    .SetLoops(-1)
```

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/2015eb94-8fd1-b166-ad80-d75f10e96a96.png)

というわけで3つ目のUnityEventを使った方法で、無事解決(サンプルとして無限に回転させてみました)

```cs
Tweener tweener = this.transform.DOBlendableRotateBy(this.transform.right * 200f, 3f, RotateMode.WorldAxisAdd)
    .SetLoops(-1)

this.onDestroy.AddListener(() => tweener.Kill());
```

# Sequenceが1周するごとに処理をしたい
OnStepComplete()でできます。
Appendなどで複数まとめたときは、ひとまとまり全て終わった段階で呼ばれます。

# 前回のSequenceから初期化したくない
通常はひとつのSequenceが終了しループするたびに、開始時の状態に戻ります。
戻したくないときは、SetLoops(n, LoopType.Incremental)とする。

# ここが不便だよDOTween
触っていくと、いろいろと不便に感じる点があったので書いていきます。
私もまだDOTweenを触ったばかりなので、「こんな実装方法があるよ」等あればぜひぜひ教えてほしいです泣

## LoopType.Incrementalにしても、rotationは初期化される
前回のSequenceの終わったときの状態を考慮する、つまりSequenceループのたびに初期化をおこなわないというものですが、この効果が出るのはどうもpositionであり、rotationは初期化されてしまうんですよね...。
どぉ～してぇ～

## ループ終わりのOnStepCompleteはあっても、ループ始まりのOnStepStartはない　※2020/07/03修正
AppendCallBackを使いましょう。
アニメーションの任意のタイミングにコールバックを加えることができます。便利！


## OnStepCompleteはループ終了後の初期化処理がおこなわれたあとに呼ばれる
例えば以下のような処理があったとき、Debug.Log()で何が出力されるでしょうか。
これはループ終了時のpositionではなく、ループ開始前のpositionの値になります。
Debug.Log(Time.time)にしてみると、ちゃんと時間経過しているのがわかるため、OnStepCompleteはループ終了後の初期化処理が終わってから呼ばれていると推測できます。
これとrotationが初期化される点で、複雑な回転ループは難しいと感じます...

```cs
Sequence sequence = DOTween.Sequence()
    .Append(this.transform.DOBlendableLocalMoveBy(Vector3.right * 10f, 1f))
    .OnStepComplete(() => Debug.Log(this.transform.position))
    .SetLoops(-1)
    .Play();
```

## Pause中はOnUpdateが呼ばれない
逆に呼ばれたら困ることのほうが多いので、これは助かりますが。
呼ばれないので、Update内で処理しましょう

# 実装例
といってもこの手の記事は、もっと優秀なものが沢山あるので少しだけです。

## 特定の垂直平面に沿った長方形周回移動

![2020-07-02-00-10-36.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/286d1cea-eb0a-b937-6c44-013477417ec7.gif)


```cs
    /// <summary>
    /// 特定の垂直平面において(反)時計回りに長方形移動し続ける
    /// </summary>
    /// <param name="horizontalDirection">水平方向移動量</param>
    /// <param name="verticalLength">垂直方向移動量(正のとき初期位置から上がって下がる)</param>
    /// <param name="oneRoutineTime">長方形を一周する時間</param>
    private void MoveSquareSample(Vector3 horizontalDirection, float verticalLength, float oneRoutineTime)
    {
        if (horizontalDirection.y != 0) throw new System.Exception();
        float horizontalLength = horizontalDirection.magnitude;
        float horizontalTime = oneRoutineTime * (horizontalLength / (2 * horizontalLength + 2 * verticalLength));
        float verticalTime = oneRoutineTime * (verticalLength / (2 * horizontalLength + 2 * verticalLength));

        Sequence sequence = DOTween.Sequence();
        sequence
            .OnStepComplete(() =>
            {
                Debug.Log("1ループ終了");
            })
            .Append(this.transform.DOBlendableLocalMoveBy(Vector3.up * verticalLength, verticalTime))
            .Append(this.transform.DOBlendableLocalMoveBy(horizontalDirection, horizontalTime))
            .Append(this.transform.DOBlendableLocalMoveBy(Vector3.down * verticalLength, verticalTime))
            .Append(this.transform.DOBlendableLocalMoveBy(horizontalDirection * -1, horizontalTime))
            .SetRelative()
            .SetLoops(-1)
            .SetLink(this.gameObject)
            .Play();
    }
```

## きりもみ回転
ここまでくると、もはやDOTweenを使う意味が...はい
あくまでなんちゃって。実用性はないですね

![2020-07-02-00-37-21.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276180/005d4229-e992-309f-a105-d15873259493.gif)



```cs
    /// <summary>
    /// きりもみ回転
    /// </summary>
    /// <param name="durationOfRotationUpAxis">transform.upを軸とした1回転の時間</param>
    /// <param name="durationOfRotationHorizontalAxis">horizontalAxisを軸とした1回転の時間</param>
    /// <param name="horizontalAxis">水平方向の回転軸。yは0</param>
    private void RotationSample(float durationOfRotationUpAxis, float durationOfRotationHorizontalAxis, Vector3 horizontalAxis)
    {
        if (horizontalAxis.y != 0f) throw new System.Exception();
        horizontalAxis = horizontalAxis.normalized;
        Sequence sequence = DOTween.Sequence();
        sequence
            .SetLoops(-1)
            .OnUpdate(() => 
            {
                this.transform.RotateAround(this.transform.position, horizontalAxis, 360f * Time.deltaTime / durationOfRotationHorizontalAxis);
                this.transform.RotateAround(this.transform.position, this.transform.up, 360f * Time.deltaTime / durationOfRotationUpAxis); 
            })
            .SetLink(this.gameObject)
            .Play();
    }
```

## 特定の軌道を往復し続ける

```cs
        this.sequence = DOTween.Sequence()
            .SetRelative()
            .SetLink(this.gameObject)
            .SetLoops(-1);
        foreach(Vector3 p in this.path.Concat(path.Reverse().Select(v => v * -1f)))
        {
            this.sequence.Append(this.transform.DOMove(p, p.magnitude / this.moveSpeed));
        }
        this.sequence.Play();
```


# よさげな文献
最初に一通り目を通すのに良いと思います。
https://gist.github.com/anzfactory/da73149ba91626ba796d598578b163cc
https://amagamina.jp/how-to-dotween/
https://qiita.com/kagigi/items/bdf4d42835add07b0077


様々なEasingのグラフが並んでいます。
https://game-ui.net/?p=835
https://easings.net/


MoveとRotateに着目して、各種操作
https://qiita.com/BEATnonanka/items/378de2bca3c972a95399
https://qiita.com/BEATnonanka/items/b4cca6471e77466cec74


Sequenceによる統合
https://qiita.com/lycoris102/items/6a9e1e39bfa69880eaba


公式ドキュメント
http://dotween.demigiant.com/documentation.php