Unityにおける非同期処理入門(async/await)

広告

コルーチンを卒業して、モダンな C# の非同期処理を使いこなそう


TL;DR

  • async/await は C# 標準の非同期処理構文で、Unity でも利用できる。
  • Task や UniTask を返すメソッドに await を書くことで「待ち時間」を自然に記述できる。
  • コルーチンよりエラーハンドリングキャンセル処理が書きやすい。
  • ただし Unity の API 操作(GameObject の生成・破棄など)は メインスレッドでのみ 実行できる点に注意が必要。

1. async/await とコルーチンの違い

コルーチンは Unity 独自の仕組みですが、async/await は C# 言語の標準機能 です。Unity 2017 以降で使えます。

大きな違いをまとめると次のとおりです。

比較項目コルーチンasync/await
戻り値の型IEnumeratorasync Task / async UniTask
待機の書き方yield return new WaitForSeconds(3)await Task.Delay(3000)
例外のキャッチ困難(try-catch が使いにくい)try-catch が普通に使える
キャンセル処理StopCoroutine()CancellationToken で制御
呼び出し元への値の返却不可Task<T> で返せる

2. イラストで理解する「処理の流れ」

通常のメソッド:

Start()
 ├─処理A
 ├─処理B
 └─処理C   ← 一気に実行

async/await を使った場合:

StartAsync()
 ├─処理A
 ├─await Task.Delay(3000)   // 3秒間待機(メインスレッドをブロックしない)
 │
 ├─処理B(3秒後に再開)
 ├─await Task.Yield()       // 1フレーム待機
 │
 └─処理C(次フレームで再開)

➡ コルーチンと同様に「一度止まって、後から再開できる」のがポイントです。


3. 基本コード例

コルーチン版と async/await 版を並べて比較します。

コルーチン版:

using System.Collections;
using UnityEngine;

public class CoroutineSample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(SampleCoroutine());
    }

    IEnumerator SampleCoroutine()
    {
        Debug.Log("処理開始");

        yield return new WaitForSeconds(3);
        Debug.Log("3秒後の処理");

        yield return null;
        Debug.Log("次のフレームで処理");
    }
}

async/await 版:

using System.Threading.Tasks;
using UnityEngine;

public class AsyncSample : MonoBehaviour
{
    async void Start()
    {
        await SampleAsync();
    }

    async Task SampleAsync()
    {
        Debug.Log("処理開始");

        await Task.Delay(3000);          // 3秒待つ(ミリ秒指定)
        Debug.Log("3秒後の処理");

        await Task.Yield();              // 1フレーム待つ
        Debug.Log("次のフレームで処理");
    }
}

ポイント: Start() に async void と書くことで、メソッド内で await が使えるようになります。


4. よく使う await の書き方

書き方意味
await Task.Delay(3000)3秒待つ(ミリ秒指定)
await Task.Yield()1フレーム待つ
await Task.WhenAll(taskA, taskB)複数のタスクがすべて完了するまで待つ
await Task.WhenAny(taskA, taskB)いずれかのタスクが完了したら進む

補足: WaitUntil / WaitWhile 相当の処理はループで書きます(後述)。


5. サンプルシーンで学ぶ

コルーチン版の OnboardingSequence を async/await で書き直したサンプルです。

シーン構成例

  • Main Camera
  • Canvas
    • UI → Text – TextMeshPro(UI: ログ表示用)
  • GameController(Script付き)

スクリプト例(基本版)

using System.Threading.Tasks;
using UnityEngine;
using TMPro;

public class GameController : MonoBehaviour
{
    public TextMeshProUGUI logText;

    async void Start()
    {
        await OnboardingSequence();
    }

    async Task OnboardingSequence()
    {
        logText.text = "ゲーム開始!";

        await Task.Delay(2000);
        logText.text = "敵が現れた!";

        await Task.Delay(3000);
        logText.text = "プレイヤーのターン!";
    }
}

