知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【Unity】UniTask 【1】#91

前回の成果

UniRxのHotとColdについて理解した。

soramamenatan.hatenablog.com


今回やること

UniTaskについて学びます。


UniTaskとは

Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ

UniTask – Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ | Cygames Engineers' Blog:より引用

asyncとawaitは既存のC#にTaskが存在するのですが、Taskよりパフォーマンスを良くしたものになります。


asyncとawaitとは

asyncとawaitは非同期処理を行うために使用するものになります。
これらを使うことで、同期処理を書くような感覚で非同期処理を書くことができます。


非同期処理

非同期処理とは、ある処理を実行している間に他のタスクで別の処理を実行するものになります。
逆に、ある処理を実行している間は他のタスクの処理を中断するものを同期処理と呼びます。

同期処理と非同期処理のイメージ

f:id:soramamenatan:20210129102642g:plain

I-26-2. 非同期処理と同期処理の実装パターンと特徴 | 日本OSS推進フォーラム:より引用


この非同期処理は、コルーチンでも実装することが出来ます。
以下のようなソースコードで非同期処理を確認することができます。

コルーチン
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AsynchronousSimple : MonoBehaviour {
    void Start() {
        LogCoroutine();
    }

    private void LogCoroutine() {
        Debug.Log("コルーチン開始");
        StartCoroutine(CoroutineTimer(1));
        Debug.Log("コルーチン終了");
    }

    private IEnumerator CoroutineTimer(int time) {
        yield return new WaitForSeconds(time);
        Debug.Log("指定時間経過");
    }
}
結果

プログラムの順番的には、

  1. コルーチン開始
  2. 指定時間経過
  3. コルーチン終了

で記載しているのに、実際は

  1. コルーチン開始
  2. コルーチン終了
  3. 指定時間経過

となっています。
これは、コルーチンが非同期処理で行われているからになります。

f:id:soramamenatan:20210129103527p:plain


具体例

非同期処理について理解できたので、asyncとawaitの具体例をみてみます。

基本的な構文
async 戻り値 メソッド名(引数) {
    // 完了するまで待つ
    await 処理;
}
asyncとawaitの具体例
private async Task TaskTimer() {
    Debug.Log("タスク開始");
    // 1000ミリ秒待つ
    await Task.Delay(1000);
    Debug.Log("タスク終了");
}
結果

タスク開始から1000ミリ秒後にタスク終了のログが出ています。

f:id:soramamenatan:20210129105410p:plain


Task

Taskとは、言葉のとおり仕事という意味です。
以下のソースコードを見ていただけるとわかりやすいです。

1000ミリ秒待機する「タスク」の完了を待つ「タスク」
private async Task TaskTimer() {
    Debug.Log("タスク開始");
    // 1000ミリ秒待つ「タスク」の完了を待つ
    await Task.Delay(1000);
    Debug.Log("タスク終了");
} // という1つの「タスク」

async、await、taskを簡単にまとめると以下の表のようになります。

\ 意味
async メソッドを非同期にする
await 指定したTaskの完了を待ち、結果を取り出す
Task 仕事、処理

詳しくはこちらのサイト様がわかりやすいです。

qiita.com


UniTaskとTask

UniTaskは、Taskをそのまま置き換えることで使用できます。

UniTask
private async UniTask UniTaskTimer() {
    Debug.Log("UniTask開始");
    // 1000ミリ秒待つ
    await UniTask.Delay(1000);
    Debug.Log("UniTask終了");
}

比較してみるとわかりやすいです。

// Task
private async Task TaskTimer() {
    Debug.Log("タスク開始");
    // 1000ミリ秒待つ
    await Task.Delay(1000);
    Debug.Log("タスク終了");
}

// UniTask
private async UniTask UniTaskTimer() {
    Debug.Log("UniTask開始");
    // 1000ミリ秒待つ
    await UniTask.Delay(1000);
    Debug.Log("UniTask終了");
}
結果

結果もTaskの時と同じです。

f:id:soramamenatan:20210129110055p:plain

ですが、UniTaskとTaskでは以下の違いがあります。

\ Task UniTask
機能 Unityで不要なものが多い Unity用に最適化
オブジェクトのサイズ 大きい 小さい
C#の最低バージョン 5.0 7.0
await中のタスク 見れない 見れる

基本的にUniTaskがTaskの上位互換となっています。


UniTaskが提供している機能

では実際にUniTaskにはどのような機能があるか見ていきます。

UniTask、UniTask<T>

TaskをUnity用に最適化したものになります。
基本的には、先程も紹介したとおりTaskをUniTaskへ置き換えるだけで使用することができます。
UniTask型の制作方法について、いくつか挙げていきます。

async、awaitの戻り値

asyncとawaitの戻り値をUniTaskに置き換えることで制作できます。

戻り値の置き換え
/// <summary>
/// async,awaitの戻り値でUniTask生成
/// </summary>
/// <returns></returns>
private async UniTask AsyncCreate() {
    await UniTask.Delay(1000);
    Debug.Log("タスク完了");
}
結果

