初めに
これは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に反映する
- 戻るボタンでキャンセルできる
シーケンス図
コード例
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行表示して少し待つ
- フェードアウトして次へ
シーケンス図
コード例
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)
やりたい事
- 通信と演出を並列で走らせる
- 通信が遅い場合は諦める(タイムアウト)
- どちらかが失敗した場合は終了
シーケンス図
コード例
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遷移に当てはめてみると、コードの安定感が一段上がるかもです。


