- 投稿日:2021-02-28T21:01:01+09:00
DOTween完全に理解するその7 オプション編
今回解説するもの
今回は各Tweenに設定できる「オプション」について解説していきたいと思います。
いくつかのオプションはかなりよく使うのでご存知の方も多いと思います。
一部使い所は限定的ですがかゆい所に手が届くものもあるので
覚えておくと意外なところで役に立つかもしれません。開発環境
Unity:2019.4.0f1
DOTween:v1.2.335今回解説するオプション
API 概要 SetAutoKill アニメーション完了時にKillするか SetRecyclable KillしたTweenを再利用できるようにするか SetUpdate Tweenの更新スケジュールをどのタイミングにするか SetRelative アニメーションを相対的にするか SetLoops アニメーションがどのようにループするか SetEase アニメーションのイージングを設定 SetLink TweenをGameObjectの状態に紐付ける SetId Tweenを管理するIdを設定 SetTarget Tweenの対象となるターゲットの設定 SetDelay Tweenの再生を遅延させるか設定 From Tweenを設定値から現在の値へのアニメーションに変更する SetSpeedBase Tweenを速度ベースのアニメーションに変更する SetAs Tweenの設定値を上書きする SetOptions ジェネリックの各型専用オプション システム的な動作に関わるオプション
Tweenのシステム的な動作を変更するオプションです。
最初に紹介する割に、あまり使う機会はありません。
ただ「Tweenはこういうルールで動いているんだな」と少し理解できるので最初に紹介します。SetAutoKill
tween.SetAutoKill(false); //アニメーション完了後に自動でKillしないTweenがアニメーションを完了した時に、自動でKillされるかを変更できます。
デフォルトの場合に自動でKillされるかどうかはDOTweenの設定によりますが
それを例外的に変更したい場合などに使用します。個人的にはデフォルトで自動Killを有効にしておいて、
一部のアニメーションだけ使い回すことがわかっている場合SetKill(false)
にしてます。SetRecyclable
tween.SetRecyclable(false); //TweenがKillされた後、このTweenを使いまわさないKillされたTweenのその後の扱いを変更します。
基本的にDOTweenは内部的に特定の値を初期値から目標値に向かって変更し続けるというオブジェクトで動作しています。
そのため、目的が違っても使い方が同じ(型が同じ)な場合オブジェクトを再利用することで
オブジェクトを生成するコストを抑えています。
こちらもデフォルト設定から変更したい場合に使いますが
基本的に再利用が可能な状態で困ることはないので、使ったことはありません...SetUpdate
tween.SetUpdate(UpdateType.Late); //LateUpdateのタイミングで動作する tween.SetUpdate(UpdateType.Fixed, true);//FixedUpdateのタイミングで動作する(※要注意)Tweenの更新タイミングを変更します。
これもあまり使い所はありませんが、DOTween.To
でTweenを作成した場合に
「どうしてもLateUpdateのタイミングの値で処理したい」というシチュエーションがたまにあるので
そういったタイミングに役立ちます。第2引数にtrueを指定した場合、TimeScaleを無視して動作します(デフォルトはfalse)
ただUpdateTypeFixedに設定した場合TimeScaleが0だとFixUpdate自体が動作しないので注意が必要です。アニメーションに関わるオプション
特にお世話になるオプションです。
SetRelative
transform.DOMoveX(9, 1); //1秒でx=9まで移動 transform.DOMoveX(9, 1).SetRelative(true); //1秒で現在位置からx方向に9移動アニメーションを「相対的」なものに変更します。
アニメーションさせたいオブジェクトの位置が変わっても、その位置から同じようにアニメーションさせたい場合に使えます。
後述の再生後に設定しても反映されないオプションの一つです。動作に関してはDOTween完全に理解するその2 Transformアニメーション編参照してください。
SetLoops
tweenA.SetLoops(3, LoopType.Restart); //はじめの状態に戻ってループ tweenB.SetLoops(3, LoopType.Yoyo); //逆再生を行うループ tweenC.SetLoops(3, LoopType.Incremental); //継続して同じ動きを繰り返すとてもお世話になるオプションその1です。
アニメーションのループの仕方を設定します。
第1引数はループ回数、第2引数はループの種類
動作は画像の通りですがIncrementalがちょっと癖のある感じです。SetRelative同様に再生後に設定しても反映されないオプションの一つです。
SetEase
tween.SetEase(Ease.OutQuad); //Settingsを変更していない場合のデフォルト tween.SetEase(Ease.OutBounce); //使っておけばいい感じになるEase代表 tween.SetEase(Ease.OutBounce, 3);//Bounceの幅を変更する(デフォルトは1.7ぐらい) tween.SetEase(Ease.OutElastic); //揺らしたい場合はこっち tween.SetEase(Ease.OutElastic, 3, 0.0001f); //幅と周期の細かさとてもお世話になるオプションその2
アニメーションにいい感じの緩急をつけてくれます。
DOTween側で用意されているEaseだけでほぼ事足ります。
ただどうしても自作したい場合はtween.SetEase(AnimationCurve); //AnimationCurveを指定する tween.SetEase(EaseFunction); //EaseFunctionを指定するという方法もあります。
AnimationCurveであればSerializeFieldを使用することで、思った通りの動きをさせることができると思います。注意点としてBack系やElastic系など目標値を超えるEaseはDOPathではうまく動作しません
各Easeがどのよう動きをしているかは、下記サイトが世界一分かりやすいのでおすすめです。
DOTweenのイージング一覧を世界一詳しく&分かりやすく説明するアニメーションと何かと紐付けるオプション
アニメーションと何かを関連づけて、特定のアニメーションだけ再生させたり
アニメーション対象の状態によって再生状態を変更したりできます。SetLink
tween.SetLink(targetA.gameObject); //デフォルト、DestroyとともにKillされる(KillOnDestroy) tween.SetLink(targetB.gameObject, LinkBehaviour.RestartOnEnable); //Enable時にRestart tween.SetLink(targetC.gameObject, LinkBehaviour.PauseOnDisable); //Disable時にPause使用率No.1オプションだと思います。
特に消滅するGameObjectをアニメーションさせる場合は必須と言っても過言ではないでしょう。
このAPIを使用しなかった場合、削除されたオブジェクトへアニメーションを行おうとしてエラーになってしまいます。DOTweenをSafeModeで実行している場合、以下のようなエラーが出る場合は
SetLink()
しておく方がいいです。
SafeModeが気を使って教えてくれているだけで、特に問題ない場合もあります。DOTWEEN ► Target or field is missing/null () ► The object of type 'Transform' has been destroyed but you are still trying to access it. Your script should either check if it is null or you should not destroy the object.設定一覧
LinkBehaviour 説明 PauseOnDisable 非アクティブ時にPause PauseOnDisablePlayOnEnable 非アクティブ時にPause
アクティブ時にPlayPauseOnDisableRestartOnEnable 非アクティブ時にPause
アクティブ時にRestartPlayOnEnable アクティブ時Play RestartOnEnable アクティブ時にRestart KillOnDisable 非アクティブ時にKill KillOnDestroy Destroy時にKill(デフォルト) CompleteOnDisable 非アクティブ時にComplete CompleteAndKillOnDisable 非アクティブ時にCompleteしてKill RewindOnDisable 非アクティブ時にRewind RewindAndKillOnDisable 非アクティブ時にRewindしてKill OnEnable系は指定したGameObjectがすでにアクティブな場合、再生されてしまうので注意してください。
また、SetLink()
を行なった場合どのLinkBehaviourを指定してもKillOnDestroyと同じ動作が含まれています。動作はこんな感じです。
1秒ごとにアクティブと非アクティブを切り替えています。
「PlayOnEnable」は非アクティブになっても関係なく動作します(ワープしてるみたいになってる)
「RestartOnEnable」はアクティブになるたびに最初から(結構便利)
「PauseOnDisable」は非アクティブ時にPauseされ、アクティブ時は何もしないので停止したまま
「PauseOnDisablePlayOnEnable」はアクティブになるとアニメーションを再開
「PauseOnDisableRestartOnEnable」は見た目はRestartOnEnableと同じですが厳密にはアニメーションが停止しているためちょっと違います以下はDisable系の比較です。
1秒ごとに非アクティブし、1秒後にDOTween.PlayAll()
しています
「KillOnDisable」は非アクティブ時にKillされるためアクティブになっても再生されず
「CompleteOnDisable」はCompleteされ、アクティブになっても再生されません
「RewindOnDisable」はRewindされているので最初の状態に戻っています
「CompleteAndKillOnDisable」はComplete状態になった上でKillされるためPlayBackwardsなどもできません
「RewindAndKillOnDisable」も同様にRewind状態になった上でKillされますSetId
tween.SetId(1); //数値を指定する tween.SetId("hoge"); //文字列を指定する tween.setId(obj); //objectを指定する DOTween.Play("hoge"); //hogeだけ再生TweenにIdを割り振ってグループ化することができます。
例えばDOTween.Play()
などに引数で指定すると、対象のTweenのみ再生されます。
動作としてはint > sting > object
の順に速いです。SetTarget
tweenA.SetTarget(targetA); tweenA.SetId(1); tweenB.SetTarget(targetA); tweenB.SetId(2); tweenC.SetTarget(targetB); DOTween.Play(targetA); //targetAのTweenのみ再生 DOTween.Play(targetA, 1); //targetAのId=1のTweenのみ再生こちらは
SetId()
に加えて、さらにターゲットという範囲で判別する場合に使用できます。
SetId()
だけで事足りる場合はSetId()
の方を使用するのが推奨されています。再生前に設定が必要なオプション
SetDelay
tween.SetDelay(1f); //1秒送らせて動作開始アニメーションの開始を指定しただけ遅らせるオプションです。
「開始」を遅らせるというのがミソで、基本的にループの場合2度目のループ開始時には遅延されません。
Restart()
やRewind()
を使用して初期状態に戻すことで再度遅延が有効になります。ちなみに
Sequence.SetDelay()
はSequence.SetPrependInterval
と同じです。SetDelay(delay, asPrependedIntervalIfSequence)
tween.SetDelay(1f, true); //1秒送らせて動作開始(ループしても有効)Sequenceのみに有効なオプションです。
asPrependedIntervalIfSequence=trueに指定した場合ループ毎にDelayが有効になりますFrom
tween.DOMoveX(8, duration); //x=8まで移動 tween.DOMoveX(8, duration).From(); //x=8「から」現在位置まで移動 tween.DOMoveX(8, duration).From(true); //x=8に「座標を変更して」現在位置まで移動 tween.DOMoveX(8, duration).From(0, false); //x=1からx=8へ移動 tween.DOMoveX(8, duration).From(0, true); //x=1に「座標を変更して」x=8へ移動 //相対位置指定版 tween.DOMoveX(8, duration).From(true); //x方向に8移動した位置「から」現在位置まで移動 tween.DOMoveX(8, duration).From(1, true, true); //x方向に1移動した位置に「座標を変更して」x=8へ移動「動作開始の値」を設定するオプションです。
全てのTweenで使用でき、各型を初期値として指定できます。
immediately=true
に設定することで「再生前に」指定した値がセットされます。文章では分かりにくいと思うので、以下のgifを参考にしてください。
SetSpeedBase
tween.DOMoveX(8, duration); //x=8まで3秒で移動 tween.DOMoveX(8, duration).SetSpeedBased(); //x=8まで秒速3で移動Tweenを速度基準のアニメーションに変更します。
1秒間にdurationずつ値が変化していきます。
SetEaseをしている場合、その影響を受けるので注意してください(等速にしたい場合はEase.Linerを指定)
その他のオプション
SetAs
tweenA.SetId(id); tweenA.SetDelay(delay); tweenA.SetLoops(3, LoopType.Yoyo); tweenA.SetEase(Ease.InOutBack); tweenB.SetAs(tweenA); //tweenAと同じ設定にする var tweenParam = new TweenParams(); tweenParam.SetId(id); tweenParam.SetDelay(delay); tweenParam.SetLoops(3, LoopType.Yoyo); tweenParam.SetEase(Ease.InOutBack); tweenC.SetAs(tweenParam); //tweenParamの設定を適用するTweenに設定された設定を別のTweenにも適用するオプションです。
共通設定となるTweenParamをあらかじめ作成しておいて、一斉に適用するなどもできます。
上記の場合はどのTweenも同じようにアニメーションします。
SetAs後に個別に設定したオプションは当然個別に適用されます。オプション以外に、イベントやtimeScaleなどの設定も適用されるので注意して下さい。
またTargetとLinkは反映されないので注意してください。
以下はその例です。
TargetAはSetLink()
されており、一定時間後にDestroyされます。
TargetBはSetAs(TargetA)
後に個別にSetRelative()
しています。
TargetCはSetAs(TargetA)
のみです(本来のTargetAと同じ動き)TargetAがDestroyされた後も他のTweenは問題なく動作し続けます。
SetOptions
Tweenの対象となっている型ごとに個別のオプションを指定できます。
以下は一例です
型 Tween 設定 効果 float DoFadeなど isSnapping 小数点以下を切り捨て Color DOColorなど alphaOnly アルファ値のみ影響を与える Vector系 DOMoveなど constraint
isSnapping動作する軸の指定
小数点以下切り捨てString DOTextなど richTextEnabled
scramble
scrambleCharsリッチテキストを使うか
ScrambleModeの指定
ランダムに表示されれる文字の指定(ScrambleMode.Custom時に使用)特殊 DOPath closePath
lockPosition
lockRotationDOTween完全に理解するその4 DOPath編
- 投稿日:2021-02-28T15:53:29+09:00
【Unity】一定時間後にスクリプトの処理を呼び出す方法まとめ
この記事について
古い記事のPVが未だにそれなりにあるので、2021年現在のやり方で書き直してみました。
やりたいこと
「スクリプトの実行タイミングを操作したい」です。
つまり一定時間後に指定した処理を実行するといった処理をどう書くか紹介します。標準機能のみで書く場合
外部ライブラリを用いず、UnityやC#の機能のみで記述する場合のやり方。
処理をN秒後に実行したい
Invokeを使う
MonoBehaviour.Invoke()
メソッドを使うことで指定した処理を一定秒数後に呼び出すことができます。
引数には対象のメソッド名を指定します(nameof
で指定すると便利)実行をキャンセルする場合は
CancelInvoke()
を使います。using UnityEngine; public class Sample : MonoBehaviour { private void Start() { //DelayMethodを3.5秒後に呼び出す Invoke(nameof(DelayMethod), 3.5f); } void DelayMethod() { Debug.Log("Delay call"); } private void OnDestroy() { // Destroy時に登録したInvokeをすべてキャンセル CancelInvoke(); } }コルーチン(おすすめ)
コルーチンを利用することでも一定時間後の処理を実行するという書き方ができます。
Invoke()
よりも細かい制御が効きます。using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // コルーチンの起動 StartCoroutine(DelayCoroutine()); } // コルーチン本体 private IEnumerator DelayCoroutine() { transform.position = Vector3.one; // 3秒間待つ yield return new WaitForSeconds(3); // 3秒後に原点にワープ transform.position = Vector3.zero; } }また、「デリゲート」という機能を組み合わせることで使い回しが効くようにできます。
using System; using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { transform.position = Vector3.one; // コルーチンの起動 StartCoroutine(DelayCoroutine(3, () => { // 3秒後にここの処理が実行される transform.position = Vector3.zero; })); } // 一定時間後に処理を呼び出すコルーチン private IEnumerator DelayCoroutine(float seconds, Action action) { yield return new WaitForSeconds(seconds); action?.Invoke(); } }async/await
C#の
async/await
という機能をつかってコルーチンに似た処理が書けます。
ただしそのままでは扱いにくいのでasync/await
を使う場合は後述するUniTask
とセットでつかったほうがオススメです。
(とくにキャンセル周りがすごい面倒くさい)using System; using System.Collections; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class Sample : MonoBehaviour { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private void Start() { var ct = _cancellationTokenSource.Token; // 非同期メソッド実行 _ = DelayAsync(ct); } // 非同期メソッド private async Task DelayAsync(CancellationToken token) { transform.position = Vector3.one; // 3秒間待つ await Task.Delay(TimeSpan.FromSeconds(3), token); // 3秒後に原点にワープ transform.position = Vector3.zero; } private void OnDestroy() { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); } }処理をNフレーム後に実行したい
コルーチン(おすすめ)
Nフレーム後に実行したいという場合はコルーチンを使うのがもっとも簡単です。
using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // コルーチンの起動 StartCoroutine(DelayCoroutine()); } // コルーチン本体 private IEnumerator DelayCoroutine() { transform.position = Vector3.one; // 10フレーム待つ for (var i = 0; i < 10; i++) { yield return null; } // 10フレーム後に原点にワープ transform.position = Vector3.zero; } }デリゲートを使うとこう書けます。
using System; using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { transform.position = Vector3.one; // コルーチンの起動 StartCoroutine(DelayCoroutine(10, () => { // 10F後にここの処理が実行される transform.position = Vector3.zero; })); } // 一定フレーム後に処理を呼び出すコルーチン private IEnumerator DelayCoroutine(int delayFrameCount, Action action) { for (var i = 0; i < delayFrameCount; i++) { yield return null; } action?.Invoke(); } }処理を一定間隔で定期的に実行したい
InvokeRepeating
MonoBehaviour.InvokeRepeating
を使うことで処理を一定間隔で実行することができます。using UnityEngine; public class Sample : MonoBehaviour { private void Start() { //DelayMethodを3.5秒後に呼び出し、以降は1秒毎に実行 InvokeRepeating(nameof(DelayMethod), 3.5f, 1.0f); } void DelayMethod() { Debug.Log("Delay call"); } private void OnDestroy() { // Destroy時に登録したInvokeをすべてキャンセル CancelInvoke(); } }コルーチン
コルーチンと
while
/for
を組み合わせるパターンです。using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // コルーチンの起動 StartCoroutine(DelayCoroutine()); } // コルーチン本体 private IEnumerator DelayCoroutine() { while (true) // このGameObjectが有効な間実行し続ける { yield return new WaitForSeconds(1); // 1秒毎に実行する Debug.Log("Do!"); } } }細かい時間の制御がしたい
Update()
とFixedUpdate()
を行ったり着たりしたいコルーチンを使うことで実行タイミングを
Update()
とFixedUpdate()
で相互に切り替えることができます。using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // コルーチンの起動 StartCoroutine(DelayCoroutine()); } // コルーチン本体 private IEnumerator DelayCoroutine() { // デフォルトは呼び出したタイミング(Start()はUpdate()と同等) Debug.Log("OnUpdate!"); // 次のFixedUpdateタイミングまでまつ yield return new WaitForFixedUpdate(); // ここの処理はFixedUpdateのタイミングと同等 Debug.Log("OnFixedUpdate!"); // 1フレーム待って次のUpdate()タイミングに移す yield return null; // ここはUpdateタイミング Debug.Log("OnUpdate!"); } }Time.timeScaleの影響を受けずに処理を一定時間後に呼び出したい
コルーチンと
WaitForSecondsRealtime
を組み合わせることで実現できます。using System.Collections; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // コルーチンの起動 StartCoroutine(DelayCoroutine()); } // コルーチン本体 private IEnumerator DelayCoroutine() { transform.position = Vector3.one; // 3秒間待つ // Time.timeScale の影響を受けずに実時間で3秒待つ yield return new WaitForSecondsRealtime(3); // 3秒後に原点にワープ transform.position = Vector3.zero; } }UniRxを使う
UniRx をつかったパターンです。
処理をN秒後に実行したい
Observable.Timerを使う
using System; using UniRx; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // 3秒後に呼び出す Observable.Timer(TimeSpan.FromSeconds(3)) .Subscribe(_ => DelayMethod()); // 3秒後に呼び出す // Time.timeScaleを無視して実時間で計測する Observable.Timer(TimeSpan.FromSeconds(3), Scheduler.MainThreadIgnoreTimeScale) .Subscribe(_ => DelayMethod()); } private void DelayMethod() { Debug.Log("Delay call"); } }処理をNフレーム後に実行したい
Observable.TimerFrameを使う
using UniRx; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // 2F後に呼び出す Observable.TimerFrame(2) .Subscribe(_ => DelayMethod()) .AddTo(this); // 2F後のFixedUpdateで実行する Observable.TimerFrame(2, FrameCountType.FixedUpdate) .Subscribe(_ => DelayMethod()) .AddTo(this); } private void DelayMethod() { Debug.Log("Delay call"); } }「次のフレーム」と指定したい場合は
NextFrame
でもOKです。using UniRx; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // 次のフレームで呼び出す Observable.NextFrame() .Subscribe(_ => DelayMethod()) .AddTo(this); // 次のFixedUpdateで実行する Observable.NextFrame( FrameCountType.FixedUpdate) .Subscribe(_ => DelayMethod()) .AddTo(this); } private void DelayMethod() { Debug.Log("Delay call"); } }処理を一定間隔で定期的に実行したい
Observable.Timer
/Observable.Interval
/Observable.TimerFrame
/Observable.IntervalFrame
でできます。using System; using UniRx; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { // 3秒後に実行したあと、以降1秒毎に実行 Observable.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(1)) .Subscribe(_ => DelayMethod()) .AddTo(this); // 常に3秒間隔で実行 Observable.Interval(TimeSpan.FromSeconds(3)) .Subscribe(_ => DelayMethod()) .AddTo(this); } private void DelayMethod() { Debug.Log("Delay call"); } }UniTaskを使う
UniTaskと
async/await
を使うパターン。
基本的にはコルーチンと同じです。処理をN秒後に実行したい
標準Taskを使う場合とあまり変わりません。
違いはCancellationToken
の生成が楽になったくらいです。using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { var ct = this.GetCancellationTokenOnDestroy(); // 非同期メソッド実行 DelayAsync(ct).Forget(); } // 非同期メソッド private async UniTask DelayAsync(CancellationToken token) { transform.position = Vector3.one; // 3秒間待つ await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: token); // 3秒後に原点にワープ transform.position = Vector3.zero; } }処理をNフレーム後に実行したい
UniTask.Yield
やUniTask.DelayFrame
でフレーム単位での待機ができます。using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { var ct = this.GetCancellationTokenOnDestroy(); // 非同期メソッド実行 DelayAsync(ct).Forget(); } // 非同期メソッド private async UniTask DelayAsync(CancellationToken token) { transform.position = Vector3.one; // 5F間待つ await UniTask.DelayFrame(5, cancellationToken: token); // 5F後に原点にワープ transform.position = Vector3.zero; // 1F待つ await UniTask.Yield(PlayerLoopTiming.Update, token); Destroy(gameObject); } }細かい時間の制御がしたい
UniTask.Yield
やUniTask.NextFrame
の引数でタイミングを指定できます。
UniTask.Yield
やUniTask.NextFrame
の違いは必ず1F待機するかどうかです。
(UniTask.Yield
の場合はタイミングの指定によっては1F待たずに実行される場合があります)using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { var ct = this.GetCancellationTokenOnDestroy(); // 非同期メソッド実行 DelayAsync(ct).Forget(); } // 非同期メソッド private async UniTask DelayAsync(CancellationToken token) { // Updateタイミングを待つ await UniTask.Yield(PlayerLoopTiming.Update, token); Debug.Log("OnUpdate!"); // 次のFixedUpdateを待つ await UniTask.WaitForFixedUpdate(token); Debug.Log("OnFixedUpdate!"); // 次のPostLateUpdateタイミングを待つ await UniTask.NextFrame(PlayerLoopTiming.PostLateUpdate, token); Debug.Log("OnPostLateUpdate!"); } }Time.timeScaleの影響を受けずに処理を一定時間後に呼び出したい
UniTask.Delay
の引数でDelayType.UnscaledDeltaTime
またはDelayType.Realtime
を指定してください。
(DelayType.UnscaledDeltaTime
はUnitTestでは正しく動作しませんが、DelayType.Realtime
はUnitTestでも利用できます)using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Sample : MonoBehaviour { private void Start() { var ct = this.GetCancellationTokenOnDestroy(); // 非同期メソッド実行 DelayAsync(ct).Forget(); } // 非同期メソッド private async UniTask DelayAsync(CancellationToken token) { // Time.timeScaleを無視して3秒後のUpdate()タイミングで実行 await UniTask.Delay( TimeSpan.FromSeconds(3), DelayType.UnscaledDeltaTime, PlayerLoopTiming.Update, token); Debug.Log("OnPostLateUpdate!"); } }まとめ
標準機能のみで扱えてバランスがよいのは「コルーチン」。
コルーチンよりも高機能だけどちょっと難しいのが「async/await
+ UniTask」。
今日においてはUniRxを率先して使う必要はないです。
(Invoke()
は実際のコードで使われてるのを見かけたことないです)
- 投稿日:2021-02-28T10:04:42+09:00
UnityでPytorchライクの機械学習ライブラリを作る。8日目:ロス関数とTensor操作
はじめに
今回はロス関数とTensorに対する操作(detach, reshapeなど)を実装したいと思います。
その前に
実装でロス関数をBackward処理を行う際に
Tensor loss = LossFunction(y, target) loss.Backward();と呼び出したいのですが、そのままだとBackward処理が連鎖していかない(起点となる処理が必要)ので、Tensorクラスで現在Backward関数としている関数をBackwardChainに変更し新たにBackwardを追加します。
Tensor.csnamespace Rein { [Serializable] public partial class Tensor { public void BackwardChain() { // BackFunctionが存在しない時は終了 if(this.BackFunction == null)return; this.UseCount--; // 他の関数に対しても出力している場合にはまだ勾配を計算しない if(this.UseCount != 0)return; this.BackFunction.Backward(); } public void Backward(){ // 一つの変数しか持たないことを確認する if (this.Size > 1)throw new InvalidSizeException($"expect size : 1, but actual : {this.Size}"); this.Grad[0] = 1.0; this.BackFunction.Backward(); } } }Loss関数の実装
それではいくつか主となるLoss関数を実装していきます。基本的にはLossの関数をLambdaで計算した後SumやMeanを計算することになります
MSELoss(二乗誤差)
これは以下のような関数です
Loss_{MSE}=\frac{1}{n}\sum_{i=1}^{n}(y_i-t_i)^2MSELossの実装
F.cs内部に直接LambdaFunctionとして実装していきます。
F.csnamespace Rein{ public static class F{ public static Tensor MSELoss(Tensor In){ return new Lambda( "MSELoss", (x) => x * x, (x) => 2 * x ).Forward(In)[0].Mean(); } } }HuberLossの実装
HuberLossは以下のような計算を行います。
f_{huber}(x) = \left\{ \begin{array}{ll} \frac{1}{2}x^2 & (-\delta \leq x \leq \delta) \\ \delta|x|-\frac{1}{2}\delta^2 & (x \lt -\delta \, or\, x \gt \delta) \end{array} \right.\\ L_{huber}=\frac{1}{n}\sum_{i=1}^{n}f_{huber}(y_i-t_i)実装
こちらも同様にF.csに加えていきます。
F.csnamespace Rein{ public static class F{ public static Tensor HuberLoss(Tensor left, Tensor right, R delta = 1.0){ R deltaSquare = delta * delta / 2; return new Lambda( "HuberLossFunction", new Func<R, R>((x) => x < -delta ? -delta * x - deltaSquare : (x > delta ? delta * x - deltaSquare : x * x / 2)), new Func<R, R>((x) => x < -delta ? -delta : (x > delta ? delta : x)) ).Forward(left - right); }public static Tensor HuberLoss(Tensor left, Tensor right, R delta = 1.0){ R deltaSquare = delta * delta / 2; return new Lambda( "HuberLossFunction", new Func<R, R>((x) => x < -delta ? -delta * x - deltaSquare : (x > delta ? delta * x - deltaSquare : x * x / 2)), new Func<R, R>((x) => x < -delta ? -delta : (x > delta ? delta : x)) ).Forward(left - right); } } }Tensorの操作
次はいくつかTensorの構造に作用する関数を実装していきたいと思います。Tensorの操作を行うメソッドでは基本的にShapeに作用するためDataの中身を変えないため、入力したTensorと同じインスタンスが出力されることとなります。
Detach
これはTensorの依存関係を切り離し、勾配の伝播を止める操作です。要は学習はさせないがネットワークの出力だけ欲しいという時に使う関数です。これをTensorの関数として実装したいのですが、一つ問題があります。
例えば以下のような形式で使用するとします。Tensor y = network(x).detach(); Tensor z = network(t); Tensor loss = (y - z) * (y - z); loss.Backward();ここでTensor yは独立したBackFuncを持たないTensorとなるのですが、network内部ではxが入力された時に計算グラフが作られ保存されているので、これらの関係を解消するためには一々yからグラフを遡る必要が出てきます。
そのため、残念ながらTensorの操作としてのDetach操作は断念せざるを得ません。
そこで、代わりにBaseFunctionに「勾配情報を保存しないForward」を定義します。これをPredictとします。実装(IFunction.csの追記)
まずIFunctionに対してPredictを追加します。
IFunction.csnamespace Rein.Functions { public interface IFunction { public Tensor[] Forward(params Tensor[] inputs); public Tensor[] Predict(params Tensor[] inputs); public void Backward(); public Tensor[] Parameters {get; } } }実装(BaseFuncttion.csの追記)
IFunctionに追加した関数の詳細をBaseFunctionで定義します。
BaseFunction.csnamespace Rein.Functions { public abstract class BaseFunction: IFunction { // ... public virtual Tensor[] Predict(params Tensor[] inputs){ return this.FunctionForward(inputs); } // ... } }これを使用することで、学習時に勾配を計算させないようにすることができます。PytorchのようにDetachをTensorの操作として呼び出したいなら、計算グラフの実装方法を変える必要があるようです。
Squeeze・Unsqueeze
SqueezeはTensorのある軸方向のサイズが1の時にその軸を消し次元を減らす操作で、
Unsqueezeは逆に次元を増やす操作です。これらも同様に関数クラスとして実装しTensorから呼び出せるようにしておきます。Squeezeの実装
Squeeze.csnamespace Rein.Functions.Process{ public class Squeeze: UnaryFunction{ private List<int> InShape; private int Dim; public Squeeze(int dim): base($"Squeeze-{dim}"){ this.Dim = dim; } protected override Tensor UnaryForward(Tensor tensor) { this.InShape = new List<int>(tensor.Shape); if(tensor.Shape[this.Dim] == 1)tensor.Shape.RemoveAt(this.Dim); return tensor; } protected override void UnaryBackward() { this.In.Shape = this.InShape; } } }Unsqueezeの実装
Unsqueezenamespace Rein.Functions.Process{ public class Unsqueeze: UnaryFunction{ private List<int> InShape; private int Dim; public Unsqueeze(int dim): base($"Unsqueeze-{dim}"){ this.Dim = dim; } protected override Tensor UnaryForward(Tensor tensor) { this.InShape = new List<int>(tensor.Shape); tensor.Shape.Insert(this.Dim, 1); return tensor; } protected override void UnaryBackward() { this.In.Shape = this.InShape; } } }Reshape
ReshapeでもSqueezeと同様にTensorのデータは変えずにShapeのみを入れ替えることになります。
実装
Reshape.csnamespace Rein.Functions.Process{ public class Reshape: UnaryFunction{ private List<int> OutShape; private List<int> InShape; public Reshape(List<int> shape): base($"Reshape-({string.Join(",", shape)})"){ this.OutShape = shape; } protected override Tensor UnaryForward(Tensor tensor) { // サイズ確認 if (this.OutShape.Aggregate((now, next) => now * next) != tensor.Size) throw new InvalidShapeException($"Expected Output Shape : ({string.Join(",", this.OutShape)}) ,Input Shape :({string.Join(",", tensor.Shape)})"); this.InShape = tensor.Shape; tensor.Shape = this.OutShape; return tensor; } protected override void UnaryBackward() { this.In.Shape = this.InShape; } } }Tensorクラスへの追加
ここまで実装したクラスのForwardをTensorから実行できるようにしておきます。
Tensor.Processing.csnamespace Rein { public partial class Tensor { public Tensor Detach(){ return new Detach().Forward(this); } public Tensor Squeeze(int dim){ return new Squeeze(dim).Forward(this); } public Tensor Unsqueeze(int dim = 0){ return new Unsqueeze(dim).Forward(this); } public Tensor Reshape(List<int> shape){ return new Reshape(shape).Forward(this); } } }これでTensor側でいつでも操作できるようになりました。
終わりに
今回は、ロス関数とTensorの操作関数を定義しました。ロス関数は他にもクロスエントロピーとかがよく使うと思いますが、現時点では使わなさそうなので必要になったら実装しようと思います。
次はOptimizerの実装を行います。
- 投稿日:2021-02-28T02:07:07+09:00
【Unity】WaitUntilが正常に止まらないときに確認すること
はじめに
ゲーム内で準備が整うまで処理を停止するときに使う WaitUntil が正常に停止してくれない!!! となったので、備忘録を兼ねて書き残しておきます。
環境
- Windows 10
- Unity 2019.4.5f1(64-bit)
WaitUntil とは
WaitUntil は、
yield return new WaitUntil(() => bool値);
のように書き、bool 値が true になるまで処理を中断します。以下にサンプルとして、ゲームのスタート画面の例を示します。
WaitUntilSample.csusing System.Collections; using UnityEngine; public class WaitUntilSample : MonoBehaviour { void Start() { StartCoroutine("Sample"); } IEnumerator Sample () { Debug.Log("Waiting..."); yield return new WaitUntil(() => Input.GetKeyDown("space")); Debug.Log("Let's play!!!"); // 以下ゲーム開始の処理 } }ユーザーがスペースキーを押すまで、
yield return new WaitUntil(() => Input.GetKeyDown("space"));
の部分で処理が止まります。ゲーム開始の処理はユーザーがスペースキーを押すまで行われず、ユーザーを待つことが出来ます。
このほかにも、ノベルゲームでのセリフ送りやロード画面など、ユーザーの入力を待機させたいときや準備が整うまで処理を停止する場合は
WaitUntil
を使うと比較的綺麗に実装することができます。詰まったところ
以下のコードを書いたところ、コンソールにバグは出ないものの、偶数番目の
WaitUntil
は正常に作動しませんでした。WaitUntilTest.csusing System.Collections; using UnityEngine; public class WaitUntilTest : MonoBehaviour { void Start() { StartCoroutine("Sample"); } IEnumerator Sample () { Log("Waiting..."); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("first"); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("second"); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("third"); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("fourth"); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("fifth"); yield return new WaitUntil(() => Input.GetKeyDown("space")); Log("sixth"); } void Log(string message) { Debug.Log(message + " (" + Time.time.ToString("n4") + " s)"); } }スペースキーを 1 回押すと 2 つの出力を受けていることが分かります。
少々話はそれますが、さらに注意深く観察すると、同時にコンソールに出ているのにも関わらず時間が異なっていることも分かります。
Debug.Log
の処理はほぼ 0 秒で行われるため、それ以外の処理に時間がかかっているということです。実は、
WaitUntil
の判定には最低 1 フレームかかるという仕様があり、詳しくは先人の「UnityのWaitUntilは使わない」の記述に譲りますが、この仕様が影響していると考えられます。解決法
結論から言うと、
yield return null
をyield return new WaitUntil(() => Input.GetKeyDown("space"))
の後ろに書くと、予想通りに動きます。WaitUntilTest.csusing System.Collections; using UnityEngine; public class WaitUntilTest : MonoBehaviour { void Start() { StartCoroutine("Sample"); } IEnumerator Sample () { Log("Waiting..."); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("first"); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("second"); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("third"); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("fourth"); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("fifth"); yield return new WaitUntil(() => Input.GetKeyDown("space")); yield return null; Log("sixth"); } void Log(string message) { Debug.Log(message + " (" + Time.time.ToString("n4") + " s)"); } }やりました!成功です!
バグの原因究明
Input.GetKeyDown
は該当のキーを押すと、1 フレームの間ずっと true となります。ふつう、コードは 1 フレームの中で実行されるので、
WaitUntil
の条件を満たすと、即座(フレームを跨がず)に次のWaitUntil
まで到達します。ここで、1 フレームの間
Input.GetKeyDown
の値は常に true なので、2 つ目のWaitUntil
に到達した時点でも true を返します。その結果、2 つ目の
WaitUntil
はスペースキーを押さなくても突破されてしまうというわけでした。ちなみに、
WaitUntil
は一度止まってから動くまで最低でも 1 フレームかかるので、3 つ目が一瞬で突破されることはありません。したがって、処理を 1 フレーム分だけ中断させることができる
yield return null
をWaitUntil
の直後に書くことで、このバグをスマートに解決することができたというわけです。終わりに
バグと向き合ったときに既存の記事が無かったようなので自分で記事にしました。(もしあったらごめんなさい)
テンポを重視してところどころ説明を端折ってしまったため、内容は完全な初心者向けではなくなってしまったかもしれませんが、この記事で
WaitUntil
への理解が深まれば幸いです。