181
140

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unity #2Advent Calendar 2019

Day 24

UniRx&UniTask とは何なのか

Last updated at Posted at 2019-12-23

はじめに

何番煎じなのかわかりませんが、過去にいろいろ解説した記事へのリンクをまとめる意味も込めて解説します。

UniRxとは

おすすめ資料

概要

UniRxとは、Reactive ExtensionsをUnity向けの実装したC#ライブラリです。
かなり昔のバージョンのUnityでも扱うことができます。
導入することで、Unityにおいて次のような処理の実装が簡単になります。

  • 非同期処理
    • 何らかの処理の完了を待ち受けて次の処理を行うような処理の管理
    • エラーハンドリングやリトライ処理の簡略化
    • 実行結果のハンドリングやキャッシュ
    • 実行スレッドの柔軟な切り替え
// data.txtをスレッドプール上で読み込み、メインスレッド上で表示する
Observable
    .Start(() => File.ReadAllText("data.txt"))
    .ObserveOnMainThread()
    .Subscribe(x => Debug.Log(x));
  • イベント処理
    • 処理のトリガーと実際にハンドリングする場所を分離して記述できる
    • イベントメッセージの加工や合成
    • 実行スレッドの柔軟な切り替え
void Start()
{
    // Jumpボタンが押されたらログに出す
    // 1度発動したら1秒間は何もしない
    this.UpdateAsObservable()
        .Where(_ => Input.GetButtonDown("Jump"))
        .ThrottleFirst(TimeSpan.FromSeconds(1000))
        .Subscribe(_ => Debug.Log("Jump!!"));
}

UniRxは「非同期処理」と「イベント処理」の2つを扱うことができるライブラリです。
特に「時間」の扱いに長けており、Unityにおける「フレームをまたいだ処理」などの実装が簡単になります。

なお、UniRxが提供するこれら便利な機能は正式には「Observable」と呼ばれています。

Observable

ObservableとはUniRxが提供する、「メッセージを通知のための機構およびそのオブジェクト」の指します。
型としてはIObservable<T>で表現されます。

// 実行結果は IObservable<T> でハンドリングする
private IObservable<string> ReadFileObservable(string path)
{
    return Observable
        .Start(() => File.ReadAllText(path))
        .ObserveOnMainThread();
}

Observableの仕組み、およびSchedulerOperatorの概念について詳しく知りたい方は次の資料を御覧ください。

Operator一覧

UniRxでもっとも便利な機能がこのOperatorです。
OperatorObservableに対するさまざまな処理を提供してくれます。
そのためこのOperatorを組み合わせるだけで、だいたいの処理の実装が完了してしまいます。

UniTaskとは

おすすめ資料

概要

UniTaskとは、C#の標準TaskおよびTaskSchedulerをUnity向けに最適化して実装したC#ライブラリです。
UniTaskは標準Taskと比べてパフォーマンスが出るため、非同期処理を扱う場合は是非導入してほしいライブラリです。ただし導入時はUnity 2018.3以降を推奨。

UniTaskを導入することで、次のようなメリットがあります。

  • Unity向けに最適化されたTaskおよびTaskSchedulerが利用できる
    • ValueTaskのUnity向け実装
    • UnityのPlayerLoopを用いたSchedule管理
    • SynchronizationContextに依存しない
async void Start()
{
    var path = "data.txt";

    // UniTaskのawaitは基本的にメインスレッドに戻る
    var result = await LoadFileAsync(path);

    Debug.Log(result);
}

private UniTask<string> LoadFileAsync(string path)
{
    return UniTask.Run(() => File.ReadAllText(path));
}
  • さまざまなオブジェクトへのAwaiter実装の提供
    • コルーチンをasync/awaitに置き換える可能に
    • yield returnの代わりにawaitで非同期処理の待機ができるようになる
private void Start()
{
    JumpAsync(this.GetCancellationTokenOnDestroy()).Forget();
}

/// <summary>
/// ボタンが押されたらジャンプする
/// </summary>
private async UniTaskVoid JumpAsync(CancellationToken token)
{
    var rigidBody = GetComponent<Rigidbody>();

    while (!token.IsCancellationRequested)
    {
        // ボタンが押されるのをUpdateタイミングで待つ
        await UniTask.WaitUntil(() => Input.GetButtonDown("Jump"),
            PlayerLoopTiming.Update,
            cancellationToken: token);

        // 押されたらFixedUpdateのタイミングでジャンプする
        await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
        rigidBody.AddForce(Vector3.up, ForceMode.Impulse);

        // 1秒待って繰り返す
        await UniTask.Delay(1000, cancellationToken: token);
    }
}

