LoginSignup
30
34

More than 5 years have passed since last update.

Unity async/awaitで非同期を書く

Last updated at Posted at 2018-03-30

はじめに

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) - 鷲ノ巣

30
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
34