0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UniTaskで“待つ処理”を設計する:キャンセル/並列/タイムアウトの基本形

Last updated at Posted at 2025-12-15

初めに

これはAizu Advent Calendar 2025の14日目の記事です。下記リンクから他の投稿も是非ご覧頂ければ🙏(投稿日...🤨)
https://adventar.org/calendars/12305

1. Unityで“待つ処理”が増えると何が辛いか

ゲーム開発では、次のような「待つ処理」が自然に増えます。

  • ロード待ち(Addressables / Scene / AssetBundle)
  • 演出待ち(フェード、SE、Timeline、アニメ再生)
  • 入力待ち(ボタン、タップ、決定キー)
  • 通信待ち(ランキング、ログイン、データ取得)

これらを場当たり的に繋ぐと、だいたい辛くなります。

  • 「処理の流れ」がバラけて、状態遷移が追いづらい
  • Coroutine がネストして、読むのがつらい
  • オブジェクト破棄後にも処理が走って、MissingReference や例外が出る
  • 中断ができず、戻るボタンが効かない / シーン遷移で暴発する

2. 非同期処理の利点(ゲーム開発目線)

「非同期=速くなる」ではなく、ゲームではだいたい 体験と設計が良くなる のが本質です。

2.1 フレームを止めない(体感が良くなる)

重い処理や待ち時間が入っても、描画や入力を止めずに進められるので、

  • ロード中でもUIが動く
  • 入力キャンセルが効く
  • 演出が滑らか

2.2 流れを“直列”に書ける(理解しやすい)

本当は「Aして、終わったらBして、終わったらC」という直列フローなのに、
コルーチンやコールバックでバラけると読みづらいです。
await で直列に書けると、ロジックが文章っぽくなる

2.3 中断できる(シーン遷移・破棄に強い)

ゲームの「待つ処理」は、最後まで走り切らないことが多いです。

  • ロード中に戻る
  • スキップ
  • シーン遷移でオブジェクト破棄
  • ネットワーク不調で諦める

だから設計として キャンセル前提 にしておくと事故が減ります。


3. UniTaskを使うと何が嬉しいか(Unity向けに“ちょうどいい”)

UniTaskを導入すると、Unityの「待つ処理」を次の形で揃えやすいです。

  • CancellationToken を基準に中断を統一できる
  • WhenAll/WhenAny で並列が自然に書ける
  • Timeout を一貫して入れられる
  • Update を自前で書かずに「待ち」を表現できる(Delay, Yield, WaitUntil など)

注意:UniTaskの導入方法(パッケージ追加やusingなど)はこの記事では最小限にして、
「どう設計するか」のパターンに集中します。


4. 事例:基本形3つ(キャンセル / 並列 / タイムアウト)

ここからは「待つ処理」を (1)キャンセルできる(2)並列にできる(3)タイムアウトで諦められる の3パターンに分けて、テンプレとして紹介します。

4.1 例1:ロード + プログレス表示 + キャンセル

やりたいこと

  • ロードを進める
  • 進捗をUIに反映する
  • 戻るボタンでキャンセルできる

シーケンス図

Sequence1.png

コード例

using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class LoadingPresenter : MonoBehaviour
{
    [SerializeField] private UnityEngine.UI.Slider progressBar;

    private CancellationTokenSource _cts;

    private void OnEnable()
    {
        _cts = new CancellationTokenSource();
        RunAsync(_cts.Token).Forget();
    }

    private void OnDisable()
    {
        _cts?.Cancel();
        _cts?.Dispose();
    }

    public void OnClickCancel()
    {
        _cts?.Cancel();
    }

    private async UniTaskVoid RunAsync(CancellationToken ct)
    {
        try
        {
            // 擬似ロード:本来は Addressables や Scene のロード処理などに置き換える
            float p = 0f;
            while (p < 1f)
            {
                ct.ThrowIfCancellationRequested();

                p += 0.02f;
                progressBar.value = p;

                await UniTask.Yield(PlayerLoopTiming.Update, ct);
            }

            // ロード完了後の遷移など
            Debug.Log("Load Complete!");
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Load Canceled");
        }
    }
}
  • キャンセルに入口を二つ持つ
    ユーザー操作(Cancelボタン)と、画面破棄(OnDisable)の両方から同じCTS.Cancel()に流す。
  • UniTaskVoid + Forget() の注意点
    便利ではあるのですが例外が見えづらい箇所でもあるので、実際はtry/catchを徹底する、又は 例外ログの方針を決めるのがいいと思います。

4.2 例2:入力待ち + 演出待ち(ゲーム進行の直列フロー)

やりたいこと

  • フェードイン
  • 「Press Any Key」待ち
  • 会話1行表示して少し待つ
  • フェードアウトして次へ

シーケンス図

Sequence2.png

コード例

using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine;

public class SimpleSequence : MonoBehaviour
{
    [SerializeField] private CanvasGroup fade;
    [SerializeField] private GameObject pressAnyKey;

