【Unity】DOTweenのPluginを作成して任意の自作オブジェクトをTweenできるようにする

#はじめに
先日、__角度を表す`Angle`構造体__を作成しました。

https://qiita.com/yutorisan/items/63679fc1babb142e5b01

この構造体をDOTweenに投入したいのですが、DOTweenに投入できる型は予め決まっており、もちろん自作したオブジェクトは投入することができません。

調べてみたところ、DOTweenには__任意のオブジェクトをTweenさせることができるPluginを作成する機能__が備わっているようです。
しかし、DOTweenのPluginを作成する情報が少なく悪戦苦闘しましたので、ここに記録します。

#DOTweenのPluginを作成する

DOTween公式ホームページに、Plugin作成サンプルが掲載されています。
以下からダウンロードできます。

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

このサンプルに従って、`Angle`構造体のPluginの作成を行ってみます。

##`ABSTweenPlugin`を継承したプラグイン本体を作成する

プラグイン本体は`ABSTweenPlugin<T1, T2, TPlugOptions>`クラスを継承する必要があります。

###型引数
このクラスは型引数が3つもあるので、まずはこちらの説明をします。

- `T1`は`getter`,`setter`で扱う値の型、すなわちTweenしたい型を指定します。
- `T2`は基本的に`T1`と同じにしますが、内部演算で異なる型を使用したい場合は異なる型を指定することができます。
- `TPlugOptions`は`IPlugOptions`インターフェイスを実装した構造体を指定します。これは、`SetOptions()`等で挙動を設定可能にしたい場合に、設定値を記憶しておくための構造体です。設定項目を作らなくても良い場合は、予め用意された`NoOptions`という型を指定すればOKです。

`Angle`構造体は内部演算で異なる型を使用する必要がなかったので、`T1`と`T2`は両方とも`Angle`を指定しました。
独自のオプションを作成する必要が出てきたので、`TPlugOptions`には`AngleOptions`構造体を作成してそれを指定しました。理由は後述します。

###実装するメソッド
`ABSTweenPlugin<T1, T2, TPlugOptions>`クラスには次の抽象メソッドが定義されており、これらを実装することでプラグインとして動作するようになります。

| メソッド名 | 実装すべき処理 |
|:-:|:-:|
|  `Reset` | TweenがResetされたときに実行する処理  |
|  `SetFrom` | `From()`されたときに開始値と終了値を補正する処理  |
|  `ConvertToStartValue` | `T1`から`T2`に変換する処理  |
|  `SetChangeValue` | Tweenの開始値と終了値の変化量を取得する処理  |
|  `SetRelativeEndValue` | `SetRelative()`されたときに終了値を修正する処理  |
|  `GetSpeedBasedDuration` | `SetSpeedBased()`されたのときにDurationを補正する処理  |
|  `EvaluateAndApply` | 毎フレーム呼ばれ、実際に値を更新する処理  |

具体的にどのように実装するのかを、`Angle`構造体用プラグインの実装例で確認していきます。

####`Reset`
TweenがResetされたときに実行する処理を記述します。
基本的にプラグイン本体は状態を持たないため、処理は何も記述しません。

```csharp
public override void Reset(TweenerCore<Angle, Angle, AngleOptions> t)
{
    //何もしない
}
```

状態を持たせたい場合、後述する`PlugOptions`を使用します。

####`SetFrom`
このメソッドは`From()`が指定されたときに呼ばれます。
`From()`は「現在値から目標値」ではなく「目標値から現在値」にTweenさせる設定なので、開始値と終了値を反転させる処理を記述します。

```csharp
public override void SetFrom(TweenerCore<Angle, Angle, AngleOptions> t, bool isRelative)
{
    //開始値と終了値を反転させる
    Angle prevEndAngle = t.endValue;
    t.endValue = t.getter();
    t.startValue = isRelative ? t.endValue + prevEndAngle : prevEndAngle;
    t.setter(t.startValue);
}
```

`From`は開始値を指定できるオーバーロードもあります。
その場合は「引数で指定された値から目標値」にTweenさせる設定になるので、開始値を引数の値に設定する処理を記述します。
オーバーロードによって挙動がまったく異なるので注意が必要です。

```csharp
public override void SetFrom(TweenerCore<Angle, Angle, AngleOptions> t, Angle fromValue, bool setImmediately, bool isRelative)
{
    //引数で指定された値を開始値に設定する
    t.startValue = fromValue;
    if (setImmediately) t.setter(fromValue);
}
```

