Unity DOTween 入門

この記事は[Unity #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/unity2)の5日目の記事です

# はじめに
- DOTweenというライブラリを使うとオブジェクトをアニメーションさせたり、マテリアルの色を徐々に変化させたい場合に非常に便利です
- アニメーションクリップを作ることなく動きを作成できるので、プログラマだけでアニメーションを実装することができます
    - デザイナーにアニメーションを作ってもらうまでのモックとして利用することもできます
- 今回は基本的な使い方、よく使う拡張、コールバック、SequenceをDOTween入門として紹介します

# 参考
DOTween Asset Store
https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676?locale=ja-JP

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

# セットアップ
Asset Storeで`dotween`と検索して入手します
PRO版は内部のソースコードが見れたりしますが、基本的には無料版で問題ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/251dc3de-29f3-cc23-ed96-7c407fb1f8e1.png)

Importが完了したら初期セットアップを行います
`Open DOTween Utility Panel`を開き
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/60cf9f3e-5393-06a1-a09d-32ec9c545330.png)

`Setup DOTween...`を選択し、`Apply`でセットアップは完了です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/05bafee6-8c75-c459-579c-98737bd66767.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/c6fd9889-c9f8-a21c-2005-c28512cd4aba.png)

# 基本編
## まず動かしてみる
3秒かけて(5,0,0)へ移動するというコードを書いてみます。
UpdateやCoroutineを使って書こうとすると大変ですが、DOTweenを使えば1行で書くことができます。
このコードから拡張していろいろな機能を紹介していきます
![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/9db6637c-74bf-ddbd-6aad-2750a2f1d827.gif)

```c#
using DG.Tweening;	//DOTweenを使うときはこのusingを入れる
using UnityEngine;

public class MoveTest : MonoBehaviour
{
	void Start()
	{
        // 3秒かけて(5,0,0)へ移動する
		this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f);
	}
}
```

## 繰り返す SetLoops
`SetLoops`を使うと、動作を繰り返すことができます
繰り返す回数(loops)と繰り返し方法(loopType)を設定します
`loops`に-1を入れるとずっと繰り返す挙動になります

![loop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/814b00b5-3abd-ec2c-5e04-4b05978d493d.gif)

```c#
void Start()
{
    //(5,0,0)へ1秒で移動するのを3回繰り返す
    this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).SetLoops(3,LoopType.Restart);
}
```


なお、Loop Typeパラメータ別の挙動は下記のようになっています

|Loop Type|説明|
|---|---|
|Yoyo|移動後、最初の位置に戻るように動きます|
|Restart|移動後、最初の位置から再び移動を開始します|
|Increment|移動後、その位置から再び移動を開始します|

![loop2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/6db06548-eb25-ebcd-a47a-7fc7d08e5142.gif)

## 遅延 SetDelay
`SetDelay`を使うことで動作が開始するのを遅延させることができます

```C#
void Start()
{
	//3秒待ってから(5,0,0)へ1秒で移動する
	this.transform.DOMove(new Vector3(5f,0f,0f), 1f).SetDelay(3f);
}
```

## Ease 
`SetEase`を使うと始点と終点をどのように繋ぐか設定することができます

![ease.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/eb0c07ee-1e5f-5aa4-1a65-0eb340e2b308.gif)

```c#
void Start()
{
	//(5,0,0)へ1秒でリニア移動する
	this.transform.DOMove(new Vector3(5f,0f,0f), 1f).SetEase(Ease.Linear);
}
```

Easeには様々な種類があります。
実際使う際はいろいろなパラメータを変えながら、作りたい動きを探すのがいいでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/5faf09fd-1363-d727-876b-e4d26346e775.png)
https://github.com/Nightonke/WoWoViewPager/blob/master/Pictures/ease.png

なお`SetEase`を使わない場合は`OutQuad`がデフォルトで設定されるようです。

## 全部一緒に使う
これらのパラメータは一度に設定して使用することができます

