はじめに
http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/
この記事は、上記のページで公開されている
Unity 2017 でコルーチンの代わりに
async / await を使用する方法を翻訳したものになります
目次
- はじめに
- 目次
- コルーチン
- async / await
- 簡単な例
- Custom Awaiters
- AsyncAwaitUtil.unitypackage
- 例外処理
- コルーチンから非同期メソッド呼び出し
- 複数のスレッド
- UniRx と非同期ロジックの連携
コルーチン
コルーチンは便利ですが、いくつかの欠点があります
- コルーチンは値を返すことができません
そのため、Action 型のコールバックを引数で渡すなどの対応が必要になります
- try-catch 内に yield を入れることはできないので、例外を処理できません
また、例外が発生した場合、スタックトレースには
例外が投げられたコルーチンが通知されるだけなので、
呼び出し元を推測する必要があります
async / await
Unity 2017 では async / await という新しい C# の機能が使えるようになりました
これは、コルーチンと比べてたくさんの素晴らしい機能が用意されています
この機能を有効にするには、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 と非同期ロジックの連携
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( "ボタンが押された" ); } }