Unity async/awaitで非同期を書く

## はじめに
Task、async/awaitの話をする際、非同期とスレッドを切り分けて説明した方がスッキリします。

ここでは非同期に絞って紹介します。

unity 2018.1以降、C＃6が正式にサポートされるようです。

[Updated scripting runtime in Unity 2018.1: What does the future hold? – Unity Blog](https://blogs.unity3d.com/jp/2018/03/28/updated-scripting-runtime-in-unity-2018-1-what-does-the-future-hold/)

業務ではUnity2017を使用していますが、一足先にasync/awaitを使っています。

unity2017で async/awaitを使うには PlayerSettings > Scripting Runtime Version を .NET 4.6 Equivalent にする必要があります。


## これからの説明で実行する処理
ゲーム制作でありそうな例です。

1. 読み込み中の表示を始める
2. masterデータを読む
3. masterデータを元に3Dモデルを読む
4. userデータとマップデータを同時に読む
5. 読み込み中の表示を終了する

これらを上から順に実行します。


## callbackを使った非同期

async/awaitもCoroutineも使わない場合、callbackを使う事になります。


下記のようにネストが深くなりがちです。
ネストを嫌ってメソッドを分割すると、今度は実行順と記述順がバラバラになり、順番の把握が難しくなります。


callbackが大量に連鎖する場合、callback地獄と呼ばれるような事になり、非常に読みづらくメンテしにくいコードになってしまいます。

```c#
private void Test(Action onEnd)
{
    // 1. 読み込み中の表示を始める
    ShowLoader(
        // 2. masterデータを読む
        LoadMasterData((res) =>
        {
            // 3. masterデータを元に3Dモデルを読む
            Load3DModel(res, () =>
            {
                // 4. userデータとマップデータを同時に読む
                LoadUserDataAndMapData(() =>
                {
                    // 5. 読み込み中の表示を終了する
                    HideLoader(onEnd);
                });
            });
        });
    );
}

private void LoadUserDataAndMapData(Action callback)
{
	// ... ユーザーデータとマップデータを同時に読む処理。両方終わったらcallback実行
}
```


## async/await で同期のように非同期を書く
このように記述できます。

```c#
private async void Test()
{
    // 1. 読み込み中の表示を始める
    await ShowLoaderAsync();
	
    // 2. masterデータを読む
    int res = await LoadMasterDataAsync();
	
    // 3. masterデータを元に3Dモデルを読む
    await Load3DModelAsync(res);

    // 4. userデータとマップデータを同時に読む
    await Task.WhenAll(new Task[]{LoadUserDataAsync(), LoadMapDataAsync()});
	
    // 5. 読み込み中の表示を終了する
    await HideLoaderAsync();
}
```

記述した順に実行されるため、非常に読みやすいものになります。
コールバック関数もなく、ネストも必要ありません。


## Coroutineの置き換え
同期風に書くのはCoroutineでも可能です。

Coroutineにも色々な使い方があると思いますが、ここで扱うのは同期風に書く用途でのCoroutineの話です。

しかしCoroutineには下記のような欠点があります。

## Coroutineの欠点
 - 値を返すのが面倒
	- 型変換をして取得する事は可能
	- または、参照を渡す等
 
 - ry-catch内にyieldを入れることはできないので、例外を処理することが出来ない
 - 例外が発生した場合、スタックトレースは例外がスローされたCoroutineを通知するだけ
 - 何もしなくても、yield returnしたら 1フレーム待つ


```c#
private IEnumerator CoroutineTest()
{
	UnityEngine.Debug.Log("Coroutine before: " + Time.frameCount);
	yield return StartCoroutine(CoroutineA());
	// 1フレーム経過している
	UnityEngine.Debug.Log("Coroutine after: " + Time.frameCount);
}

private IEnumerator CoroutineA()
{
	if (false)
	{
		yield return  null;
	}
}
```
 
 - delegateで実行出来ない

```c#
// NG. deleagte内ではyield return出来ない
Func<IEnumerator> callback = () =>
{
	yield return null;
};
StartCoroutine(callback());
```

 - callback形式のメソッドをCoroutineに対応させるのが面倒

 
async/awaitは上記をカバーできます。

## Coroutineをasync/awaitに対応させる

Unity3dAsyncAwaitUtilというユーティリティが公開されています。

[https://github.com/svermeulen/Unity3dAsyncAwaitUtil](https://github.com/svermeulen/Unity3dAsyncAwaitUtil)

[例えばここ。](https://github.com/svermeulen/Unity3dAsyncAwaitUtil/blob/master/UnityProject/Assets/Plugins/AsyncAwaitUtil/Tests/AsyncUtilTests.cs#L129-L133)

```c#
async Task RunMultipleThreadsTestAsyncWait()
{
    UnityEngine.Debug.Log("RunMultipleThreadsTestAsyncWait1");
    await new WaitForSeconds(1.0f);
    UnityEngine.Debug.Log("RunMultipleThreadsTestAsyncWait2");
}
```

yield return する箇所を、そのまま awaitで置き換え可能です。
これにより、過去に作成したメソッドをそのまま活用でき、置き換えが容易になります。

## 既存クラスをasycn/awaitに対応させる
### [DOTween](http://dotween.demigiant.com/)でやってみる


拡張メソッドを使い、既存クラスも簡単にawaitに対応させる事が可能です。

[非同期メソッドの内部実装 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C ](http://ufcpp.net/study/csharp/sp5_awaitable.html)

下記拡張メソッドを用意します。

```c#
// TweenerをAwaitableにする
public static TaskAwaiter<bool> GetAwaiter(this Tweener tweener)
{
    var tcs = new TaskCompletionSource<bool>();

    TweenCallback callback = null;
    callback = () =>
    {
        tweener.onComplete -= callback;
        tcs.SetResult(true);
    };
    tweener.onComplete += callback;

    return tcs.Task.GetAwaiter();
}

// SequenceをAwaitableにする
public static TaskAwaiter<bool> GetAwaiter(this Sequence seq)
{
    var tcs = new TaskCompletionSource<bool>();

    TweenCallback callback = null;
    callback = () =>
    {
        seq.onComplete -= callback;
        tcs.SetResult(true);
    };
    seq.onComplete += callback;

    return tcs.Task.GetAwaiter();
}
```

これにより

```c#
await image.rectTransform.DOScale(new Vector3(0, 1, 1), 1f);

var seq = DOTween.Sequence();
seq.Append(_img.DOFade(1, 1f));
seq.Append(_img.DOColor(Color.black, 0.5f));
seq.Append(_img.rectTransform.DOScale(new Vector3(2, 2, 2), 0.5f));
seq.Append(_img.rectTransform.DOScale(new Vector3(1, 1, 1), 0.5f));
seq.OnComplete(() =>
{
    Debug.Log("DOTween.Sequence end in OnComplete");
});

await seq;
```

と記述できるようになります。

(実際はTweenerとSequence同時に使うとコンパイラのwarningが出てしまうため、もうちょっと工夫する必要があります。)

## 注意点
Taskを返しているのに awaitしていない場合、処理中に例外が起きてもunity editorのlogに出力されません。

```c#
// log出ない例
private async Task ExceptionTest()
{
	Image img = null;
	img.gameObject.SetActive(true);
}

ExceptionTest();
```

awaitしない場合、awaitするか、戻り値を async void にする必要があります。

```c#
// log出る
private async void ExceptionTest()
{
	Image img = null;
	img.gameObject.SetActive(true);
}

ExceptionTest();
```

これはコンパイラがwarningで指摘してくれます。

```
[CS4014] The statement is not awaited and execution of current method continues before the call is completed. Consider using `await' operator or calling `Wait' method
```

## Destroy･非アクティブにされてもCoroutineのように止まらない
これは良し悪しありますが、Coroutineと同じように止めたい場合は面倒です。

## 他の気になる点
### デバッグ
C# Riderを使用していますが、問題なくブレークポイントを張りデバッグ出来ています。

## 終わりに
他にはチュートリアルなど、順に実行されるものなどが楽に記述できそうな予感がします。

### 参考URL

下記blog記事を非常に参考にしています。この記事の半分は下記blogの紹介です。

[How to use Async-Await instead of coroutines in Unity3d 2017 | Steve Vermeulen](http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/)


### その他参考URL

[[雑記] 非同期制御フロー - C# によるプログラミング入門 | ++C++; // 未確認飛行 C](http://ufcpp.net/study/csharp/misc_asyncflow.html)

[非同期メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C](http://ufcpp.net/study/csharp/sp5_async.html)

[async/await と SynchronizationContext (2) - 鷲ノ巣](http://tech.blog.aerie.jp/entry/2015/09/17/110425)