```c#
void Start()
{
	//2秒待ってから(5,0,0)へ3秒で移動するのを4回(2往復) OutBounceで行う
	this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f).SetDelay(2f).SetLoops(4, LoopType.Yoyo).SetEase(Ease.OutBounce);
}
```

読みやすくするためにドットで改行して書くことも可能です

```c#
void Start()
{
	//2秒待ってから(5,0,0)へ3秒で移動するのを4回(2往復) OutBounceで行う
	this.transform.DOMove(new Vector3(5f, 0f, 0f), 3f)
					.SetDelay(2f)
					.SetLoops(4, LoopType.Yoyo)
					.SetEase(Ease.OutBounce);
}
```

# 止める Kill
DOTweenの実行を止める方法を4つ紹介します。
シチュエーションに合った止め方を選択してください。
Killするとオブジェクトは移動中であってもその場で停止します。


```c#
Tween tween;

void Start()
{
	this.tween = this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).SetLoops(-1, LoopType.Yoyo);
}

void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
		//返り値を保存しておいて止める方法
		this.tween.Kill();
    }

	if(Input.GetKeyDown(KeyCode.S))
    {
		//参照元を指定して止める方法
		this.transform.DOKill();
    }

    if(Input.GetKeyDown(KeyCode.D))
    {
		//Objectを指定して止める方法
		DOTween.Kill(this.transform);
    }

	if(Input.GetKeyDown(KeyCode.F))
    {
		//全ての実行を止める方法
		DOTween.KillAll();
    }
}
```

# いろいろな拡張
移動させるDOMove以外の拡張を紹介します
## ローカル座標での移動 DOLocalMove
```c#
//ローカル座標の(5,0,0)へ3秒で移動する
this.transform.DOLocalMove(new Vector3(5f, 0f, 0f), 3f);
```

## X,Y,Zの移動量を指定して移動する
```c#
//現在の座標からX+5の座標へ3秒で移動する
this.transform.DOMoveX(5f, 3f);

//現在の座標からY+5の座標へ3秒で移動する
this.transform.DOMoveY(5f, 3f);

//現在の座標からZ+5の座標へ3秒で移動する
this.transform.DOMoveZ(5f, 3f);

```

## ジャンプして移動する DOJump
![jump.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/32fde6d5-7644-bfc4-d763-d7ce5e33b4a6.gif)

```c#
//(5,0,0)の位置に4秒で2回ジャンプして移動する
this.transform.DOJump(new Vector3(5f, 0f, 0f), jumpPower: 3f, numJumps: 2, duration: 4f);}
```
`jumpPower`の値を調整するとジャンプの高さが変わります

## 回転 DORotate
![rotate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/a2566893-a616-e1c3-6cd2-b67f974f1978.gif)

```c#
//1秒でRotationが(0,180,0)になるように回転する
this.transform.DORotate(Vector3.up * 180f, 1f)
```

第3引数に回転方法を設定できます

```c#
public enum RotateMode
{
    //
    // 概要:
    //     Fastest way that never rotates beyond 360°
    Fast = 0,
    //
    // 概要:
    //     Fastest way that rotates beyond 360°
    FastBeyond360 = 1,
    //
    // 概要:
    //     Adds the given rotation to the transform using world axis and an advanced precision
    //     mode (like when using transform.Rotate(Space.World)).
    //     In this mode the end value is is always considered relative
    WorldAxisAdd = 2,
    //
    // 概要:
    //     Adds the given rotation to the transform's local axis (like when rotating an
    //     object with the "local" switch enabled in Unity's editor or using transform.Rotate(Space.Self)).
    //     In this mode the end value is is always considered relative
    LocalAxisAdd = 3
}
```

`Fast`と`FastBeyond360`の比較すると図のようになります。
どちらもYが0度から350度へ回しています
![fast.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/04f5078e-b0af-0c8a-811e-a9433da53ecb.gif)

```c#
this.transform.DORotate(Vector3.up * 350f, 1f, mode: this.rotateMode)
```

`WorldAxisAdd`と`LocalAxisAdd`は現在の角度から`何度回転させるか`という操作になります
回転させるという意味ではこちらの方が直感的かもしれません