####`ConvertToStartValue`
`T1`と`T2`を異なる型にした場合に、`T1`から`T2`に変換する処理を記述します。
`T1`と`T2`が同じの場合は、ただ単に`return`するだけでOKです。

```csharp
public override Angle ConvertToStartValue(TweenerCore<Angle, Angle, AngleOptions> t, Angle value)
{
    //T1とT2は同じなのでただreturnするだけ
    return value;
}
```


####`SetChangeValue`

開始値と終了値の差分を計算する処理を記述します。

```csharp
public override void SetChangeValue(TweenerCore<Angle, Angle, AngleOptions> t)
{
    //TweenerCoreのchangeValueに開始値と終了値の差分を計算して代入する
    t.changeValue = t.endValue - t.startValue;
}
```

####`SetRelativeEndValue`

`SetRelative()`が指定されているときに呼ばれます。
目標値が相対値となるように、目標値を修正する処理を記述します。

```csharp

public override void SetRelativeEndValue(TweenerCore<Angle, Angle, AngleOptions> t)
{
    t.endValue += t.startValue;
}
```

####`GetSpeedBasedDuration`

`SetSpeedBased()`が指定されているときに呼ばれます。

通常、Tweenの継続時間はdurationに[単位：s]で指定しますが、継続時間ではなく速度で指定したいときがあると思います。
そういうときに、`SetSpeedBased()`を指定すると、__durationに指定された値が速度[単位：/s]として扱われるようになります__。
正確には、ユーザーが[/s]として指定した速度値から、継続時間[s]に変換する処理が内部的に入ることになります。

つまり、このメソッドには、__durationに指定された速度値[/s]を、継続時間[s]に直す処理を書けば良い__ことになります。

`Angle`構造体の場合、速度とは角速度です。しかし、単位は[rad/s]と[°/s]の2パターンあり、durationに指定された`float`値からは単位がどちらなのか判断できません。
そこで、次のような列挙型を作って`AngleOptions`構造体に乗せ、`Unspecified`の場合は例外を投げるようにしました。

```csharp
/// <summary>
/// 角速度の単位
/// </summary>
public enum AngularVelocityUnit
{
    /// <summary>
    /// 未指定
    /// </summary>
    Unspecified,
    /// <summary>
    /// °/s
    /// </summary>
    DegreePerSecond,
    /// <summary>
    /// rad/s
    /// </summary>
    RadianPerSecond
}
```

最終的な実装は次のようになりました。

```csharp
public override float GetSpeedBasedDuration(AngleOptions options, float unitsXSecond, Angle changeValue)
{
    switch (options.AngularVelocityUnit)
    {
        case AngularVelocityUnit.DegreePerSecond:
            return changeValue.TotalDegree / unitsXSecond;
        case AngularVelocityUnit.RadianPerSecond:
            return changeValue.TotalRadian / unitsXSecond;
        case AngularVelocityUnit.Unspecified:
            throw new InvalidOperationException("角速度の単位が指定されていません。");
        default:
            throw new InvalidEnumArgumentException();
    }
}
```

`Angle`構造体の場合は単位の性質上このように複雑な処理になってしまいましたが、通常は単に[/s]から[s]に単位変換する処理を書けば良いと思います。

####`EvaluateAndApply `
最後に`EvaluateAndApply`です。これは毎フレーム呼ばれ、実際にTweenで値を更新するロジックを記述します。
公式の実装例を参考にして、次のように実装しました。

```csharp
public override void EvaluateAndApply(AngleOptions options,
                                        Tween t,
                                        bool isRelative,
                                        DOGetter<Angle> getter,
                                        DOSetter<Angle> setter,
                                        float elapsed,
                                        Angle startValue,
                                        Angle changeValue,
                                        float duration,
                                        bool usingInversePosition,
                                        UpdateNotice updateNotice)
{
    //イージング係数を取得
    float ease = EaseManager.Evaluate(t, elapsed, duration, t.easeOvershootOrAmplitude, t.easePeriod);
    //setterに更新された値を渡す
    setter(startValue + changeValue * ease);
}
```

以上でメソッドの実装は完了となります。

###シングルトンパターン

作成したプラグインを使うにはプラグインのインスタンスが必要なのですが、プラグインは状態を持たないのでインスタンスを共有しても問題がないです。
したがって、以下のようにシングルトンパターンを適用しておきます。

```csharp
public class AnglePlugin : ABSTweenPlugin<Angle, Angle, AngleOptions>
{
    private static AnglePlugin _instance;
    public static AnglePlugin Instance => _instance ?? (_instance = new AnglePlugin());

    private AnglePlugin() { }
   
    //..中略..
}
```
これで`AnglePlugin`は唯一インスタンスしか作られなくなるので、メモリに優しいです。

