はじめに
これはLerp関数を最近知った筆者がTweenを自作してみた話
Tweenとは?
始点、終点、間の挙動を指定することで、間を補間してアニメーションさせることのできるライブラリです。
Unityだと DOTween1 や、 MagicTween2 などが有名です。
Tweenを使用すると、アニメーションを簡潔に実装できます。
次のコードはDOTweenを使用してx軸正方向に1だけ2秒かけて移動する処理を実装したものです。
transform.DOMoveX(transform.position.x + 1, 2f);
Tweenの自作
実際に作っていきます。
Lerp関数を使ってUpdate関数内に実装する
Lerp関数とは?
この自作Tweenの一番の要はLerp関数です。
Lerp(a, b, t)
とは、始点a
, 終点b
としたときの割合t
だけ進んだ値を返す関数で、グラフでは下のようになります。
例えば
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を実装する
前節の計算を deltaTime
が duration
を超えるまで繰り返せば良いため、
Update関数内に次のコードを書くことで、始点から終点までを指定時間かけて等速で移動します。
下のコードでは、 Vector3
に用意された Lerp を使用しています。
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;
}
}
実際に上のスクリプトをアタッチして動かしてみるとこんな感じ
Taskを使用してTweenを実装する
前章では、Lerpを使用してUpdate関数内にTweeenを実装してみました。
今章ではTweenを使いまわせるようにTask
を使って関数にします。
Task内で経過時間を取得するには?
Time.deltaTiime
で取得できる経過時間は前回のフレームからの時間です。
Update関数はフレーム毎に呼ばれていたため、
deltaTime += Time.deltaTime;
と書くだけで経過時間を計算できました。
それに対してTask
ではループの中で 1フレーム待つ必要があるため、 await Task.Yield();
を使用します。
次のコードはTask
を使用して実装したTweenと、その使用例です。
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();
}
}
}
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実行時の警告を消す
現時点だとTask
をawait
していないため警告がでます。
これはTweenPosition
の戻り値がTask
であるためなので、
戻り値の型をvoid
にしてやると警告は出なくなりますが、
そのまま変更すると今度はTweenPosition
をawait
できなくなります。
そこで、何もしない拡張メソッド Forget()
を追加することで警告を出さないようにしましょう。
以下はForget
の実装と、使用例です。
// 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させています。
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();
}
}
void Start()
{
// Tweenを開始する
MyTween.TweenFloat( -3, 3, duration, (x) =>
{
transform.position = new Vector3(x, transform.position.y, transform.position.z);
}).Forget();
}
先ほどの例に挙げていた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();
}
動かしてみるとこのようになります
イージング
次にTweenFloat
にイージングを付けてみましょう。
イージングとは?
イージングとは、Tweenに緩急をつけることで動きに面白さを加えられる機能です。
例えば、プレイヤーが移動を
- 最初はゆっくり加速
- 真ん中で最速になる
- 最後は減速して止まる
のように実装するとリアルな動きになります。
イージングの実装と関数
急ですがここで下図のような関数を考えてみます3
この関数に対してdeltaTime/duration
を入力として入れると、
Tweenが始まってすぐ(deltaTime/duration ≒ 0) ではy
の値が急激に増え、
Tweenが終わる頃(deltaTime/duration ≒ 1) になるとy
の値がゆっくり増えます。
この関数を使った値をLerp関数に入れることではじめは速く、終わりはゆっくりなEasingを実現できます。
実際に何種類かのEasingを実装したものが次のコードです。
ちなみにEasing用の関数はGeogebra3 等を使用すると作りやすいです。
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
CancellationToken
オブジェクトが破壊された、途中でシーンが変わった、途中でゲームが終了した
といったときにTask
を停止するためにCancellationToken
を持たせましょう。
Task.Yield();
にはCancellationToken
を渡すことができないので、キャンセルを判定してreturn
します。
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を開始する
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);
}
}
動かすとこんな感じ
Beat
脈打つようなTween
使用例
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);
}
}
動かすとこんな感じ
完成品
今までの機能を盛り込んだMyTween
クラス
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,
}
}
最後に
本記事ではLerp
やTask
を使用して簡単なTweenを作成しました。
パフォーマンスの観点ではTask
を使用している点がダメそうですが、1時間足らずで実装できるため学習用に実装してみてはいかがでしょうか。