## 色を変える DOColor
![color.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/4df2cf8c-16b9-3c02-c698-28e74192af95.gif)



```c#
[SerializeField]
Renderer rendererComponent;

void Start()
{
	//1秒で赤色に変える
	this.rendererComponent.material.DOColor(Color.red, 1f);
}
```

MaterialだけでなくImageにも使うことができます

![color2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/8ff79d4e-30c8-86fd-2172-23876dd6abff.gif)

```c#
[SerializeField]
UnityEngine.UI.Image image;

void Start()
{
	//1秒で赤色に変化し元の色に戻るのをずっと繰り返す
	this.image.DOColor(Color.red, 1f).SetLoops(-1,LoopType.Yoyo);
}
```

## アルファを変化させる DOFade
![fade.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/0293015e-a8a2-7a9c-bf04-88546dd90dd1.gif)

```c#
[SerializeField]
UnityEngine.UI.Image image;

void Start()
{
	//1秒でImageのアルファを0にする
	this.image.DOFade(endValue: 0f, duration: 1f);
}
```

DOFadeはImageだけでなくCanvasGroupやMaterialにもつかうことができます

```c#
//1秒でCanvasGroupのアルファを0にする
this.canvasGroup.DOFade(endValue: 0f, duration: 1f);

//1秒でMaterialのアルファを0にする
this.rendererComponent.material.DOFade(endValue: 0f, duration: 1f);
```

## 音量のフェードを行う DOFade
また、AudioSourceもDOFadeで操作することができます

```c#
[SerializeField]
AudioSource audioSource;

void Start()
{
	//1秒でAudioSourceのVolumeを0にする
	this.audioSource.DOFade(endValue: 0f, duration: 1f);
}
```

## 指定したポイントを通過する DOPath
![path.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/3896edb1-e233-bd69-869a-9850040ba738.gif)

```c#
Vector3[] path =
{
	new Vector3(0f,0f,10f),
	new Vector3(5f,0f,10f),
	new Vector3(5f,0f,0f),
	new Vector3(0f,0f,0f)
};

//指定したPathを10秒で通り、進行方向を向く
this.transform.DOPath(path, 10f).SetLookAt(0.01f);
```

DOPathにはVector3の配列でPathを渡すことができます
また`SetLookAt`で進行方向に向けることができます
また、DOPathの実行中はシーンビュー上にPathが表示されます

# コールバックを使う
DOTweenは実行時に各種タイミングでコールバックを呼び出すことができます
代表的なものを紹介していきます
## 完了時のコールバック OnComplete

```c#
//(5,0,0)に2秒で移動し、移動が完了したらログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(() =>
{
	Debug.Log("OnComplete!");
});
```

ラムダ式を使わず、このように書くこともできます

```c#
void Start()
{
	//(5,0,0)に2秒で移動し、移動が完了したらログを出す
	this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(CallbackFunction);
}

void CallbackFunction()
{
	Debug.Log("CallbackFunction");
}
```

OnComplete内でさらに新しい動作を登録することもできます

```c#
//(5,0,0)に2秒で移動し、移動が完了したらY軸180度に1秒で回転する
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnComplete(() =>
{
	this.transform.DORotate(Vector3.up * 180f, 1f);
});
```

![complate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/abf9b52c-07b1-390a-a6ff-2d68a5c5f33e.gif)

## 実行開始時のコールバック OnStart
DOTweenの実行時にコールバックが呼び出されます。

```c#
//(5,0,0)に2秒で移動し、移動開始時にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnStart(() =>
{
	Debug.Log("OnStart");
});
```

なお、`SetDelay`などを使って遅延させた場合は遅延終了後にOnStartが呼ばれます

```c#
Debug.Log(Time.realtimeSinceStartup);

//(5,0,0)に2秒で移動し、移動開始時にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 2f).OnStart(() =>
{
	Debug.Log("OnStart"+ Time.realtimeSinceStartup);
}).SetDelay(2f);
```

```console
1.700801
OnStart3.848413
```