f:id:soramamenatan:20210130131655p:plain


UniTaskCompletionSource

TaskにはTaskCompletionSourceというTaskの結果を取得できるものがあります。
これと同じような機能がUniTaskにも用意されているので、それを使用することでもUniTaskを制作できます。

UniTaskの結果を受け取る
// UniTaskのステータス
private enum STATUS {
    SUCCESS,
    ERROR,
    CANCEL,
}

[SerializeField]
private STATUS _status;

/// <summary>
/// UniTaskCompletionSourceでUniTask生成
/// </summary>
/// <returns></returns>
private void CompletionSourceCreate() {
    UniTaskCompletionSource<int> utc = new UniTaskCompletionSource<int>();
    UniTask<int> task = utc.Task;
    switch (_status) {
        case STATUS.SUCCESS:
            // 結果を設定して完了状態にする
            utc.TrySetResult(123);
        break;
        case STATUS.ERROR:
            // 失敗状態にする
            utc.TrySetException(new Exception("error message"));
        break;
        case STATUS.CANCEL:
            // キャンセル状態にする
            utc.TrySetCanceled();
        break;

    }
    Debug.Log("Task Status : " + task.Status);
}
_status = SUCCESS

f:id:soramamenatan:20210130132655p:plain

_status = ERROR

f:id:soramamenatan:20210130132652p:plain

_status = CANCEL

f:id:soramamenatan:20210130132650p:plain


IObservableから変換

UniRxのIObservableから変換することも出来ます。
逆に、UniTaskをIObservableに変換することも出来ます。
コメントにも記載してあるのですが、Observableが完了しないものをUniTaskに変換しないように注意してください。

IObservableから変換する例
/// <summary>
/// IObservableをUniTaskに変換
/// </summary>
private void IObservableCreate() {
    IObservable<int> observable = Observable.Return(0);
    // IObservableからUniTaskへ変換
    UniTask<int> uniTask = observable.ToUniTask();
    // UniTaslからIObservableへ変換
    IObservable<int> newObservable = uniTask.ToObservable();

    // Observableが終わらないものは変換しない
    UniTask<long> badUniTask = Observable.EveryUpdate().ToUniTask();
}


静的なメソッド群

次にUniTaskが提供しているstaticなメソッドを紹介します。
全ては紹介しきれないので、いくつか挙げます。


UniTask.Delay

引数で指定した秒数、待つことができます。
また、Unityのどのタイミングで計測するかを指定することができます。
デフォルトはUpdate()のタイミングとなっています。

UniTask.Delayの例
/// <summary>
/// 指定秒数待つ
/// </summary>
/// <returns></returns>
private async UniTask ExcuteDelay() {
    // 1000ミリ秒待つ
    await UniTask.Delay(1000);
    Debug.Log("1000ミリ秒経過");
    // 1秒待つ
    await UniTask.Delay(TimeSpan.FromSeconds(1));
    Debug.Log("1秒経過");
    // FixedUpdateタイミングで1000ミリ秒待つ、デフォルトはUpdateタイミング
    await UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate);
    Debug.Log("FixedUpdateで1000ミリ秒経過");
}

UniTask.DelayFrame

引数で指定したフレーム数待つことができます。
また、Unityのどのタイミングで計測するかを指定することができます。 デフォルトはUpdate()のタイミングとなっています。

UniTask.DelayFrameの例
/// <summary>
/// 指定フレーム待つ
/// </summary>
/// <returns></returns>
private async UniTask ExcuteDelayFrame() {
    await UniTask.DelayFrame(60);
    Debug.Log("60フレーム経過");
    await UniTask.DelayFrame(60, delayTiming: PlayerLoopTiming.FixedUpdate);
    Debug.Log("FixedUpdateで60フレーム経過");
}

UniTask.Yield

指定したタイミングで1フレーム待つことができます。
イメージとしては、コルーチンのyeild return nullに近いです。
注意点として、1フレーム待ったあとの処理は指定したタイミングで実行されます。
ですので、Updateタイミングに戻したい場合は引数無しか、Updateを指定する必要があります。

/// <summary>
/// 指定タイミングで1フレーム待機
/// </summary>
/// <returns></returns>
private async UniTask ExcuteYield() {
    // Updateで1フレーム待機
    await UniTask.Yield();
    // FixedUpdateで1フレーム待機
    await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
    Debug.Log("このLogはFixedUpdateのタイミングで実行される");
    await UniTask.Yield();
    Debug.Log("このLogはUpdateのタイミングで実行される");
}

UniTask.SwitchToThreadPool / UniTask.SwitchToMainThread

UniTask.SwitchToThreadPoolは以降の処理をスレッドプールで行うようにできます。
UniTask.SwitchToMainThreadはメインスレッドで行うようにできます。
先ほど紹介したYield()でもメインスレッドに切り替えることができますが、必ず1フレーム待ちます。

