Help us understand the problem. What is going on with this article?

コルーチンを全てUniTaskに置き換える

More than 1 year has passed since last update.

はじめに

先日、unity1weekでゲームを作った際、今までコルーチンを使っていたところを全てUniTaskで書きました(作ったものはこちら→https://unityroom.com/games/autokanjichess)

思ったよりも簡単にUniTaskへ書き換えられましたので、この記事ではコルーチンと勝手が違うところを解説し、UniTaskへの書き換え方をまとめてみたいと思います。

UniTask – Unityでasync/awaitを最高のパフォーマンスで実現するライブラリ - Cygames Engineers' Blog
http://tech.cygames.co.jp/archives/3256/

確認環境

この記事は以下の環境で確認しています。

Unity 2018.4.0f1
UniTask v1.1.0

コルーチンから書き換えるメリット/デメリット

メリットは以下の通り。

  • 戻り値が使える
  • 標準で動作中の関数を一覧出来るツールが付いている(UniTask Tracker)
  • Update以外のタイミングで待てる(コルーチンはUpdate後だけ)
  • いざという時、別スレッドに回せる
  • (個人的に)書きやすい

デメリットは以下の通り。

  • GameObjectが削除されたりしても止まらず、途中で処理を止めるのが大変
  • 資料がまだまだ少ない

これはコルーチンよりもキャンセル処理を厳密に制御出来るのでメリットとも取れるのですが、初学者にはキツいポイントだと思います。詳しくはキャンセル処理の解説時に。

導入

https://github.com/Cysharp/UniTask/releases
ここから最新版の.unitypackageをダウンロード

少し前までは、AssetStoreのUniRxを導入すればセットでUniTaskも付いてきましたが、現在は分離されているので注意。

最初に気をつけること

  • UniTaskはシーンの切り替えや、オブジェクトの破棄では止まらない

これは気づきにくい割に、バグの温床になりやすいので最初に書きます。
コルーチンは、StartCoroutineしたGameObjectに紐づくんですが、UniTaskはそういった紐づけはありません。
例えば、更新用のコルーチンで無限ループで動作し続ける関数とか作ることがあると思います。
そういったコルーチンを何も考えずにUniTaskに書き換えると、止まるハズの関数が止まらず、おそらくバグります。バグらなくても、裏で動くUniTaskが増え続け、パフォーマンスに影響を与えるでしょう。

じゃあ、どうやってUniTaskを止めるかですが、UniTaskの関数を呼ぶ時に引数でthis.GetCancellationTokenOnDestroy()を渡します。詳しくは最後に載せますが、これがコルーチンから書き換える際の唯一の難点です…。
あと、こういった動いているUniTaskを追う方法も紹介します。

基本の書き方

コルーチンの関数の書き換え方

Coroutine.cs
IEnumerator CoroutineHoge()
{
    Debug.Log( "Coroutine:1フレーム目" );
    yield return null;
    Debug.Log( "Coroutine:2フレーム目" );
}

例えばこんな感じのコルーチンの関数があるとします。
これをUniTaskで書き換えると、

Coroutine.cs
async UniTask UniTaskHoge( CancellationToken cancellation_token )              // IEnumerator→async UniTask
{
    Debug.Log( "UniTask:1フレーム目" );
    await UniTask.Yield( PlayerLoopTiming.Update, cancellation_token );               // yield return null;→await UniTask.Yield( PlayerLoopTiming.Update, cancellation_token );
    Debug.Log( "UniTask:2フレーム目" );
}

こんな感じになります。
IEnumeratorasync UniTaskに、yield return nullawait UniTask.Yield( PlayerLoopTiming.Update, cancellation_token );になっている以外は大きな違いはありません。
PlayerLoopTiming.Updateは関数の更新タイミングをUnity標準のUpdate関数に合わせる指定です。
CancellationTokenは前述の「処理を途中で止める」のに必要な引数です。こちらは詳しくは後述します。

そして、関数の呼び出しも書き換えます。

Start.cs
void Start()
{
    // おなじみStartCoroutine
    StartCoroutine( CoroutineHoge() );
    // asyncの関数はそのまま書ける
    // 「this.GetCancellationTokenOnDestroy()」はオブジェクトの破棄で関数を止めるために必要
    UniTaskHoge( this.GetCancellationTokenOnDestroy() ).Forget();            // このStart関数だと「.Forget()」がないと警告が出る
}

戻り値UniTaskの関数は、普通の関数と同じように書けます。これはC#自体の仕様になります。そして前述の通りthis.GetCancellationTokenOnDestroy()を関数に渡しています。
また、.Forget()と見慣れない関数がくっついているのが気になるでしょう。
.Forget()関数無しでも、動作はしますが以下のような警告が出ます。

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

「この関数は非同期(async)だから、呼んだ関数が終わるまで待たないけど、大丈夫?」みたいな警告が出ます。.Forget()はこの警告が出ないように関数を呼んでくれる関数です。

yield returnを書き換える

コルーチンでは、yield return xxxxと書いて処理を待機しました。
UniTaskでは、await xxxxと書いて待機します。

Wait.cs
// yield return null;
await UniTask.Yield( PlayerLoopTiming.Update, cancellation_token );
await UniTask.DelayFrame( 0, false, PlayerLoopTiming.Update, cancellation_token );        // こちらでも1フレーム待てるが、タイプ数が多く、引数も紛らわしいのでおすすめしない
// yield return new WaitForSeconds(1f);
await UniTask.Delay( 1000, false, PlayerLoopTiming.Update, cancellation_token );        // Delayはms単位なので注意
// yield return new WaitWhile( () => _is_wait  );
await UniTask.WaitWhile( () => _is_wait, PlayerLoopTiming.Update, cancellation_token );
// yield return CoroutineFuga();
await CoroutineFuga();              // UniTaskからコルーチンを呼び出し・待機することが可能
await UniTaskFuga( cancellation_token );                // UniTaskFugaが終わるまで待機

コルーチンでよく使う待機処理の書き換えを書いてみました。

また、UniTaskの実装により、UnityのWWWなども待機出来ます。
それについてはこちらの記事が詳しいです。

UniRx.Async(UniTask)機能紹介
https://qiita.com/toRisouP/items/4445b6b9bf00e49eb147

UniTaskを途中で止める

最初に書いた通り、UniTaskは呼び出したオブジェクトが破棄されたりしても、素知らぬ顔して動き続けます。
なので、Update関数代わりのコルーチンを書き換える際には、UniTaskを途中で止めるためにキャンセル処理を書く必要があります。そのための準備が、今までたくさん書いてきたCancellationTokenになります。

動いているUniTaskを確認する

さて、実際にキャンセル処理について解説する前に、問題のある関数を調べる方法をご紹介しておきます。
メニューバー→Window→UniRx→UniTask Trackerで、現在動いているUniTaskの関数が一覧出来ます。

image.png

シーンを切り替えたりした後にこの画面を確認して、キャンセル出来ていない関数が無いか調べると良いでしょう。

キャンセル処理

まず、すぐ思いつく方法としては、フラグ変数を用意して、キャンセルしたくなったらフラグを下ろすとか。

Cancel.cs
    bool _is_update = true;

    void OnDisable()
    {
        _is_update = false;
    }

    async UniTask UniTaskHoge()
    {
        while( _is_update )      // 削除されているのにアクセス出来るので気持ちが悪い
        {
            Debug.Log( "更新中…" );
            await UniTask.Yield();
        }
    }

こうすれば、オブジェクトが非アクティブになったり、削除されると、無限ループを抜けて、関数は終了します。が、いちいちオブジェクトを作る度にフラグ変数用意して…とやっていると大変ですし、削除されたはずのオブジェクトの変数にアクセスしているので怖いです。

そこでCancellationTokenというものを使います。
まず、関数の引数にCancellationToken型の変数を1つ追加し、Yield関数にこの変数を渡します。

CancellationToken.cs
async UniTask UniTaskHogeCancel( CancellationToken cancellation_token )
{
    while( true )
    {
        Debug.Log( "更新中…" );
        await UniTask.Yield( PlayerLoopTiming.Update, cancellation_token );
    }
}

そして、この関数を呼ぶ際にthis.GetCancellationTokenOnDestroy()を渡せば、オブジェクトの破棄に合わせて、関数もキャンセルされます。

Call.cs
UniTaskHogeCancel(this.GetCancellationTokenOnDestroy()).Forget();

CancellationTokenは先程のフラグ変数のようなものです。Yieldなど、UniTaskの待機関数にはCancellationTokenを渡すことが出来ます。そして、渡したトークンがキャンセルされると、関数を抜けてくれます。GetCancellationTokenOnDestroy()はオブジェクトが削除された時にこのキャンセルされるトークンを取得する関数というわけです。
(だいぶ要約したのでちょっと誤りがあるかも。)

キャンセルについてはこちらの記事がより詳しいです。
https://qiita.com/su10/items/ccb12742ad0be790b323

また、これがUniTaskというやasync/awaitの難点なんですが、このCancellationTokenを受け取った関数で呼ぶasyncな関数にもCancellationTokenを渡す必要があります。渡さないと、関数の先でキャンセル出来ませんからね。
なので、GameObjectの寿命に紐づくコルーチンから書き換えたUniTaskの関数は、すべからくこのCancellationTokenを引数に持ち、そこから呼び出すUniTaskの関数にバケツリレーのようにトークンを渡していく必要があります。超面倒。
非同期処理がどこでキャンセル出来るか細かく把握・制御出来るので、安全っちゃ安全ですが…。

おわりに

最初はコルーチンをすべて書き換えるのは無理かなと思っていましたが、いざやってみると意外と行けました。が、やっぱりキャンセル処理が難点です。最初は気にせず作ってたんですが、UniTask Trackerの存在を使い始めてから知って、大量のゾンビタスクが表示された時は肝が冷えました。
とはいえ、GetCancellationTokenOnDestroy()して、トークンをバケツリレーするだけなので、一度覚えてしまえばあんまり気にならなかったです。
ただ、こうして記事にしてみると、キャンセルのための記述がハードル高めていそうです。Yieldなどの記述がかなり増えるので、初学者にはしんどそう…。ううむ…。

参考

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合 - neue cc
http://neue.cc/2018/07/12_567.html
【Unity】UniTask+C#7.0でモダンなUpdateLoopを作る - のたぐすブログ
https://notargs.hateblo.jp/entry/async_update
UniTask入門 - Speaker Deck
https://speakerdeck.com/torisoup/unitaskru-men?slide=27

pio
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした