0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tweenを簡単に自作してみる

Last updated at Posted at 2024-09-22

はじめに

これはLerp関数を最近知った筆者がTweenを自作してみた話

Tweenとは?

始点、終点、間の挙動を指定することで、間を補間してアニメーションさせることのできるライブラリです。
Unityだと DOTween1 や、 MagicTween2 などが有名です。

Tweenを使用すると、アニメーションを簡潔に実装できます。
次のコードはDOTweenを使用してx軸正方向に1だけ2秒かけて移動する処理を実装したものです。

DOTweenの使用例
transform.DOMoveX(transform.position.x + 1, 2f);

Tweenの自作

実際に作っていきます。

Lerp関数を使ってUpdate関数内に実装する

Lerp関数とは?

この自作Tweenの一番の要はLerp関数です。

Lerp(a, b, t)とは、始点a, 終点b としたときの割合tだけ進んだ値を返す関数で、グラフでは下のようになります。

image.png

例えば
a=1, b=5, t=0.5のとき、1 から 5 までの道を 50% 進んだ点なので Lerp(1, 5, 0.5) = 3 となります。
a=4, b=8, t=0のとき、4 から 8 までの道を 0% 進んだ点なので Lerp(4, 8, 0) = 4 となります。

Lerp関数を使用してdeltaTime経過時の位置を求める

dulation秒かけてaからbまで同じ速度で移動するとき、
移動開始からの経過時間をdeltaTimeとすると、

deltaTime経過時の位置は Lerp(a, b, deltaTime/duration) と表せます。

deltaTime = 0 のとき、deltaTime/duration = 0%
deltaTime = duration/2 のとき、deltaTime/duration = 50%
deltaTime = duration のとき、deltaTime/duration = 100%
のようになる。

Update関数内にTweenを実装する

前節の計算を deltaTimeduration を超えるまで繰り返せば良いため、
Update関数内に次のコードを書くことで、始点から終点までを指定時間かけて等速で移動します。
下のコードでは、 Vector3 に用意された Lerp を使用しています。

TweenTest.cs
using UnityEngine;

public class TweenTest : MonoBehaviour
{
    // Tween開始からの経過時間
    float deltaTime = 0f;
    // Tweenにかける時間
    float duration = 2f;

    // 移動の開始位置と終了位置
    Vector3 startPos = new Vector3(-3, 0, 0);
    Vector3 endPos = new Vector3(3, 0, 0);
    
    void Start()
    {
        // Tweenを開始するのでdeltaTimeを0にする
        deltaTime = 0f;
    }

    void Update()
    {
        // 経過時間を足す
        deltaTime += Time.deltaTime;

        Vector3 pos = Vector3.Lerp(startPos, endPos, deltaTime/duration);
        transform.position = pos;
    }
}

実際に上のスクリプトをアタッチして動かしてみるとこんな感じ

Linear Tween Circle.gif

Taskを使用してTweenを実装する

前章では、Lerpを使用してUpdate関数内にTweeenを実装してみました。
今章ではTweenを使いまわせるようにTaskを使って関数にします。

Task内で経過時間を取得するには?

Time.deltaTiime で取得できる経過時間は前回のフレームからの時間です。
Update関数はフレーム毎に呼ばれていたため、

deltaTime += Time.deltaTime;

と書くだけで経過時間を計算できました。

それに対してTaskではループの中で 1フレーム待つ必要があるため、 await Task.Yield(); を使用します。

次のコードはTaskを使用して実装したTweenと、その使用例です。

MyTween.cs
using System.Threading.Tasks;
using UnityEngine;

public static class MyTween
{
    public static async Task TweenPosition(Transform transform, Vector3 startPos, Vector3 endPos, float dulation)
    {
        float deltaTime = 0f;

        Vector3 pos;

        while (deltaTime <= duration)
        {
            // 1フレームの間の秒数を足す
            deltaTime += Time.deltaTime;

            // 位置を求める
            pos = Vector3.Lerp(startPos, endPos, deltaTime/duration);
            
            // 位置を更新
            transform.position = pos;

            // 1フレーム待つ
            await Task.Yield();
        }
    }
}
TweenTest.cs
using UnityEngine;

public class TweenTest : MonoBehaviour
{
    // Tweenにかける時間
    float duration = 2f;

    // 移動の開始位置と終了位置
    Vector3 startPos = new Vector3(-3, 0, 0);
    Vector3 endPos = new Vector3(3, 0, 0);
    
    void Start()
    {
        // Tweenを開始する
        MyTween.TweenPosition(transform, startPos, endPos, duration);
    }
}

実際に動かしてみると、Update関数内に実装した時と同じように動くことが確認できます。

Task実行時の警告を消す

現時点だとTaskawaitしていないため警告がでます。

image.png

これはTweenPositionの戻り値がTaskであるためなので、
戻り値の型をvoidにしてやると警告は出なくなりますが、
そのまま変更すると今度はTweenPositionawaitできなくなります。