SwitchToThreadPool / SwitchToMainThreadの例
/// <summary>
/// スレッドを切り替える
/// </summary>
/// <returns></returns>
private async UniTask ExcuteThreadPool() {
    // スレッドプールに切り替える
    await UniTask.SwitchToThreadPool();
    Debug.Log("スレッドプール上での処理");
    // メインスレッドに切り替える
    await UniTask.SwitchToMainThread();
    Debug.Log("メインスレッド上での処理");
    await UniTask.SwitchToThreadPool();
    // Yieldでメインスレッドに切り替えることもできるが、1フレーム待ってしまう
    await UniTask.Yield();
}

UniTask.Run

引数で指定したデリゲートをスレッドプール上で実行することができます。
実行後は、メインスレッドへ戻ります。
Instantiateのようなスレッドプール上では実行ができないメソッドもあるので、気をつけてください。

UniTask.Runの例
/// <summary>
/// 引数のデリゲートを実行
/// </summary>
/// <returns></returns>
private async UniTask ExcuteRun() {
    int hoge = 1;
    int fuga = 1;
    // スレッドプール上で実行、処理後はメインスレッドへ
    await UniTask.Run(() => {
        int hogeFuga = hoge + fuga;
        Debug.Log(hogeFuga);
    });

    // Instantiateのようなスレッドプールでは実行できないものもある
    await UniTask.Run(() => {
        GameObject obj = Instantiate(_prefab, this.transform);
    });
}

UniTask.WhenAll

指定した全てのUniTaskが完了するのを待つことができます。
また、タプルを使用することで結果を受け取ることもできます。

UniTask.WhenAllの例
/// <summary>
/// 指定した全てのUniTaskが完了するまで待機
/// </summary>
/// <returns></returns>
private async UniTask ExcuteWhenAll() {
    UniTask<int> task1 = UniTask.Run(() => 100);
    UniTask<string> task2 = UniTask.Run(() => "Finish");
    // 指定した全てのUniTaskの完了を待機
    // 結果をタプルで受け取ることもできる
    var (t1, t2) = await UniTask.WhenAll(task1, task2);
    Debug.Log("t1 : " + t1);
    Debug.Log("t2 : " + t2);
}
結果

UniTask.Runでの結果をUniTask毎に確認することができます。

f:id:soramamenatan:20210207143047p:plain

UniTask.WhenAny

指定したUniTaskのどれか1つでも完了するまで待機します。
指定したUniTaskの方が全て同じの場合、どれが完了したのかを直接取得することもできます。

UniTask.WhenAnyの例
/// <summary>
/// 指定したどれかのUniTaskが完了するまで待機
/// </summary>
/// <returns></returns>
private async UniTask ExcuteWhenAny() {
    UniTask task1 = UniTask.Delay(3000);
    UniTask<string> task2 = UniTask.Run(() => "Finish");
    // 指定したどれかのUnitaskの完了を待機
    await UniTask.WhenAny(task1, task2);
    Debug.Log("どちらかのUnitaskが完了");

    List<UniTask<string>> listTask = new List<UniTask<string>>();
    listTask.Add(UniTask.Run(() => "No.1 Task"));
    listTask.Add(UniTask.Run(() => "No.2 Task"));
    listTask.Add(UniTask.Run(() => "No.3 Task"));
    // タスクの方が全て同じの場合、完了したタスクを直接取得することができる
    var (_, result) = await UniTask.WhenAny(listTask.ToArray());
    Debug.Log("終了したタスク : " + result);
}
結果

「どちらかのUnitaskが完了」でDelayを待たずに、タスクが完了したログが出ています。
また、「終了したタスク : No.1 Task」でどのタスクが完了してWhenAnyの待機が終了したかが確認できているのがわかります。

f:id:soramamenatan:20210207144918p:plain

UniTask.WaitUntil / UniTask.WaitWhile

UniTask.WaitUntilは指定した条件がtrueになるまで待機します。
UniTask.WaitWhileは指定した条件がfalseになるまで待機します。

UniTask.WaitUntil / UniTask.WaitWhileの例
[SerializeField]
private bool _isWaitFlag;

/// <summary>
/// 指定した条件がtrue/falseになるまで待機
/// </summary>
/// <returns></returns>
private async UniTask ExcuteWaitFlag() {
    _isWaitFlag = false;
    // 指定した条件がtrueになるまで待機
    await UniTask.WaitUntil(() => _isWaitFlag);
    Debug.Log("WaitUntil終了");
    // 指定した条件がfalseになるまで待機
    await UniTask.WaitWhile(() => _isWaitFlag);
    Debug.Log("WaitWhile終了");
}
結果

指定したフラグがtrue、falseになるまで待機しています。

f:id:soramamenatan:20210207145812p:plain

UniTask.WaitUntilValueChanged

指定した値が変化するまで待機します。
UniRxのObserveEveryValueChangedと同じです。

UniTask.WaitUntilValueChangedの例

/// <summary>
/// 指定した値が変化するまで待機
/// </summary>
/// <returns></returns>
private async UniTask ExcuteWaitUntilValueChanged() {
    // 自身の座標が変化するまで待機
    await UniTask.WaitUntilValueChanged(transform, x => x.position);
    Debug.Log("移動した");
}

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