20210228のUnityに関する記事は4件です。

DOTween完全に理解するその7 オプション編

前回:DOTween完全に理解するその6 イベント編

今回解説するもの

今回は各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);  //継続して同じ動きを繰り返す

Loops1.gif

とてもお世話になるオプションその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); //幅と周期の細かさ

Ease.gif

とてもお世話になるオプションその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.

SetLink0.gif

設定一覧

LinkBehaviour 説明
PauseOnDisable 非アクティブ時にPause
PauseOnDisablePlayOnEnable 非アクティブ時にPause
アクティブ時にPlay
PauseOnDisableRestartOnEnable 非アクティブ時にPause
アクティブ時にRestart
PlayOnEnable アクティブ時Play
RestartOnEnable アクティブ時にRestart
KillOnDisable 非アクティブ時にKill
KillOnDestroy Destroy時にKill(デフォルト)
CompleteOnDisable 非アクティブ時にComplete
CompleteAndKillOnDisable 非アクティブ時にCompleteしてKill
RewindOnDisable 非アクティブ時にRewind
RewindAndKillOnDisable 非アクティブ時にRewindしてKill

OnEnable系は指定したGameObjectがすでにアクティブな場合、再生されてしまうので注意してください。
また、SetLink()を行なった場合どのLinkBehaviourを指定してもKillOnDestroyと同じ動作が含まれています。

動作はこんな感じです。
1秒ごとにアクティブと非アクティブを切り替えています。

SetLink1.gif
「PlayOnEnable」は非アクティブになっても関係なく動作します(ワープしてるみたいになってる)
「RestartOnEnable」はアクティブになるたびに最初から(結構便利)
「PauseOnDisable」は非アクティブ時にPauseされ、アクティブ時は何もしないので停止したまま
「PauseOnDisablePlayOnEnable」はアクティブになるとアニメーションを再開
「PauseOnDisableRestartOnEnable」は見た目はRestartOnEnableと同じですが厳密にはアニメーションが停止しているためちょっと違います

以下はDisable系の比較です。
1秒ごとに非アクティブし、1秒後にDOTween.PlayAll()しています
SetLink2.gif
「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を参考にしてください。
From.gif

SetSpeedBase

tween.DOMoveX(8, duration);                 //x=8まで3秒で移動
tween.DOMoveX(8, duration).SetSpeedBased(); //x=8まで秒速3で移動

Tweenを速度基準のアニメーションに変更します。
1秒間にdurationずつ値が変化していきます。
SetEaseをしている場合、その影響を受けるので注意してください(等速にしたい場合はEase.Linerを指定)
SpeedBase1.gif

その他のオプション

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は反映されないので注意してください。
以下はその例です。
SetAs.gif
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
lockRotation
DOTween完全に理解するその4 DOPath編
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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を使う

UniTaskasync/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.YieldUniTask.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.YieldUniTask.NextFrameの引数でタイミングを指定できます。

UniTask.YieldUniTask.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()は実際のコードで使われてるのを見かけたことないです)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでPytorchライクの機械学習ライブラリを作る。8日目:ロス関数とTensor操作

はじめに

 今回はロス関数とTensorに対する操作(detach, reshapeなど)を実装したいと思います。

その前に

 実装でロス関数をBackward処理を行う際に

Tensor loss = LossFunction(y, target)
loss.Backward();

 と呼び出したいのですが、そのままだとBackward処理が連鎖していかない(起点となる処理が必要)ので、Tensorクラスで現在Backward関数としている関数をBackwardChainに変更し新たにBackwardを追加します。

Tensor.cs
namespace 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)^2

MSELossの実装

F.cs内部に直接LambdaFunctionとして実装していきます。

F.cs
namespace 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.cs
namespace 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.cs
namespace 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.cs
namespace 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.cs
namespace 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の実装

Unsqueeze
namespace 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.cs
namespace 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.cs
namespace 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の実装を行います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】WaitUntilが正常に止まらないときに確認すること

はじめに

ゲーム内で準備が整うまで処理を停止するときに使う WaitUntil が正常に停止してくれない!!! :cry: となったので、備忘録を兼ねて書き残しておきます。

環境

  • Windows 10
  • Unity 2019.4.5f1(64-bit)

WaitUntil とは

WaitUntil は、yield return new WaitUntil(() => bool値); のように書き、bool 値が true になるまで処理を中断します。

以下にサンプルとして、ゲームのスタート画面の例を示します。

WaitUntilSample.cs
using 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.cs
using 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)");
    }
}

mv4x0-ctrrs.gif

スペースキーを 1 回押すと 2 つの出力を受けていることが分かります。

少々話はそれますが、さらに注意深く観察すると、同時にコンソールに出ているのにも関わらず時間が異なっていることも分かります。

Debug.Log の処理はほぼ 0 秒で行われるため、それ以外の処理に時間がかかっているということです。

実は、WaitUntil の判定には最低 1 フレームかかるという仕様があり、詳しくは先人の「UnityのWaitUntilは使わない」の記述に譲りますが、この仕様が影響していると考えられます。

解決法

結論から言うと、yield return nullyield return new WaitUntil(() => Input.GetKeyDown("space")) の後ろに書くと、予想通りに動きます。

WaitUntilTest.cs
using 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)");
    }
}

qiita2.gif

やりました!成功です!

バグの原因究明

Input.GetKeyDown は該当のキーを押すと、1 フレームの間ずっと true となります。

ふつう、コードは 1 フレームの中で実行されるので、WaitUntil の条件を満たすと、即座(フレームを跨がず)に次の WaitUntil まで到達します。

ここで、1 フレームの間 Input.GetKeyDown の値は常に true なので、2 つ目の WaitUntil に到達した時点でも true を返します。

その結果、2 つ目の WaitUntil はスペースキーを押さなくても突破されてしまうというわけでした。

ちなみに、WaitUntil は一度止まってから動くまで最低でも 1 フレームかかるので、3 つ目が一瞬で突破されることはありません。

qiita (3).png

したがって、処理を 1 フレーム分だけ中断させることができるyield return nullWaitUntil の直後に書くことで、このバグをスマートに解決することができたというわけです。

終わりに

バグと向き合ったときに既存の記事が無かったようなので自分で記事にしました。(もしあったらごめんなさい)

テンポを重視してところどころ説明を端折ってしまったため、内容は完全な初心者向けではなくなってしまったかもしれませんが、この記事で WaitUntil への理解が深まれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む