## 実行中のコールバック OnUpdate
実行中に毎フレーム呼び出されるコールバックです

```c#
//(5,0,0)に1秒で移動し、移動中にログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).OnUpdate(() =>
{
	Debug.Log($"[{Time.frameCount}] OnUpdate {this.transform.position}");
});
```

```console
[1] OnUpdate (0.2, 0.0, 0.0)
[2] OnUpdate (0.4, 0.0, 0.0)
...
[60] OnUpdate (5.0, 0.0, 0.0)
```

## 停止時のコールバック OnKill
Killが呼び出されたときのコールバックです

```c#
void Start()
{
	//(5,0,0)に10秒で移動し、Killされたらログを出す
	this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f).OnKill(() =>
	{
		Debug.Log("OnKill"+transform.position);
	});
}

private void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
		this.transform.DOKill();
    }
}
```

なお移動完了時もOnKillは呼び出されます
下記のようにOnKillとOnComplete両方を登録した場合
移動完了時に`両方の`コールバックが呼び出されます。
途中でKillした場合は`OnKillのみ`が呼び出されます

```c#
//(5,0,0)に10秒で移動し、Killされたらログを出す
this.transform.DOMove(new Vector3(5f, 0f, 0f), 1f)
	.OnKill(() =>
	{
		Debug.Log("OnKill"+this.transform.position);
	})
	.OnComplete(()=>
	{
		Debug.Log("OnComplete" + this.transform.position);
	});
```

# Sequence
DoTweenを連続で使用する場合、OnCompketeを使って繋げるのもいいのですが
Sequenceという機能を使うと連続的な挙動を簡単に実装できます

![seq1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/59e0317a-28d9-f529-79a3-5b05357abb15.gif)

```c#
//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play();
```

## 同時に実行する Join
Joinを使うと移動しながらスケールを変えるといった動作が可能になります

![seq2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/876f0483-a7a5-ff3b-3f45-f2d188313ecd.gif)


```c#
//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));
//Joinはひとつ前の動作と同時に実行される
sequence.Join(this.transform.DOScale(Vector3.one * 2f, 1f));

//Playで実行
sequence.Play();
```

## 待機する AppendInterval
AppendIntervalを使うことでSequenceの途中で待機を入れることができます
![seq3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105021/89b15ae9-0ce6-245d-363c-227759e0032e.gif)

```c#
//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
//AppendIntervalで途中に待機を入れられる
sequence.AppendInterval(3f);
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play();
```

## 止める
SequenceもKillを使って止めます

```c#
//メンバ変数でSequenceのインスタンスを作成
Sequence sequence = DOTween.Sequence();

void Start()
{
	//Appendで動作を追加していく
	sequence.Append(this.transform.DOMoveX(5f, 2f));
	sequence.Append(this.transform.DOMoveY(2f, 1f));

	//Playで実行
	sequence.Play();
}

void Update()
{
    if(Input.GetKeyDown(KeyCode.A))
    {
		this.sequence.Kill();
    }
}
```

## コールバック
Sequenceに対してもコールバックを設定することができます

```c#
//Sequenceのインスタンスを作成
var sequence = DOTween.Sequence();

//Appendで動作を追加していく
sequence.Append(this.transform.DOMoveX(5f, 2f));
sequence.Append(this.transform.DOMoveY(2f, 1f));

//Playで実行
sequence.Play()
	.OnStart(() =>
	{
		//開始時に呼ばれる
		Debug.Log("OnStart");
	})
	.OnUpdate(()=>
	{
		//実行中に毎フレーム呼ばれる
		Debug.Log("OnUpdate");
	})
	.OnComplete(() =>
	{
		//完了時に呼ばれる
		Debug.Log("OnComplete");
	})
	.OnKill(()=>
	{
		//Kill時に呼ばれる
		Debug.Log("OnKill");
	});
```


# まとめ
DOTweenのいろいろな使用方法を見てきました
今回紹介した拡張だけでなくDOTweenには数多くの拡張が存在します。
ぜひいろんな使い方を探ってみてください