【Unity】MagicTweenとPrimeTweenを触ってみた

本記事は [QualiArts Advent Calendar 2023](https://qiita.com/advent-calendar/2023/qualiarts)の11日目の記事です。

## MagicTweenとPrimeTweenを触ってみた

Unityでアニメーションを実装する際にTweenAssetを使えば簡単に演出を作ることができます。
これまでは、[DOTween](https://dotween.demigiant.com/documentation.php)と呼ばれるアセットを使うことが多かったのですが、今年になり、新しいTweenアセットが公開され気になっていました。

 * PremiTween
 * MagicTween 
 
私自身はまだこれらのアセットを使った経験がありませんが、今回、記事を書かせてもらう機会をいただいたので、これら2つの新しいアセットがどのようなものなのかを調べてみることにしました。

#### 調査環境
Unityのバージョンは2022.3.8f1を使っています。

 ## PrimeTween
 まずはPrimeTweenを簡単に紹介します。
 
 https://github.com/KyryloKuzyk/PrimeTween#dotween-adapter

 ### 導入
 導入方法なのですが、UnityのAssetStoreに公開されていますので、まずはこちらからUnityプロジェクトへインストールを行います。

https://assetstore.unity.com/packages/tools/animation/primetween-high-performance-animations-and-sequences-252960

インストールが終われば、PrimeTweenInstallerを開きます。
「Install Prime Tween」を押せば利用できるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/234c245d-2d88-b39b-14da-fdca1089a1ee.png" width="500">

### 基本アニメーション
PrimeTweenを使って実現できる基本的なアニメーション――ポジション、スケール、回転――を紹介します。
特定のプロパティを操作し、目的とするアニメーションを簡単に実現することができます。

#### コード例
PrimeTweenのライブラリにプロパティに応じて関数が用意されています。
これをアニメーションに応じて使い分けます。
```c#
//基本的な記述 例
// Tween.関数(対象プロパティ、終了値、経過時間)
Tween.PositionY(transform, endValue: 10, duration: 1f);
```

 ```c#:実装例
// ポジション
Tween.Position(transform, endValue: Vector3.one, duration: 1f);

// スケール
Tween.Scale(transform, 2.0f, 1f);

// 回転
Tween.LocalRotation(transform, endValue: new Vector3(0,180,0), 1f);

```

#### コールバック
コールバックを実装する場合は次のようなコードになります。
```c#:コールバック例
Tween.PositionX(transform, 100 , 0.5f)
        .OnComplete(() => Hoge());
```

#### Sequence
Sequenceは順に実行されるTweenとCallbackの集合体です。.Group()でTweenを平行実行し、.Chain()で連続実行できます。ChainDelay()を使い開始遅延を設定してTweenのタイミングを調整することもできます。

 ```c#:Sequenceの並列処理
Sequence.Create()
    .Group(Tween.PositionX(_image.transform, 200, 0.3f))
    .Group(Tween.Scale(_image.transform, 2.0f, 1f));
 ```

```c#:Sequenceの直列処理
Sequence.Create()
    .Chain(Tween.PositionX(_image.transform, 200, 0.3f))
    .ChainDelay(1) // 遅延処理
    .Chain(Tween.Scale(_image.transform, 2.0f, 0.1f));
```

#### Easing
Easingを設定する場合は
```C#:Easing設定
Tween.PositionX(transform, endValue: 10, duration: 1, ease: Ease.InOutSine);
```

#### ループ
ループを設定する場合は、cyclesとCycleModeの設定を行います。
```C#:ループ設定
// 5回ループ
var tween = Tween.PositionY(transform, 300, 0.5f, cycles: 5, cycleMode: CycleMode.Yoyo);
```

### カスタムアニメーション
Tween.Custom() というメソッドを使うことで、さまざまな種類のデータをアニメーションとして表現することができます。

```c#:実装例
// 1 => 10へ1.0fかけて値を変更する
Tween.Custom(1, 10, 1.0f, onValueChange: x => _image.transform.localScale = new Vector3(x, x, x));
```

スケール値を変更する Custom の実装例になります。Tween.Scale を使えば良いのですが、これ を Custom に置き換えると次のような感じになります。
onValueChangeで、変化した値をセットしています。

### Inspectorで値を設定
PrimeTweenとInspectorの統合について説明します。
この機能を利用することで、コードを一切変更せずにInspectorから直接アニメーションの設定を行えるようになります。
すべてのアニメーション設定は、MonoBehaviourやScriptableObjectに保存（シリアライズ）でき、それらをTweenメソッドに適用することができます。

```c#
[SerializeField]
private TweenSettings<float> _positionYTweenSettings;
```

Inspectorで各パラメーターを設定ができます。
このパラメーターを使ってアニメーションを作成することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/5909a1c0-bd20-acda-6f8f-1064d1906ae5.png" width="500">

```c#:利用例
Tween.PositionY(transform, _positionYTweenSettings);
```

#### 感想
Inspectorを使用するメリットの一つは、コードを書かずに実装が可能であることです。特に、 デザイナーや他の非エンジニアの人々と作業を進める際に便利で、エンジニア抜きにTweenの設定を変更することができます。

### デリゲート処理のゼロアロケーション
先程コールバックで説明したデリゲート処理なのですが、PrimeTweenの場合、実装を少し変更するとメモリー確保することなく実行することができます。

```c#:実装例
Tween.PositionY(_image.transform, 300, 4.5f)
           .OnComplete(target: this, target => target.Hoge());
```
 OnComplete()にthis参照が渡され、Hoge()を直接呼び出す代わりにtargetパラメータが使用されることに注意してください。

#### アロケーションの計測結果
実際に正しく動作しているか確認してみます。UnityのProfileを使って計測をしてみました。

```c#:計測に利用したコード
Profiler.BeginSample("GCTest: Normal");
Tween.Position(_image.transform, new Vector3(10, 0), duration: 0.05f)
    .OnComplete(() => Hoge());
Profiler.EndSample();
Profiler.BeginSample("GCTest: Zero allocation");
Tween.Position(_image2.transform, new Vector3(10, 0), duration: 1)
    .OnComplete(target: this, target => target.Hoge());
Profiler.EndSample();
```

計測結果は次のようになります。
Normalの方ではGC.Allocが発生しているのがわかると思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/c825b4f1-494b-a6c1-3e75-78d55927e94d.png" width="800">

### デバック
PrimeTweenを利用するときに専用のデバック画面が用意されています。
Unity を再生すると、DontDestoryOnLoad配下にPrimeTweenManagerが生成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/88f76c11-4d5d-8f48-a6d4-89cb6249d406.png" width="400">

#### Window
PrimeTweenManagerのInspector、少し見にくいですが次のような感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/b915eeee-f1d8-f9c3-d5a1-851c2d878efa.png" width="600">

ここでは、現在実行中のすべてのTweenとそれらのプロパティを確認できます。

 * Alive Tweens : 現在再生中の Tween の数
 * Max Alive Tweens : 同時に再生されたTweenの最大値が表示されます

Max Alive Tweensを見て、適切なTweenの許容値を設定することでランタイム中に追加のメモリを割り当てることなく動作できます。
```c# : Tweenの最大値設定
PrimeTweenConfig.SetTweensCapacity(10);
```

### DOTweenからPrimeTweenへの移行
DOTweenで実装している個所をPrimeTweenへ置き換えてくれる機能(アダプター)が用意されています。
この機能を有効にするためには、「PRIME_TWEEN_DOTWEEN_ADAPTER」定義を
プロジェクトのScriptCompilationに指定します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/576268a4-9563-8127-1fe9-a3e4c14dfa82.png" width="600">

#### 移行
設定が終われば、既存のDOTweenのコードのNameSpaceを変更してみます。
```c#:置き換え
// これを
using DG.Tweening;
// 置き換える
using PrimeTween;
```
通常なら、デバックエラーが出るのですが、PrimeTween用のアダプターが大部分の変換を自動で処理してくれるので、そのままコードの実行ができます。コードは外観上は DOTweenのままでありながら動作することができます。

:::note warn
コードが内部的にPrimeTweenに変換されるため、DOTween特有の動作や振る舞いが正しく変換できない場合がありますので、動作の確認を行ってください。
:::
 
 ## MagicTween
 次に紹介をするのがMagicTweenです。
 
https://github.com/AnnulusGames/MagicTween

### 導入
次のUnity環境が MagicTween を動かすのに必要になります。
 * Unity 2022.1 or higher
 * Entities 1.0.0 or higher 
 * Burst 1.8.8 or higher

MagicTweenはPackageManagerの「Git URL からパッケージを追加」からインストールを行います。
URLはGithubの画面から取得します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/6c2bb5e7-e2c0-effb-2370-bcdf3357d22e.png" width="400">

### 基本アニメーション
MagicTwenの使い方について紹介をします。
DOTweenを使っている人であるなら、ほとんど 記述形式が似ていますので直感的に使うことができます

#### コード例
MagicTweenのライブラリにプロパティに応じて関数が用意されています。
```c#:基本的記述
transform. //コンポーネント 
TweenPosition( // Tween関数
new Vector3(1.0f, 2.0f, 3.0f), // 終了値 
1.0f); //経過時間
);

// 開始値を設定する例 
transform.
TweenPosition(
new Vector3(0f, 0f, 0f), //開始値
new Vector3(1.0f, 2.0f, 3.0f), // 終了値 
1.0f);
);
```

まず基本的なアニメーション機能(位置、スケール、回転)を解説します。
MagicTween は DOTween と非常に類似したアセットで、よく似たコードを使用して、ほぼ同じ結 果を得ることが可能です。
 ```c#:実装例
// ポジション
transform.TweenPosition(new Vector3(50,0), 0.3f);

// スケール
transform.TweenLocalScale(new Vector3(2,4), 0.3f);

// 回転
var rotation = Quaternion.Euler(0, 0, 45); 
_image.transform.TweenRotation(rotation, 0.3f);
```

#### Sequence
複数の Tween を再生する仕組みが Sequence(シークエンス)です。Sequence を利用することで
複数の Tween を組み合わせて、複雑なアニメーションを作成することができます。

次の関数を組み合わせて作成します。

 * Append
 * Insert
 * Prepend
 
 Append系関数は、Sequenceの末尾にTweenを追加します。
```c#:Append
// Tween挿入 
sequence.Append(_image.transform.TweenLocalPositionX(50, 0.3f));
// インターバル挿入 
sequence.AppendInterval(0.5f);
// コールバック挿入
sequence.AppendCallback(() => Debug.Log("コールバック"));
```

Tweenを任意の位置に挿入したい場合は、Insert系関数を利用できます。
```c#:Insert
// Tween挿入 
sequence.Insert(0.0f,_image.transform.TweenLocalPositionX(50, 0.3f));

// コールバック挿入 
sequence.InsertCallback(0.5f, () =>{
    Debug.Log(("コールバック"));
});
```

Sequenceの先頭にTweenを追加したい場合、Prepend系関数を使います。
```c#:Prepend
// Tween追加 
sequence.Prepend(_image.transform.TweenLocalPositionX(0, 0.3f));
// インターバル 
sequence.PrependInterval(0.5f);
// コールバック挿入 
sequence.PrependCallback(() => {
    Debug.Log(("コールバック")); 
});
```

### カスタム
汎用的なTweenアニメーションを作成するための関数です。
特定の値を時間経過と共に滑らかに変化させたい場合に利用します。

```C#:カスタム
int hoge = 0;
Tween.To(() => hoge, x => hoge = x + 1, 100, 1f);
```
### TextMeshPro
TextMeshPro 専用の関数が用意されています。 

#### 文字アニメーション
一部ですが、Text 単位で次のようなアニメーションを再生することができます
 * TweenColor : 文字色変更
 * TweenOffset : 位置調整 
 * TweenAlpha : アルファ調整
 * TweenFontSize : フォントサイズ
 
実際の使い方は次のようになります。
拡張メソッドを用意していますが、実際の処理は Tween.Toを利用しています。
```c#:利用例
private TMP_Text _text;
_text.TweenFontSize(48.0f,1.0f);

// 実際の中身
Tween.To(self, self => self.fontSize, (self, x) => self.fontSize = x, endValue, duration);
```
#### 文字ごとのアニメーション
文字ごとのアニメーションをさせたい場合は次のような関数が用意されています。
 
 * TweenCharColor : 文字色変更
 * TweenCharOffset : 位置調整
 * TweenCharRotation : 角度調整
 * TweenCharScale : スケール調整
 * TweenCharColorAlpha : アルファ調整

```c#:利用例
for (int i = 0; i < _text.GetCharCount(); i++)
{
    _text.TweenCharColor(i, Color.blue, 1.0f).SetDelay(0.5f * i);
}
```

### 拡張
アニメーションの細かい制御が必要な時、MagicTweenでは、型の拡張のために二つのインターフェースICustomTweenPlugin、ITweenOptionsを提供しています。

#### TweenPlugin
ICustomTweenPluginを継承して作成を行います。

```c#:実装例
[TweenPlugin]
public readonly struct CustomMagicTweenPlugin : ICustomTweenPlugin<float,NoOptions>
{
    public float Evaluate(in float startValue, in float endValue, in NoOptions,
        in TweenEvaluationContext context)
    {
        // 計算を行いvalueを返す
    }
}
```

TweenEvaluationContextにはアニメーション情報が入ってきます。
 * Progress : 経過時間
 * IsRelative : 相対設定かどうか
 * IsInverted : 反転かどうか

 この情報を使って実際の振る舞いを計算します。

 ```c#:単純なfloat側を返す処理
public float Evaluate(in float startValue, in float endValue, in NoOptions,
        in TweenEvaluationContext context)
    {
        // IsRelative (相対設定）
        var customValue = context.IsRelative ? startValue + endValue : endValue;

        // IsInverted(逆向け再生かどうか）
        return context.IsInverted
            ? Mathf.Lerp(customValue, startValue, context.Progress)
            : Mathf.Lerp(startValue, customValue, context.Progress);
    }
 ```

次のように利用をします。
 ```c#:実装
 Tween.FromTo<float, NoOptions, CustomMagicTweenPlugin>(x => value = x, 0f, 10.0f, 0.5f);
 ```

 先程の例では Option を何も指定していませんでした(NoOptions)が、独自の値を使いたい場合、Opetion を 拡張すれば可能になります。

```c#:オプション拡張
public struct CustomTweenOptions : ITweenOptions
{
    public readonly bool IsCustom;

    public CustomTweenOptions(bool isCustom)
    {
        IsCustom = isCustom;
    }
}
```

## 最後に
Tween系のアセットだけではなく、外部Assetをプロジェクトへ導入するときにはいくつか気をつける点があると思います。
やりたいことがこのアセットの機能で実現できるかどうか、パフォーマンスは良いのか、継続的にアップデートを行ってくれるのかどうか、ドキュメントが充実しているかどうか、などです。

これはプロジェクトの方針もあると思うので一概にこうだとは言えないですが、最低限継続的にアップデートをしてくれるかどうかが個人的には重要だと思っています。Unityのアプデなどが今後継続して行われるので、このバージョンでは使えませんとか、対応するけど数ヶ月先ですとかであれば、ちょっと考えます。

今回紹介したTweenアセットに関してはまだ始まったばかりですので、このあたりがとても気になりますね。