➡ 実行すると UI テキストが時間差で切り替わります。コルーチン版とほぼ同じ見た目のコードで書けます。


6. 条件待ちの書き方

コルーチンの WaitUntil に相当する処理は、while ループで代替します。

// コルーチン版
yield return new WaitUntil(() => isEnemyDefeated);

// async/await 版
while (!isEnemyDefeated)
{
    await Task.Yield();   // 1フレームごとに条件をチェック
}

7. キャンセル処理(CancellationToken)

async/await の強みのひとつが、キャンセル処理を明示的に書けることです。
GameObject が破棄されたときに処理を止めたい場合に活用します。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using TMPro;

public class GameController : MonoBehaviour
{
    public TextMeshProUGUI logText;

    private CancellationTokenSource _cts;

    async void Start()
    {
        _cts = new CancellationTokenSource();
        await OnboardingSequence(_cts.Token);
    }

    async Task OnboardingSequence(CancellationToken token)
    {
        logText.text = "ゲーム開始!";

        await Task.Delay(2000, token);
        logText.text = "敵が現れた!";

        await Task.Delay(3000, token);
        logText.text = "プレイヤーのターン!";
    }

    void OnDestroy()
    {
        _cts?.Cancel();   // オブジェクト破棄時にキャンセル
        _cts?.Dispose();
    }
}

ポイント: OnDestroy でキャンセルを呼ぶことで、シーン遷移時などに処理が残り続けるバグを防げます。


8. エラーハンドリング

コルーチンでは難しかった try-catch が、async/await では自然に書けます。

async Task LoadDataAsync()
{
    try
    {
        await Task.Delay(1000);
        // ここで例外が発生してもキャッチできる
        throw new System.Exception("通信エラー!");
    }
    catch (System.Exception e)
    {
        Debug.LogError($"エラー: {e.Message}");
    }
}

9. 注意点

① Unity の API はメインスレッドで呼ぶ

Task.Run() でバックグラウンドスレッドに処理を逃がすことができますが、その中から transform.position などの Unity API を呼ぶとエラーになります。重い計算だけをバックグラウンドに逃がし、Unity API の操作はメインスレッドに戻してから行いましょう。

// NG例(バックグラウンドスレッドから Unity API を触る)
await Task.Run(() =>
{
    transform.position = Vector3.zero; // エラー!
});

// OK例(計算はバックグラウンド、Unity API はメインスレッドで)
var result = await Task.Run(() => HeavyCalculation());
transform.position = new Vector3(result, 0, 0); // await の後はメインスレッド

② async void は原則 Start/イベントハンドラのみ

async void はエラーが握りつぶされやすいため、内部的に呼ぶメソッドは async Task で書き、await で呼び出す設計にしましょう。

③ UniTask の活用を検討する

Unity 向けにチューニングされた UniTask(外部パッケージ)を使うと、WaitForSeconds 相当が書けたり、パフォーマンスが改善したりします。本格的に使うなら導入を検討してください。

// UniTask 版(パッケージ導入後)
await UniTask.Delay(TimeSpan.FromSeconds(3));
await UniTask.WaitForSeconds(3);  // Time.deltaTime 基準

まとめ

async/await は C# 標準のモダンな非同期処理構文 で、コルーチンより読みやすく、エラー処理もキャンセルも柔軟に扱えます。

場面推奨
シンプルな演出・時間制御どちらでも OK
エラーハンドリングが必要async/await
キャンセル処理が必要async/await
値を返したいasync/awaitTask<T>
Unity 2017 未満コルーチン

コルーチンを完全に置き換える必要はありませんが、新しく書くコードには async/await を積極的に使うことで、保守性の高いゲームコードが書けるようになります。


関連記事:Unityにおけるコルーチン入門

訪問数 20 回, 今日の訪問数 20回

広告

Unity

Posted by hidepon