UniTaskは「非同期処理」に特化したライブラリです。
導入することでほぼすべてのコルーチンをasync/awaitに置換することができます。

標準Taskとの比較

UniTaskC# 7.0で追加された、ValueTaskライクに作られています。
そのためasync/await時、処理が同期で完了する場合においてはヒープアロケーションが発生しないという特徴があります。また、UniTaskは実行コンテキストの管理にSynchronizationContextではなくUnity Player Loopを用いるように作られています。

そのため、標準のTask/ValueTaskと比較してUniTaskの方がよりUnityではパフォーマンスを出すことができるようになっています。

Task UniTask
機能 Unityでは不要な機能が多い Unityで活用できる機能のみ実装
オブジェクトサイズ 大きい 小さい
実行コンテキストの管理 TaskScheduler & SynchronizationContext UnityのPlayerLoop
必要なC# version C# 5.0以上 C# 7.0以上
TaskTracker UnityEditor上で確認可能
メモリアロケーション 常にヒープを確保する 同期処理で完了する場合はヒープを確保しない

UniRxとUniTask、それぞれの使い分け

UniTaskUniRxの非同期処理の使い分けの基準は何か」という疑問を持つ人もいるでしょう。
それぞれの使い分けについて説明します。

UniTaskを使うべき場合

非同期処理の結果通知が「1回」で済む場合

結果通知が1回である、つまりは単発で完了する普通の非同期処理の場合です。
こちらはUniTask(とasync/await)を使うべきです。

理由として次が挙げられます。

  • 同期処理と見た目がほぼ変わらないコードを書ける
  • 単発で済む非同期処理に対して、Observable自体がオーバースペック気味

async/awaitを使うと非同期処理をほぼ同期処理と変わらない見た目で記述することができます。
そのため同期処理で書いていた部分をあとから非同期化する、またはその逆といった対応も比較的楽にすみます。

Observableの場合、処理が1回で済む非同期処理に対してはかなりオーバースペックになってしまいます。まず「メッセージの発行元は何か」「メッセージは何回発行されるのか」を常に意識しなくてはいけません。
さらにはHotやColdといった性質も考えなくてはならず、UniTaskほど気軽に扱えません。

また、Observableを用いた非同期処理は同期処理とまったく異なる記述法になってしまいます。
そのため、あとから処理内容を同期処理へ直そうと思ったときに、Observableを使っている場所ほぼすべてを新しく書き直す必要が出てきてしまいます。

処理を手続き的に記述したい場合

Observableでは非同期処理をOperatorを用いて処理を宣言的に記述することができました。
これは処理の内容がOperatorの表現範囲で済む限りにおいては便利ではあります。
ですが実際の開発においてはそう簡単に行きません。Operatorではどうしても表現しきれない処理が出てきた場合、これを手続き的に書き下す必要があります。

また、Observableは「処理の条件分岐ができない」という欠点があります。
メッセージ内容に応じてその場で実行する処理をごっそり切り替えるといったことができません。
(処理内容ごとに新しいObservableを定義しなくてはいけないため無駄が多い)

UniTask(とasync/await)であれば、処理をほぼ同期処理と変わらずに手続き的に記述することができます。
そのため、要求どおりの仕様を満たした処理をObservableOperatorで記述するよりも、よりわかりやすい簡単に実装することができます。

UniRx(Observable)を使うべき場合

非同期処理の結果が複数個になる場合

ここでいう複数個の結果とは、UniTask<IEnumerable<T>>で済むような場合の話ではありません。非同期処理そのものが継続的に実行され、結果が断続的に発行されるようなシチュエーションを想定しています。
(たとえばディレクトリの中身をまとめて読み込んで、読み込みが先に終わったファイルから処理を次々に行うなど)

このような非同期処理はUniTaskでは表現が不可能なため、IObservable<T>を使うことになります。

補足 IAsyncEnumerable<T>IObservable<T>の違い

C# 8.0IAsyncEnumerable<T>というものが追加されました。名前のとおり「非同期ストリーム」です。IObservable<T>と役割が被ってそうですが、そこは明確に異なります。

  • IObservable<T> : 非同期処理が常に動いておりその結果がPUSHで通知される
  • IAsyncEnumerable<T> : 結果をPULLしたときに1つ非同期処理が走る

非同期処理を裏で動かしてその結果がPUSH通知されるのを待つならIObservable<T>を使う。
await foreachと組み合わせて、逐次処理をしていくならIAsyncEnumerable<T>を使う。

といった使い分けが今後は必要になってくるでしょう。
(まだUnityはC# 8.0に対応していないので、もう少しあとの話ですが)

イベント処理を行う場合

C#event構文や、UnityEventの代替としてUniRxは利用することができます。
またUpdate()FixedUpdate()Observableに変換したり、uGUIと組み合わせて使うと表現力が広がりかなり便利に使うことができます。

(そもそも「イベント処理」=「値が複数回発行される非同期処理」とほぼ同義なので、同じことを2回説明しているだけですが)

Unityで動くC#バージョンが低い場合

古のUnityでは対応しているC#バージョンが低く、async/awaitすら使えない場合があります。
特に長期運用しているプロジェクトですと未だにUnity 5系を使っているなんてこともあるでしょう。

この場合はUniTaskを用いることはできないので、UniRxが非同期処理における唯一の選択肢となります。
コルーチンとObservableを併用するなどするとよいでしょう。

相互変換

なお、ObservableUniTaskはそれぞれ相互変換が可能です。
そのためどちらか片方にこだわる必要はなく、場面に応じて変換して使い分けるとよいでしょう。

UniTask -> UniRx

キャンセルを考えない場合

UniTask.ToObservable()で変換できます。
この場合、Schedulerは自動的にMainThreadScheduler指定となります。

using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.Networking;

public class UniTaskToUniRx : MonoBehaviour
{
    void Start()
    {
        // UniTask<string>
        var uniTask = LoadTextAsync("https://github.com/");

        // UniTask<string> -> IObservable<string>
        // UniTaskのキャンセルを考えないならこれだけでOK
        uniTask.ToObservable().Subscribe(Debug.Log);
    }

    /// <summary>
    /// Textを指定のパスから読み込む
    /// </summary>
    private async UniTask<string> LoadTextAsync(string path)
    {
        var uwr = UnityWebRequest.Get(path);
        await uwr.SendWebRequest();
        return uwr.downloadHandler.text;
    }
}

キャンセルを考える場合

一発で変換するメソッドはありません。
そのため次のような方法でがんばるしかないです。

UniRx -> UniTask

IObservable -> UniTask

ToUniTask()を使うことでUniTaskに変換できます。
ただしデフォルトではOnCompletedメッセージが発行されるまで待ち受けてしまいます。
次に発行される1メッセージだけを待ち受けたい場合は、useFirstValueオプションをつけましょう。

private async UniTaskVoid CheckEnemyAsync(CancellationToken cancellationToken)
{
    // 新たに生成されたEnemyが通知されるObservableがあったとして
    IObservable<Enemy> enemyObservable = _enemySpawner.OnEnemySpawned;

    while (!cancellationToken.IsCancellationRequested)
    {
        // ToUniTask(useFirstValue: true) でUniTask化してawaitできる
        var enemy = await enemyObservable
            .ToUniTask(cancellationToken, useFirstValue: true);

        Debug.Log(enemy.Name);
    }
}

IReadonlyReactivePropertyのawait

正確にいえばUniTask変換ではないですが、どうせasync/awaitとセットで使うので解説します。
IReadonlyReactiveProperty<T>awaitすることで、次のメッセージ発行を待ち受けることができます。

using System.Threading;
using UniRx;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;

public class ReactivePropertyAwait : MonoBehaviour
{
    // なにかのステート
    private enum GameState
    {
        Ready,
        Battle,
        Result
    }

    // ステート管理するReactiveProperty
    private ReactiveProperty<GameState> _currentGameState
        = new ReactiveProperty<GameState>();

    private void Start()
    {
        StateChangedAsync(this.GetCancellationTokenOnDestroy()).Forget();
    }

    /// <summary>
    /// ステート遷移するたびに処理を走らせる
    /// </summary>
    private async UniTaskVoid StateChangedAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            // ステート遷移を待つ
            var next = await _currentGameState;

            // 遷移先に合わせて処理をする
            switch (next)
            {
                case GameState.Ready:
                    // Do something...
                    break;
                case GameState.Battle:
                    // Do something...
                    break;
                case GameState.Result:
                    // Do something...
                    break;
            }
        }
    }
}

その他、変わった変換の例

その他の変わった変換パターンについては別記事にまとめました。

まとめ

UniRxUniTaskはUnityでC#を触るならぜひとも導入してほしいライブラリです。
入れて絶対に損はしない、むしろ無いと困るくらいには便利なものなので使い方を覚えて活用できるようになるとよいでしょう。

また、現在UniRxとUniTaskの書籍の執筆を行っています。全体の執筆が概ね終わり、現在はレビューフェーズに入っています。発売されたらぜひよろしくおねがいします。

181
140
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
181
140

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?