知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【Unity】UniTask【3】 #92

前回の成果

UniTaskのAwaiterについて学んだ。

soramamenatan.hatenablog.com


今回やること

前回に引き続き、UniTaskについて学んでいきます。


キャンセル

非同期処理ではキャンセルについて考えなければなりません。
UniTaskも同様に、何らかの原因で例外が発生したときにキャンセルにより処理を停止させる必要があります。

CancellationToken

こちらは最もシンプルなキャンセルの方法となります。

キャンセル

処理の流れはコメントに記載の通りです。
tokenを生成し、キャンセルにします。
キャンセルの判定を行い、キャンセルならログと例外を投げます。

/// <summary>
/// CancellationTokenの生成
/// </summary>
private void CreateCancellationToken() {
    // tokenの生成
    CancellationTokenSource source = new CancellationTokenSource();
    CancellationToken token = source.Token;
    // キャンセルにさせる
    source.Cancel();

    // キャンセル状態の時
    if (token.IsCancellationRequested) {
        Debug.Log("Cancel");
    }
    // キャンセル状態なら、OperationCanceledExceptionを投げる
    token.ThrowIfCancellationRequested();
}
結果

ログとOperationCanceledExceptionが投げられています。

f:id:soramamenatan:20210213111722p:plain

試しに、以下をコメントアウトすると何もログが出ません。

tokenのキャンセル処理をコメントアウト
/// <summary>
/// CancellationTokenの生成
/// </summary>
private void CreateCancellationToken() {
    // tokenの生成
    CancellationTokenSource source = new CancellationTokenSource();
    CancellationToken token = source.Token;
    // コメントアウト
    // source.Cancel();

    // キャンセル状態の時
    if (token.IsCancellationRequested) {
        Debug.Log("Cancel");
    }
    // キャンセル状態なら、OperationCanceledExceptionを投げる
    token.ThrowIfCancellationRequested();
}
結果

tokenがキャンセルされていないので、ログも例外も投げられません。

f:id:soramamenatan:20210213111916p:plain


UniTaskの大体のメソッドは引数でCancellationTokenを指定できます。
少し手間ですが指定するようにしてください。
ボタン押下でUniTaskをキャンセルさせるコードを記載したので参考にしてみてください。

Button押下でUniTaskをキャンセル
[SerializeField]
private Button _button;

/// <summary>
/// ループを止めるボタン
/// </summary>
private void LoopCancelButton() {
    CancellationTokenSource source = new CancellationTokenSource();
    CancellationToken token = source.Token;
    // ボタンが押された初回のみOnNext
    _button
        .OnClickAsObservable()
        .First()
        .Subscribe(x => {
            // 押されたらUniTaskをキャンセル
            Debug.Log("ボタンが押されました");
            source.Cancel();
            token.ThrowIfCancellationRequested();
    });
    LoopDelay(token).Forget();
}

/// <summary>
/// Delayをループする
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
private async UniTask LoopDelay(CancellationToken token) {
    int count = 0;
    while (true) {
        count++;
        // 引数でCancellationTokenを指定
        await UniTask.Delay(1000, cancellationToken:token);
        Debug.Log(count + "回目のDelay");
    }
}
結果

ボタンが押されたらDelayをキャンセルし、例外を投げています。
また、その後Delayが走りません。

f:id:soramamenatan:20210214091528p:plain

GetCancellationTokenOnDestroy()

こちらはGameObjectが破棄されたときにキャンセルされるCancellationTokenを発行してくれるメソッドになります。
MonoBehaviourを継承しているクラスであれば使用することができます。

// GameObjectが破棄されるとキャンセル状態にしてくれるtoken
CancellationToken token = this.GetCancellationTokenOnDestroy();


OperationCanceledException

これを投げられたUniTaskはキャンセル状態になります。
こちらは外からキャンセル要求が来た時にthrowするものですので、それ以外の用途で使用しないようにしてください。
また、この例外はエラーログには出ません。

使用しては駄目な例
/// <summary>
/// 行っては駄目なメソッド
/// </summary>
/// <param name="pathName"></param>
/// <param name="token"></param>
/// <returns></returns>
private async UniTask DontUseMethod(string path, CancellationToken token) {
    await UniTask.Run(() => {
        Texture2D texture = FindTexture(path);
        // テクスチャが見つからなかったら例外を投げる
        if (texture == null) {
            // 処理に失敗したときにログを出したくないから使うのような使用用途はNG
            throw new OperationCanceledException();
        }
    });
}