以上でプラグイン本体の実装は完了となります。

##`IPlugOptions`を実装したプラグインのオプションを作成する

続いて、自作のPluginに独自の設定を与えられるPlugOptionsを作成します。

`AnglePlugin`には次のオプションを設定可能にしました。

- `SetSpeedBased()`されたときに角速度の単位が[rad/s]なのか[°/s]なのかを指定するオプション
- 常に正転Tweenするオプション

1個めのオプションについては[GetSpeedBasedDuration](#GetSpeedBasedDuration)節で説明したとおりです。
2個めのオプションは、例えば270°から0°にTweenするとき、270°→0°と逆回転するのではなく、270°→360°のように正転に変換してTweenさせるような設定を可能にするものです。

それでは、実際に`AnglePlugin`用のオプション構造体`AnglePlugOptions`を作っていきます。

PlugOptionsとして扱うには、`IPlugOptions`インターフェイスを実装した構造体としてオブジェクトを定義する必要があります。

```csharp
public struct AngleOptions : IPlugOptions
{

}
```

この中に保存したいオプションの値を追加していけば良いです。

また、`IPlugOptions`インターフェイスの定義は次のようになっています。

```csharp
public interface IPlugOptions
{
    void Reset ();
}
```
TweenがResetされるときにこちらも呼ばれるようです。
設定を初期状態に戻すコードを実装すれば良さそうですね。

上記をすべて実装すると次のようになります。

```csharp
/// </summary>
/// プラグインのオプション本体
/// </summary>
public struct AngleOptions : IPlugOptions
{
    public void Reset()
    {
        Direction = AngleTweenDirection.Both;
        AngularVelocityUnit = AngularVelocityUnit.Unspecified;
    }

    public AngleTweenDirection Direction { get; set; }
    public AngularVelocityUnit AngularVelocityUnit { get; set; }
}

/// <summary>
/// Tweenする回転方向
/// </summary>
public enum AngleTweenDirection
{
    /// <summary>
    /// 両方
    /// </summary>
    Both,
    /// <summary>
    /// 正転のみ
    /// </summary>
    Forward
}

/// <summary>
/// 角速度の単位
/// </summary>
public enum AngularVelocityUnit
{
    /// <summary>
    /// 未指定
    /// </summary>
    Unspecified,
    /// <summary>
    /// °/s
    /// </summary>
    DegreePerSecond,
    /// <summary>
    /// rad/s
    /// </summary>
    RadianPerSecond
}
```

ここで追加した`AngleTweenDirection`のオプションを使えるようにするために、プラグイン本体の`SetChangeValue`メソッドを次のように修正しました。

```csharp

public override void SetChangeValue(TweenerCore<Angle, Angle, AngleOptions> t)
{
    var diffAngle = t.endValue - t.startValue;
    //正転オプションが有効になっていたら、AngleをPositiveNormalizeして正転方向に正規化する
    if (t.plugOptions.Direction == AngleTweenDirection.Forward) t.changeValue = diffAngle.PositiveNormalize();
    //オプションがなければそのまま
    else t.changeValue = diffAngle;
}
```

これでオプションが使えるようになります。

##独自の`SetOptions`拡張メソッドを定義する

オプションは使えるようになりましたが、オプションを設定する方法がありません。
そこで、独自の`SetOptions`拡張メソッドを定義します。
ついでに、`SetSpeedBased`時に同時に角速度の単位を指定できるようなオーバーロードも作っておきます。

```csharp

public static class AngleTweenExtensions
{
    //AnglePlugin用のオプション設定を行うメソッド
    public static Tweener SetOptions(this TweenerCore<Angle, Angle, AngleOptions> t, AngleTweenDirection direction, AngularVelocityUnit angularVelocityUnit = AngularVelocityUnit.Unspecified)
    {
        if (t == null || !t.active) return t;
        t.plugOptions.Direction = direction;
        t.plugOptions.AngularVelocityUnit = angularVelocityUnit;
        return t;
    }

    //同時に角速度の単位の設定ができるSetSpeedBasedメソッドのオーバーロード
    public static Tweener SetSpeedBased(this TweenerCore<Angle, Angle, AngleOptions> t, AngularVelocityUnit unit)
    {
        if (t == null || !t.active) return t;
        t.SetSpeedBased();
        t.plugOptions.AngularVelocityUnit = unit;
        return t;
    }
}

```

#作成した`AnglePlugin`を使ってみる

ようやくここまで来ました。
作成した`Angle`構造体用のDOTweenプラグイン`AnglePlugin`を使ってみましょう。
プラグインの使い方としては、`DOTween.To`の第一引数に、プラグインのインスタンスを指定するだけです。

```csharp
DOTween.To(AnglePlugin.Instance,          //AnglePluginを使う
           () => Angle.Zero,              //初期値は0°
           ang => { /*何らかの更新処理*/ },  //更新処理
           Angle.Round,                   //目標値は360°
           1f);                           //継続時間は1秒
```

これに従って、オブジェクトを円周上で回転移動Tweenさせてみます。
次のようなコードを用意しました。

```csharp
using DG.Tweening;
using DG.Tweening.Core;
using UnityEngine;
using UnityUtility;

public class DOTweenAngle : MonoBehaviour
{
    //90°から360°まで回転移動するTween
    private TweenerCore<Angle, Angle, AngleOptions> GetBaseTween(float duration) =>
        DOTween.To(AnglePlugin.Instance,
                   () => Angle.FromDegree(90),
                   ang => MoveOnCircumference(ang),
                   Angle.Round,
                   duration)
               .SetEase(Ease.Linear)
               .SetDelay(0.5f);

    //角度を指定すると半径1のXY円周上の座標に移動する
    private void MoveOnCircumference(Angle angle) =>
        this.transform.position = new Vector3(Mathf.Sin(angle.TotalRadian),
                                              Mathf.Cos(angle.TotalRadian),
                                              0);

    [ContextMenu(nameof(Basic))]
    private void Basic()
    {
        //90°から360°まで
        GetBaseTween(1f);
    }

    [ContextMenu(nameof(Relative))]
    private void Relative()
    {
        //90°から450°まで一周
        GetBaseTween(1f).SetRelative();
    }

    [ContextMenu(nameof(From))]
    private void From()
    {
        //360°から90°まで
        GetBaseTween(1f).From();
    }

    [ContextMenu(nameof(From180))]
    private void From180()
    {
        //180°から360°まで
        GetBaseTween(1f).From(Angle.FromDegree(180));
    }

    [ContextMenu(nameof(SpeedBased))]
    private void SpeedBased()
    {
        //90°/sの速さで90°から360°まで
        GetBaseTween(90).SetSpeedBased(AngularVelocityUnit.DegreePerSecond);
    }

    [ContextMenu(nameof(AlwaysForward))]
    private void AlwaysForward()
    {
        //0°から90°まで
        GetBaseTween(1f).From().SetOptions(AngleTweenDirection.Forward);
    }
}

```

##結果

90°から360°まで1秒でTween
![Basic.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/dbbce450-41de-27e2-0000-20ac59005cdc.gif)


`Relative()` →　90°から450°になる
![Relative.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/1a57a2e3-e7bf-983e-89f6-45b54e862415.gif)


`From()` → 開始値と終了値が逆転、360°から90°になる
![From.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/7de3f7b4-b56f-1b07-1755-05deed6bbed2.gif)

`From(Angle.FromDegree(180))` → 180°から360°
![From180.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/4e652529-16e9-89dd-673b-19523998c46a.gif)

Durationに90をセット、角速度の単位を°/sにして、`SetSpeedBased()` → 90°から360°を90°/sの速さでTween
![SpeedBased.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/8864d933-82e6-a60c-c4ee-3e9f61ccbf01.gif)

`From()`（360°から90°にTween）に対して`SetOptions(AngleTweenDirection.Forward)` → 正転方向にTweenするので0°から90°
![AlwaysForward.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697830/4eef33c7-5906-8a1c-e224-44c1b2949b96.gif)

#最後に

この記事では、DOTweenのPlugin作成機能を使って任意のオブジェクトをTweenする方法を紹介しました。
情報があまりなくて手探り状態で色々試しましたが、きちんとモノになってよかったです。
ここで紹介した手法を参考にすれば、任意のオブジェクトをTweenさせるPluginも作成可能と思います。

この記事で使用したUnityプロジェクトとソースコードは下記GitHubでも公開しています。

https://github.com/yutorisan/DOTweenPlugin-For-AngleStruct

また、Angle構造体本体については下記GitHub及び記事を参照ください。

https://github.com/yutorisan/UnityUtility/blob/develop/UnityUnility/Structs/Angle.cs

https://qiita.com/yutorisan/items/63679fc1babb142e5b01

ご意見ご指摘等ありましたらコメントをよろしくお願い致します。