はじめに

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

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

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

Updated scripting runtime in Unity 2018.1: What does the future hold? – Unity Blog

業務では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地獄と呼ばれるような事になり、非常に読みづらくメンテしにくいコードになってしまいます。

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 で同期のように非同期を書く

このように記述できます。

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フレーム待つ

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で実行出来ない
// 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

例えばここ。

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

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

既存クラスをasycn/awaitに対応させる

DOTweenでやってみる

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

非同期メソッドの内部実装 - C# によるプログラミング入門 | ++C++; // 未確認飛行 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();
}

これにより

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に出力されません。

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

ExceptionTest();

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

// 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

その他参考URL

[雑記] 非同期制御フロー - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

非同期メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

async/await と SynchronizationContext (2) - 鷲ノ巣

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.