まとめ

TaskよりもUniTask

UniTaskはTaskをUnityに向けて改良したものとなります。
ですので、よっぽどの理由がない限りはUniTaskに置き換えましょう。


コルーチンよりもUniTask

戻り値が扱える、UniTask Trackerで動作中の関数を見れる、別スレッドに回せるといったメリットがあるので、基本的にはUniTaskを使用するようにしましょう。
ただし、キャンセルが辛い時はコルーチンにしましょう。


UniRxとUniTask

イベント処理や、結果が複数になる非同期処理の場合はUniRxを使用します。
結果が1つの処理の時はUniTaskを使用します。


キャンセルは忘れないようにする

手間ですが、非同期処理を扱う上では必須なので忘れないようにしましょう。

今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

softmedia.sakura.ne.jp

www.slideshare.net

speakerdeck.com


備忘録

各種using

UniTaskのversionが2以降になってusingが変わったものがある。
また、どれがどのusingかは忘れやすいので残しておく

using一覧
// UniTaskを使用
using Cysharp.Threading.Tasks;

// AwakeAsync, StartAsync, OnDestroyAsyncを使用
using Cysharp.Threading.Tasks.Triggers;

// OperationCanceledExceptionを使用
using System;

// CancellationTokenを使用
using System.Threading;


UniTask.Run

UniTask.RunはCancellationTokenを指定できないとあるが、UniTaskのversionが2以降だと指定できる。

UniTask.Runの定義
public static async UniTask Run(Action action, bool configureAwait = true, CancellationToken cancellationToken = default)
{
    cancellationToken.ThrowIfCancellationRequested();

    await UniTask.SwitchToThreadPool();

    cancellationToken.ThrowIfCancellationRequested();

    if (configureAwait)
    {
        try
        {
            action();
        }
        finally
        {
            await UniTask.Yield();
        }
    }
    else
    {
        action();
    }

    cancellationToken.ThrowIfCancellationRequested();
}


AsyncOperationをawait

シーンなどのロード時に、

await SceneManager.LoadSceneAsync("name")
                  .ConfigureAwait(Progress.Create<float>(x => {
                      Debug.Log(x);
                  }));

上記のような形で進捗が取れたが、ConfigureAwaitが廃止となったので使用できなくなった。
代わりに、ToUniTask()やWithCancellation()を使用するようにする。


その他の変更

以下サイト様に記載されているので参考にする。

qiita.com


コルーチンからUniTaskへの置き換え

コルーチン
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using System.Collections;

public class DownloadTexture : MonoBehaviour {

    [SerializeField]
    private Image _image;
    private const string TEXTURE_URL = "https://1.bp.blogspot.com/-cT-3wp1oE8E/X9lJoOcpbnI/AAAAAAABc7U/l27LG4wgsoc-D9AhUwDuYfF70r6-A2ccQCNcBGAsYHQ/s872/sori_snow_boy.png";

    void Start() {
        StartCoroutine(CoroutineGetTexture());
    }

    /// <summary>
    /// コルーチンでURLから画像を取得
    /// </summary>
    /// <returns></returns>
    private IEnumerator CoroutineGetTexture() {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(TEXTURE_URL);
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError) {
            Debug.LogError(www.error);
        } else {
            Texture2D texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
            _image.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
        }
    }
}
UniTask
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;
using System;

public class UniTaskDownloadTexture : MonoBehaviour {

    [SerializeField]
    private Image _image;
    private const string TEXTURE_URL = "https://1.bp.blogspot.com/-cT-3wp1oE8E/X9lJoOcpbnI/AAAAAAABc7U/l27LG4wgsoc-D9AhUwDuYfF70r6-A2ccQCNcBGAsYHQ/s872/sori_snow_boy.png";

    void Start() {
        SetTexture();
    }

    private async void SetTexture() {
        Texture2D texture = await UniTaskGetTexture();
        _image.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
    }

    /// <summary>
    /// UniTaskでURLから画像を取得
    /// </summary>
    /// <returns></returns>
    private async UniTask<Texture2D> UniTaskGetTexture() {
        UnityWebRequest www = UnityWebRequestTexture.GetTexture(TEXTURE_URL);
        await www.SendWebRequest();
        if (www.isNetworkError || www.isHttpError) {
            throw new Exception(www.error);
        }
        return ((DownloadHandlerTexture)www.downloadHandler).texture;
    }
}