コガネブログ

平日更新を目標に Unity や C#、Visual Studio、ReSharper などのゲーム開発アレコレを書いていきます

【Unity】Unity 2017 でコルーチンの代わりに async / await を使用する

はじめに

http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/

この記事は、上記のページで公開されている
Unity 2017 でコルーチンの代わりに
async / await を使用する方法を翻訳したものになります

目次

コルーチン

コルーチンは便利ですが、いくつかの欠点があります

  • コルーチンは値を返すことができません
    そのため、Action 型のコールバックを引数で渡すなどの対応が必要になります
     
  • try-catch 内に yield を入れることはできないので、例外を処理できません
    また、例外が発生した場合、スタックトレースには
    例外が投げられたコルーチンが通知されるだけなので、
    呼び出し元を推測する必要があります

async / await

Unity 2017 では async / await という新しい C# の機能が使えるようになりました
これは、コルーチンと比べてたくさんの素晴らしい機能が用意されています

f:id:baba_s:20180506101521p:plain

この機能を有効にするには、Unity メニューの「File>Build Settings...」から
「Player Settings」を選択し、「Scripting Runtime Version」を
「Experimental (.NET 4.6 Equivalent)」に変更する必要があります

簡単な例

ここではコルーチンと async / await を簡単に比較してみます
まずはコルーチンの使用例です

using System.Collections;
using UnityEngine;

public class Example : MonoBehaviour
{
    private IEnumerator Start()
    {
        Debug.Log( "1秒待つ..." );
        yield return new WaitForSeconds( 1f );
        Debug.Log( "完了!" );
    }
}

次は、async / await の使用例になります

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log( "1秒待つ..." );
        await Task.Delay( TimeSpan.FromSeconds( 1 ) );
        Debug.Log( "完了!" );
    }
}

コルーチンを使用したコードは、このように async / await に置き換えることができます

Custom Awaiters

前述した async / await のサンプルコードでは、

await Task.Delay( TimeSpan.FromSeconds( 1 ) );

1秒待機するコードが少し冗長でしたが、

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public static class TimeSpanExt
{
    public static TaskAwaiter GetAwaiter( this TimeSpan self )
    {
        return Task.Delay( self ).GetAwaiter();
    }
}

このような拡張メソッドを定義することで

await TimeSpan.FromSeconds( 1 );

TimeSpan を直接使用して1秒待機するコードをこのように記述できるようになります

AsyncAwaitUtil.unitypackage

上記のページで公開されている
AsyncAwaitUtil.unitypackage をプロジェクトに導入すると、
Unity が用意している WaitForEndOfFrame、WaitForSeconds、WWW なども
async / await で使用できるようになります

using System.Collections;
using System.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

public class Example : MonoBehaviour
{
    private async void Start()
    {
        // 1フレーム待つ
        await new WaitForEndOfFrame();

        // 1秒待つ
        await new WaitForSeconds( 1f );

        // 1秒待つ(Time.timeScale は無視)
        await new WaitForSecondsRealtime( 1f );

        // コルーチンの完了を待つ
        await Wait();

        // コルーチンの完了を待機して戻り値を受け取る
        var value = ( string )( await WaitWithValue() );

        // シーンの読み込みを待つ
        await SceneManager.LoadSceneAsync( "" );

        // リソースの読み込みを待つ
        var prefab = await Resources.LoadAsync<GameObject>( "" );

        // アセットバンドルのダウンロードを待つ
        var ab = ( await new WWW( "" ) ).assetBundle;

        // アセットバンドルからアセットの読み込みを待つ
        var mat = ab.LoadAssetAsync<Material>( "" );

        // HTTP 通信の完了を待つ
        var request = UnityWebRequest.Get( "" );
        await request.SendWebRequest();
        var data = request.downloadHandler.data;

        // プログラムを起動して終了を待つ
        var returnCode = await Process.Start( "" );
    }