    private async UniTask Start()
    {
        var ct = this.GetCancellationTokenOnDestroy();

        await Fade(1f, 0f, 0.4f, ct);
        pressAnyKey.SetActive(true);

        await UniTask.WaitUntil(() => Input.anyKeyDown, cancellationToken: ct);

        pressAnyKey.SetActive(false);
        Debug.Log("Dialogue: Hello!");

        await UniTask.Delay(800, cancellationToken: ct);
        await Fade(0f, 1f, 0.4f, ct);
    }

    private async UniTask Fade(float from, float to, float sec, CancellationToken ct)
    {
        fade.alpha = from;

        float t = 0f;
        while (t < sec)
        {
            ct.ThrowIfCancellationRequested();

            t += Time.deltaTime;
            fade.alpha = Mathf.Lerp(from, to, t / sec);

            await UniTask.Yield(PlayerLoopTiming.Update, ct);
        }

        fade.alpha = to;
    }
}

解説:UniTaskの強みが一番出るやつ

  • “ゲームの進行”は本質的に直列
    「演出→入力→演出→次へ」は、まさに文章のように上から読むべきロジック。
    コルーチンで分岐・ネストが増えるほど追いづらくなるので、await が効きます。 :contentReference[oaicite:10]{index=10}
  • GetCancellationTokenOnDestroy() が保険として強い
    シーン遷移や破棄で「入力待ちが残り続ける」事故を根本から潰せます。
  • “待ち”の種類を揃えると設計が楽になる
    • 入力待ち:WaitUntil
    • 演出待ち:Delay
    • フレーム更新:Yield(Update)
      これらを揃えておくと、会話・チュートリアル・UI導線が爆速で組めます。

例3:並列(WhenAll) + タイムアウト(Timeout)

やりたい事

  • 通信と演出を並列で走らせる
  • 通信が遅い場合は諦める(タイムアウト)
  • どちらかが失敗した場合は終了

シーケンス図

Sequence3.png

コード例

using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;

public class ParallelAndTimeout : MonoBehaviour
{
    private async UniTask Start()
    {
        var ct = this.GetCancellationTokenOnDestroy();

        try
        {
            await UniTask.WhenAll(
                Fetch(ct).Timeout(TimeSpan.FromSeconds(2), cancellationToken: ct),
                PlayLoadingAnim(ct)
            );

            Debug.Log("Both completed!");
        }
        catch (TimeoutException)
        {
            Debug.LogWarning("Network timeout! (show retry UI)");
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Canceled");
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }

    private async UniTask Fetch(CancellationToken ct)
    {
        // 擬似通信:本来は UnityWebRequest 等に置き換える
        await UniTask.Delay(3000, cancellationToken: ct);
    }

    private async UniTask PlayLoadingAnim(CancellationToken ct)
    {
        // くるくる回す演出など(キャンセル対応しておく)
        await UniTask.Delay(2000, cancellationToken: ct);
    }
}

解説

  • 最初から何秒で諦めるかを決める
    Fetch(ct).Timeout(TimeSpan.FromSeconds(2), ct) の 2秒が仕様で、UI側も「2秒でリトライ表示」に合わせて設計できる
  • 通信のみをタイムアウトし、演出は継続する
    演出は PlayLoadingAnim(ct) として別タスクにし、UXを保ちつつ通信だけ見切る
  • 並列処理こそキャンセルが重要
    GetCancellationTokenOnDestroy() で止め、閉じた後に「成功ログ」などが走る事故を防ぐ

5.ありがちな事例と、この記事で紹介したことで潰せること

  • 事例1 : シーン遷移後も「待ち」が残る
    →OnDisable/OnDestroy をキャンセル起点にする(例1/例2)。
  • 事例2 : 入力待ちが残ってUIが破壊されてしまう
    →WaitUntil(..., ct) にする
  • 事例3 : 通信が遅すぎてゲームの状態がなかなか移らない
    →タイムアウトを仕様化する(例3)。
  • 事例4 : 並列にして止めどころ(管理者)が分からなくなる
    → 「誰がキャンセルを持つか」を最初に決める。

まとめ

ゲーム開発における「待つ処理」は、最後まで走り切ることよりも、途中で中断されたり分岐したりすることのほうが多いです。ロード中に戻る、入力待ちの途中でUIを閉じる、通信が遅くて諦める、といった状況は日常的に起きます。だからこそ、待ち処理を「いつでもキャンセルされ得るもの」として設計しておくことが重要になります。

UniTaskは単なる高速化の道具というより、「待つ・やめる・諦める」を仕様として扱い、ゲームの手触りと保守性を同時に上げるための道具です。まずはこの記事で紹介したキャンセル/並列/タイムアウトの基本形を自分のプロジェクトのロードやUI遷移に当てはめてみると、コードの安定感が一段上がるかもです。

0
4
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
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?