はじめに
Unity 2018からついに.NET 4.6がstableになり、async/await
を使いこなせるようになっておく必要がでてきました。
今回は個人的な練習として、Unityのコルーチンをasync/await
で待機できるようにしてみました。
そのまえに:UniRxを導入すれば全部できるよ
実はこれから紹介するような実装をわざわざする必要はありません。
UniRx
を導入すればいい感じにコルーチンやAsyncOperation
をawait
できるようになります。
using System;
using System.Collections;
using UnityEngine;
using UniRx;
class LoadTextureTest : MonoBehaviour
{
async void Start()
{
var request = Resources.LoadAsync<Texture>("texture/player");
await request; //テクスチャの読み込み待機
GetComponent<Renderer>().material.mainTexture = request.asset as Texture;
}
}
using System;
using System.Collections;
using UnityEngine;
using UniRx;
class CoroutineAwaitTest: MonoBehaviour
{
async void Start()
{
await MoveCoroutine(Vector3.forward, 1.0f, 1.0f);
await MoveCoroutine(Vector3.right, 1.5f, 2.0f);
await MoveCoroutine(Vector3.back, 2.0f, 5.0f);
}
/// <summary>
/// 指定した方向に指定秒数移動する
/// </summary>
IEnumerator MoveCoroutine(Vector3 direction, float speed, float seconds)
{
var start = Time.time;
while (Time.time - start <= seconds)
{
transform.position += direction * speed * Time.deltaTime;
yield return null;
}
}
}
今回の記事は「自分でawaitable
なオブジェクトを作るにはどうしたらいいのか」を紹介するのが目的のため、UniRxは使いません。
前提「 await
を使えるようにするためには」
あるオブジェクトをawait
できるようにするためには、次の2つを実装する必要があります。
-
INotifyCompletion
の実装およびIsCompleted
とGetResult
を定義した「待機用オブジェクト」 - 「待機用オブジェクトを返すGetAwaiter()メソッド」
逆に言えば、この2つさえ実装してあればどんなものもawait
することができるようになります。
コルーチンの終了をawait
できるようにしてみる
まずはシンプルな例として、「結果を返さないコルーチン」の終了をawait
で待機できるようにしてみます。
1. コルーチンを実行するGameObjectを用意する
コルーチンを処理するためにはなにかしらアクティブなGameObject
がシーン上に必要になります。
今回は予めシーンにGameObjectを用意しておくことにします。
名前は「MainThreadDispatcher
」という名前にしておきます。
(中身は空でいいのでとりあえずGameObjectだけ用意しておく)
2. コルーチンを実際に処理するComponent
を定義する
コルーチンを実際に処理して、その終了を通知することができるComponent
を作ります。
名前はGameObject名に合わせて「MainThreadDispatcher
」にします。
using System;
using System.Collections;
using UnityEngine;
public class MainThreadDispatcher : MonoBehaviour
{
private static MainThreadDispatcher instance;
public static MainThreadDispatcher Instance
{
get
{
if (instance == null)
{
//とりあえず雑に
instance = GameObject.FindObjectOfType<MainThreadDispatcher>();
}
return instance;
}
}
/// <summary>
/// 登録されたコルーチンを実行する
/// </summary>
/// <param name="coroutine">対象のコルーチン</param>
/// <param name="callback">終了時のコールバック</param>
public void RegisterCoroutione(IEnumerator coroutine, Action callback)
{
StartCoroutine(WorkCoroutine(coroutine, callback));
}
/// <summary>
/// コルーチンを実行し、終了時にコールバック関数を実行する
/// </summary>
private IEnumerator WorkCoroutine(IEnumerator target, Action callback)
{
yield return target;
callback();
}
}
そしてこれを先程のGameObjectに貼り付けておきます。
3. INotifyCompletion
の実装およびIsCompleted
とGetResult
を定義した「待機用オブジェクト」を定義する
await
時に呼び出される待機用のオブジェクトを定義します。
内容としてはコルーチンをコンストラクタで受け取り、INotifyCompletion.OnCompleted
内でごねごねする形になります。
/// <summary>
/// コルーチンの待機を補助するオブジェクト
/// </summary>
public class AwaitableEnumerator : INotifyCompletion
{
/// <summary>
/// 対象となるコルーチン
/// </summary>
private readonly IEnumerator target;
/// <summary>
/// 非同期処理が完了しているか
/// </summary>
public bool IsCompleted { get; private set; }
/// <summary>
/// 結果のオブジェクトを取得する場合に利用する
/// 今回は結果の値がないのでvoid
/// </summary>
public void GetResult()
{
}
/// <summary>
/// コンストラクタ
/// </summary>
public AwaitableEnumerator(IEnumerator coroutine)
{
target = coroutine;
}
/// <summary>
/// await開始時に実行される関数
/// </summary>
/// <param name="continuation">「await以降の処理」を扱うAction</param>
public void OnCompleted(Action continuation)
{
// awaitが開始されたらコルーチンを実行する
MainThreadDispatcher.Instance.RegisterCoroutione(target, () =>
{
// コルーチンが終了したらフラグを立てて、処理の継続を行う
IsCompleted = true;
continuation();
});
}
}
OnCompleted(Action continuation)
が最も重要なメソッドです。
await
が実行されたときにこのメソッドが呼び出されるため、ここに「awaitを終了する条件および継続処理の呼び出し」を記述します。
Action continuation
は「await以降の処理」を指し示すAction
です。そのため、非同期処理が終わったあとに処理を継続する場合は明示的にこのAction
を実行する必要があります。
今回はこのOnCompleted()
内でコルーチンを起動し、そのコールバックをもってawait
を終了するという形にします。
4. IEnumerator
からAwaitableEnumerator
に変換するGetAwaiter()
を定義する。
コルーチン(IEnumrator
)をawait
するためには、IEnumrator
にGetAwaiter()
メソッドが定義されている必要があります。
今回は拡張メソッドでこれを追加します。
public static class IEnumeratorExtension
{
public static AwaitableEnumerator GetAwaiter(this IEnumerator coroutine)
{
return new AwaitableEnumerator(coroutine);
}
}
4.実際にコルーチンをawait
で待機する
これで下準備は完了したため、実際に使ってみましょう。
次のようなスクリプトをオブジェクトに貼り付けて実行してみます。
class AwaitCoroutineSample : MonoBehaviour
{
async void Start()
{
// 前へ2秒進む
await MoveCoroutine(Vector3.forward, 2.0f);
// 右へ1秒進む
await MoveCoroutine(Vector3.right, 1.0f);
// 後ろへ3秒進む
await MoveCoroutine(Vector3.back, 3.0f);
}
/// <summary>
/// 指定した方向へ、指定した時間[秒]、移動する
/// </summary>
IEnumerator MoveCoroutine(Vector3 direction, float seconds)
{
var start = Time.time;
while (Time.time - start <= seconds)
{
transform.position += direction * Time.deltaTime;
yield return null;
}
}
}
このように、コルーチンをasync/await
で待ち受けることができるようになりました!
もっと簡単に定義するために「TaskCompletionSource
」を使う
今回は説明のためにAwaitable
パターンを自前ですべて実装しました。
ですがC#には非同期処理をTask
にラップしてしまう機能が用意されており、そちらを使うとAwaitableEnumerator
の定義すら不要になります。
というわけで、先程のGetAwaiter
メソッドをTaskCompletionSource
を使って書き換えると次のようになります。
public static class IEnumeratorExtension
{
public static TaskAwaiter<bool> GetAwaiter(this IEnumerator coroutine)
{
// TaskCompletionSourceを生成
// 非ジェネリック版のTaskCompletionSourceは存在しないので、便宜上boolで代用
var tcs = new TaskCompletionSource<bool>();
MainThreadDispatcher.Instance.RegisterCoroutione(coroutine, () =>
{
// コルーチン終了時に値を設定
tcs.TrySetResult(true);
});
return tcs.Task.GetAwaiter();
}
}
このように、TaskCompletionSource
を使えば簡単にコルーチンの処理をTask
に置き換えてawait
できるようになります。
コルーチンの実行結果を取り出せるようにする
では続いて、await
でコルーチンの実行結果を取得できるようにしてみましょう。
1. MainThreadDispatcherにメソッドを追加する
MainThreadDispatcher
に次のメソッドを追加します。
単にコルーチンを実行するだけのメソッドです。
/// <summary>
/// 登録されたコルーチンを実行する
/// </summary>
/// <param name="coroutine">対象のコルーチン</param>
public void RegisterCoroutione(IEnumerator coroutine)
{
StartCoroutine(coroutine);
}
2. statiメソッド経由でコルーチンからTaskに変換できるようにする
コルーチンから結果を取り出そうとした場合、コルーチン内で明示的にコールバックを呼び出す必要があります。
今回はコルーチンに直接TaskCompletionSource
を渡し、内部で結果を詰めてもらう仕組みにします。
そこで、そのようなコルーチンを簡単に生成できるようにするためのstaticメソッドを用意しておきます。
public static class Awaitable
{
public static Task<T> Create<T>(Func<TaskCompletionSource<T>, IEnumerator> creation)
{
var tcs = new TaskCompletionSource<T>();
// コルーチンを生成
var coroutine = creation(tcs);
MainThreadDispatcher.Instance.RegisterCoroutione(coroutine);
return tcs.Task;
}
}
Func<TaskCompletionSource<T>, IEnumerator>
は「TaskCompletionSource<T>
を引数にとり、IEnumerator
を戻り値とする関数」を意味します。
3. 実際にコルーチンを生成して利用する
以上で準備が整ったので、コルーチンをawaitしてみます。
さきほどのAwaitable.Create<string>()
を用いて、結果を返すコルーチンを生成し、それをTask<string>
に変換します。
using System;
using System.Collections;
using System.Threading.Tasks;
using Assets;
using UnityEngine;
public class AwaitCoroutineSample2 : MonoBehaviour
{
async void Start()
{
var url = "https://unity3d.com/jp/";
// コルーチンをawaitableにする
var awaitableCoroutine = Awaitable.Create<String>(tcs => GetCoroutine(url, tcs));
// await
var result = await awaitableCoroutine;
Debug.Log(result);
}
/// <summary>
/// WWWを使って通信して結果をStringで通知するコルーチン
/// </summary>
IEnumerator GetCoroutine(string url, TaskCompletionSource<string> taskCompletionSource)
{
var www = new WWW(url);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
taskCompletionSource.TrySetException(new Exception(www.error));
}
else
{
taskCompletionSource.TrySetResult(www.text);
}
}
}
(こんなことしなくても)
UniRx
を導入すれば全部できます。
使い方もだいたい同じです。
using System;
using System.Collections;
using UniRx;
using UnityEngine;
public class AwaitCoroutineSample3 : MonoBehaviour
{
async void Start()
{
var url = "https://unity3d.com/jp/";
// await
var result = await Observable.FromCoroutine<string>(observer => GetCoroutine(url, observer));
// 結果がとれる
Debug.Log(result);
}
/// WWWを使って通信して結果をStringで通知するコルーチン
/// </summary>
IEnumerator GetCoroutine(string url, IObserver<string> observer)
{
var www = new WWW(url);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
//失敗
observer.OnError(new Exception(www.error));
}
else
{
// 正常系
observer.OnNext(www.text);
observer.OnCompleted();
}
}
}
最後に
自前でAwaitable
パターンのインタフェースを満たしてしまえば、どんなものでもawait
できるようになります。
とくにTaskCompletionSource
を使うと非同期処理を簡単にTaskに変換してawaitable
にできるのでおすすめです。