    private IEnumerator Wait()
    {
        yield return new WaitForSeconds( 1f );
    }

    private IEnumerator WaitWithValue()
    {
        yield return new WaitForSeconds( 1f );
        yield return "ピカチュウ";
    }
}

例外処理

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void OnGUI()
    {
        if ( GUILayout.Button( "タスク開始" ) )
        {
            RunTaskAsync();
        }
    }

    private async Task RunTaskAsync()
    {
        await new WaitForSeconds( 1f );
        throw new Exception(); // ここで例外発生
    }
}

例えばこれは、ボタンが押された時に非同期メソッドが開始され、
1秒後に例外が発生するコードです
しかし、Unity で実行すると例外が検知されないことがわかります

例外を検知するためには下記のようなコードに変更します

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void OnGUI()
    {
        if ( GUILayout.Button( "タスク開始" ) )
        {
            RunTask();
        }
    }

    private async void RunTask()
    {
        await RunTaskAsync();
    }

    private async Task RunTaskAsync()
    {
        await new WaitForSeconds( 1f );
        throw new Exception(); // ここで例外発生
    }
}

このように、async void の関数を間に挟むことで、例外が検知できるようになります

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void OnGUI()
    {
        if ( GUILayout.Button( "タスク開始" ) )
        {
            RunTaskAsync().WrapErrors();
        }
    }

    private async Task RunTaskAsync()
    {
        await new WaitForSeconds( 1f );
        throw new Exception(); // ここで例外発生
    }
}

前述した AsyncAwaitUtil.unitypackage をプロジェクトに導入した場合は、
このように WrapErrors 関数を使用して例外を検知することも可能です

コルーチンから非同期メソッド呼び出し

using System.Collections;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine( RunTask() );
    }

    private IEnumerator RunTask()
    {
        yield return RunTaskAsync().AsIEnumerator();
        Debug.Log( "カイリュー" );
    }

    private async Task RunTaskAsync()
    {
        await new WaitForSeconds( 1f );
        Debug.Log( "ピカチュウ" );
    }
}

AsyncAwaitUtil.unitypackage に含まれている AsIEnumerator 関数を使用すると、
コルーチンから非同期メソッドを呼び出すことができます

複数のスレッド

async / await を使用して複数のスレッドを実行することもできます
最初の方法は、ConfigureAwait 関数を使うことです

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private async void Start()
    {
        // ここは Unity のスレッドなので、
        // Unity の API を実行できる
        var go1 = new GameObject();

        await Task
            .Delay( TimeSpan.FromSeconds( 1f ) )
            .ConfigureAwait( false )
        ;

        // ここは別のスレッドなので、
        // Unity の API を実行するとエラーが発生する
        var go2 = new GameObject();
    }
}

ConfigureAwait 関数以下のコードは Unity スレッドではなく
別のスレッドで動作するようになるため、
Unity API を呼び出せないことに注意してください

AsyncAwaitUtil.unitypackage を使用する場合は、
次のようにすることもできます

using System;
using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private async void Start()
    {
        // ここは Unity のスレッドなので、
        // Unity の API を実行できる
        new GameObject();

        await new WaitForBackgroundThread();

        // ここは別のスレッドなので、
        // Unity の API を実行するとエラーが発生する
        new GameObject();

        await new WaitForUpdate();

        // ここは Unity のスレッドなので、
        // Unity の API を実行できる
        new GameObject();
    }
}

WaitForBackgroundThread で新しいスレッドを開始し、
WaitForUpdate で Unity のスレッドに戻ることができます

UniRx と非同期ロジックの連携

f:id:baba_s:20180506113402p:plain

UniRx を使用しているプロジェクトであれば、
AsyncAwaitUtil.unitypackage に含まれている「UniRx.zip」を展開すると、
下記のようなコードを実装できるようになります

using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    public Button m_button;

    private async void Start()
    {
        await m_button.OnClickAsObservable();
        Debug.Log( "ボタンが押された" );
    }
}