そこで、何もしない拡張メソッド Forget() を追加することで警告を出さないようにしましょう。
以下はForgetの実装と、使用例です。

MyTweenクラスに追加
// Task を await しなかったときでも警告が出ないようにするための拡張メソッド
public static void Forget(this Task task) { }
使用例
// awaitしなくても警告が出なくなる
MyTween.TweenPosition(transform, startPos, endPos, duration).Forget();
// awaitもできる
await MyTween.TweenPosition(transform, startPos, endPos, duration);

UnityActionを使用してなんでもTweenする

前章まででは、positionの変更のみに対応したTweenを実装してきました。
この処理を基準にposition以外にも使い回せるようにしていきましょう

Mathf.Lerp

今までは

Vector3.Lerp(a, b, t);
Vector3のLerpを使用してきましたが、

Mathf.Lerp(a, b, t);
というfloat型のLerpも用意されています。

float型の数値をTweenさせてその値を使用することで、様々な要素のTweenに応用できます。

例えば、

Textの色をTweenさせたい場合は、
text.color = new Color(r, g, b);
r,b,g をTweenさせることで実現でき、

Textの数値をTweenさせたい場合は、
text.text = x.ToString();
x をTweenさせることで実現できます。

UnityAction

Tweenさせた値をどのように貰うかという問題については、UnityActionラムダ式 を使用することで解決します。

次のコードでは、float型の変数valueをTweenさせ、valueの更新時にUnityActionを実行することで、更新したvalueを渡し、position.xをTweenさせています。

MyTween内に追加した関数
public static async Task TweenFloat(float startValue, float endValue, float duration, UnityAction<float> action)
{
    float deltaTime = 0f;

    float value = startValue;

    while (deltaTime <= duration)
    {
        // 1フレームの間の秒数を足す
        deltaTime += Time.deltaTime;

        // valueを求める
        value = Mathf.Lerp(startValue, endValue, deltaTime/duration);
        
        // valueの更新を伝える
        action.Invoke(value);

        // 1フレーム待つ
        await Task.Yield();
    }
}
変更したTweenTestの処理
void Start()
{
    // Tweenを開始する
    MyTween.TweenFloat( -3, 3, duration, (x) =>
    {
        transform.position = new Vector3(x, transform.position.y, transform.position.z);
    }).Forget();
}

先ほどの例に挙げていたTextのTweenは以下のようにして実装できます。

TextのTween
[SerializeField] Text text;
    
void Start()
{
    // Tweenを開始する
    MyTween.TweenFloat( 0, 1, duration, (x) =>
    {
        text.text = x.ToString();
        text.color = new Color(x, x, x);
    }).Forget();
}

動かしてみるとこのようになります

Linear Tween Text Value Color.gif

イージング

次にTweenFloatにイージングを付けてみましょう。

イージングとは?

イージングとは、Tweenに緩急をつけることで動きに面白さを加えられる機能です。

例えば、プレイヤーが移動を

  • 最初はゆっくり加速
  • 真ん中で最速になる
  • 最後は減速して止まる

のように実装するとリアルな動きになります。

イージングの実装と関数

急ですがここで下図のような関数を考えてみます3

image.png

この関数に対してdeltaTime/durationを入力として入れると、
Tweenが始まってすぐ(deltaTime/duration ≒ 0) ではyの値が急激に増え、
Tweenが終わる頃(deltaTime/duration ≒ 1) になるとyの値がゆっくり増えます。

この関数を使った値をLerp関数に入れることではじめは速く、終わりはゆっくりなEasingを実現できます。

実際に何種類かのEasingを実装したものが次のコードです。
ちなみにEasing用の関数はGeogebra3 等を使用すると作りやすいです。

Easingを実装したMyTween
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

public static class MyTween
{
    // floatの値をTweenする関数
    public static async Task TweenFloat(float startValue, float endValue, float duration, EasingType easingType , UnityAction<float> action)
    {
        float deltaTime = 0f;

        float x,y;
        float value = startValue;

        while (deltaTime <= duration)
        {
            // 1フレームの間の秒数を足す
            deltaTime += Time.deltaTime;

            // Easing
            x = deltaTime / duration;
            y = ConvertEasingValue(x, easingType);

            // valueを求める
            value = Mathf.Lerp(startValue, endValue, y);
            
            // valueの更新を伝える
            action.Invoke(value);

            // 1フレーム待つ
            await Task.Yield();
        }
    }

    // Task を await しなかったときでも警告が出ないようにするための拡張メソッド
    public static void Forget(this Task task) { }

    // EasingTypeごとに値を返す関数
    private static float ConvertEasingValue(float x, EasingType easingType)
    {
        switch (easingType)
        {
            case EasingType.Linear:
                return x;
            case EasingType.QuadOut:
                return -(x - 1) * (x - 1) + 1;
            case EasingType.SinIn:
                return Mathf.Sin(Mathf.PI * 0.5f * (x - 1)) + 1;
            default:
                return x;
        }
    }

    // Easingの種類
    public enum EasingType
    {
        Linear,
        QuadOut,
        SinIn,
    }
}

使用するとこんな感じ

上:QuadOut , 中:SinIn , 下:Linear
Tween Easing 3.gif

CancellationToken

オブジェクトが破壊された、途中でシーンが変わった、途中でゲームが終了した

といったときにTaskを停止するためにCancellationTokenを持たせましょう。

Task.Yield();
にはCancellationTokenを渡すことができないので、キャンセルを判定してreturnします。

CancellationTokenを追加したMyTween
public static async Task TweenFloat(float startValue, float endValue, float duration, EasingType easingType , UnityAction<float> action, CancellationToken token)
{
    float deltaTime = 0f;

    float x,y;
    float value = startValue;

    while (deltaTime <= duration)
    {
        // タスクを終了する
        if (token.IsCancellationRequested)
        {
            return;
        }

        // 1フレームの間の秒数を足す
        deltaTime += Time.deltaTime;

        // Easing
        x = deltaTime / duration;
        y = ConvertEasingValue(x, easingType);

        // valueを求める
        value = Mathf.Lerp(startValue, endValue, y);
        
        // valueの更新を伝える
        action.Invoke(value);

        // 1フレーム待つ
        await Task.Yield();
    }
}

使用側についてですが、
UnityにはdestroyCancellationTokenという、Monobehaviourが破棄された時のためのCancellationTokenが用意されているので、これをTweenFloatを実行するときに渡します。

Tween実行側にtokenを渡す処理を追加
// Tweenを開始する
MyTween.TweenFloat(-3, 3, duration, MyTween.EasingType.Linear, (x) =>
    {
        transform.position = new Vector3(x, transform.position.y, transform.position.z);
    }, 
    destroyCancellationToken).Forget();

ちょっと変わったEasing関数

自作Tweenならではの変わったEasingを紹介します。

Random

開始値から終了値までのランダムな値をとるTween

使用例

ルーレット
void Start()
{
    // Tweenを開始する
    Roulette(destroyCancellationToken).Forget();
}

async Task Roulette( CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        await MyTween.TweenFloat(0, 100, 1f, MyTween.EasingType.Random, (x) =>
            {
                text.text = ((int)x).ToString();
            }, 
            token);

        await Task.Delay(500, token);
    }
}

動かすとこんな感じ

Tween random.gif

Beat

脈打つようなTween

グラフ
image.png

使用例

使用例:Beat
void Start()
{
    // Tweenを開始する
    Beat(destroyCancellationToken).Forget();
}

async Task Beat(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        await MyTween.TweenFloat(6, 8f, 0.2f, MyTween.EasingType.Beat, (x) =>
            {
                transform.localScale = new Vector3(x, x, transform.position.z);
            }, 
            token);

        await Task.Delay(800, token);
    }
}

動かすとこんな感じ

Tween beat.gif

完成品

今までの機能を盛り込んだMyTweenクラス

MyTween.cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

public static class MyTween
{
    public static async Task TweenFloat(float startValue, float endValue, float duration, EasingType easingType , UnityAction<float> action, CancellationToken token)
    {
        float deltaTime = 0f;

        float x,y;
        float value = startValue;

        while (deltaTime <= duration)
        {
            // タスクを終了する
            if (token.IsCancellationRequested)
            {
                return;
            }

            // 1フレームの間の秒数を足す
            deltaTime += Time.deltaTime;

            // Easing
            x = deltaTime / duration;
            y = ConvertEasingValue(x, easingType);

            // valueを求める
            value = Mathf.Lerp(startValue, endValue, y);
            
            // valueの更新を伝える
            action.Invoke(value);

            // 1フレーム待つ
            await Task.Yield();
        }
    }

    // Task を await しなかったときでも警告が出ないようにするための拡張メソッド
    public static void Forget(this Task task) { }


    private static float ConvertEasingValue(float x, EasingType easingType)
    {
        switch (easingType)
        {
            case EasingType.Linear:
                return x;
            case EasingType.QuadOut:
                return -(x - 1) * (x - 1) + 1;
            case EasingType.SinIn:
                return Mathf.Sin(Mathf.PI * 0.5f * (x - 1)) + 1;
            case EasingType.Beat:
                return Mathf.Pow(Mathf.Sin(Mathf.PI * x), 3);
            case EasingType.Random:
                return Random.Range(0f, 1f);
            default:
                return x;
        }
    }


    public enum EasingType
    {
        Linear,
        QuadOut,
        SinIn,
        Beat,
        Random,
    }
}

最後に

本記事ではLerpTaskを使用して簡単なTweenを作成しました。
パフォーマンスの観点ではTaskを使用している点がダメそうですが、1時間足らずで実装できるため学習用に実装してみてはいかがでしょうか。

  1. https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676

  2. https://github.com/AnnulusGames/MagicTween/blob/main/README_JA.md

  3. https://www.geogebra.org/graphing 2

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?