現在のプロジェクトのコルーチン箇所をUniTaskに置き換えようと思い色々調べたので備忘録
メリット・デメリット
メリット
- 戻り値が使える
- 標準で動作中の関数を一覧出来るツール(UniTask Tracker)が付いている
- Update以外のタイミングで待てる
デメリット
- シーン遷移やGameObjectが削除されたりしても止まらず、途中で処理を止めるためのハンドリングが必要
基本文法
コルーチンの場合
// 関数側(関数の戻り値がIEnumratorになる)
public IEnumrator Init() {
// 待つ場合は yield return(自身で作った関数はStartCorutinが必要).
yield return StartCorutin(charaState.AddHp(50));
yield return null;
}
UniTask
// 関数側(関数の戻り値がasync UniTaskや async UniTaskVoidになる)
public async UniTask Init()
{
// 待つ場合は await(自身で作った関数もそのまま呼べる).
await charaState.AddHp(50);
await UniTask.Yield();
}
ちょっと応用
関数がいろんなオブジェクトについている場合
コルーチンの場合
適当な例なので処理が変かも
// シーン上のマネージャオブジェクトにアタッチ
public class MainScreenManager : MonoBehaviour
{
public CharaCtrl charaCtrl; // シリアライズでセット
private Void Start()
{
StartCorutin(charaCtrl.Init());
}
}
// ゲームオブジェクトAにアタッチ
public class CharaCtrl : MonoBehaviour
{
public CharaState charaState; // シリアライズでセット
public IEnumrator Init() {
yield return StartCorutin(charaState.AddHp(50));
yield return StartCorutin(Log());
}
private IEnumrator Log() {
// ログ処理を記載(ここでは省略)
yield return null;
}
}
// ゲームオブジェクトBにアタッチ
public class CharaState : MonoBehaviour
{
private int hp;
private void Start()
{
hp = 10;
}
public IEnumrator AddHp(int value){
int addVal = value / 5;
for(int i = 0; i < 5; i++){
hp += addVal;
yield return null;
}
}
}
UniTaskの場合
// シーン上のマネージャオブジェクトにアタッチ
public class MainScreenManager : MonoBehaviour
{
public CharaCtrl charaCtrl; // シリアライズでセット
private async UniTaskVoid Start()
{
await charaCtrl.Init();
}
}
// ゲームオブジェクトAにアタッチ
public class CharaCtrl : MonoBehaviour
{
public CharaState charaState; // シリアライズでセット
public async UniTask Init()
{
await charaState.AddHp(50);
await Log();
}
private async UniTask Log()
{
// ログ処理を記載 (ここでは省略)
await UniTask.Yield(); // サンプルとして非同期処理
}
}
// ゲームオブジェクトBにアタッチ
public class CharaState : MonoBehaviour
{
private int hp;
private void Start()
{
hp = 10;
}
public async UniTask AddHp(int value)
{
int addVal = value / 5;
for (int i = 0; i < 5; i++)
{
hp += addVal;
await UniTask.Yield(); // フレームをまたぐ処理
}
}
}
並列処理の例
複数の非同期タスクを並列で実行したい場合は、UniTask.WhenAllやUniTask.WhenAnyを使用すると簡単に実現できます。
サンプルコード: 並列実行 (全タスク完了を待つ場合)
public async UniTask DoMultipleTasks(CancellationToken ct)
{
await UniTask.WhenAll(
TaskA(ct),
TaskB(ct),
TaskC(ct)
);
Debug.Log("全てのタスクが完了しました");
}
private async UniTask TaskA(CancellationToken ct)
{
await UniTask.Delay(1000, cancellationToken: ct);
Debug.Log("TaskA完了");
}
private async UniTask TaskB(CancellationToken ct)
{
await UniTask.Delay(2000, cancellationToken: ct);
Debug.Log("TaskB完了");
}
private async UniTask TaskC(CancellationToken ct)
{
await UniTask.Delay(3000, cancellationToken: ct);
Debug.Log("TaskC完了");
}
並列処理の活用シーン
- キャラクターアニメーションやエフェクトを同時に再生する
- サーバーリクエストを複数並行して送信する
- バトル中の複数の非同期ロジックを同時進行する
オブジェクトの破棄に対応するための方法と設計方針
メリデメで記載した通りコルーチンと違いオブジェクトが消えても処理が継続されるので明確にキャンセル処理が必要
シーン制御クラスや呼び出す側の関数でCancellationTokenを取得して利用することで対処できる
// シーン遷移やオブジェクト削除時にキャンセルされるトークンを取得
CancellationToken ct = this.GetCancellationTokenOnDestroy();
UniTaskの既存関数の場合
UniTask.Yieldやawait UniTask.DelayなどのUniTask側が用意している関数は引数にCancellationTokenを渡すといい感じにキャンセルを行ってくれる。
独自関数の場合
独自関数の場合はtry/catchで中断処理が必要
サンプル
public class MainScreenManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
// シーン遷移やオブジェクト削除時にキャンセルされるトークンを取得
CancellationToken ct = this.GetCancellationTokenOnDestroy();
try
{
await testObj1.InitA(ct); // InitAでキャンセルが発生
await testObj2.InitB(ct); // キャンセルされるとここには到達しない
}
catch (OperationCanceledException)
{
Debug.Log("SomeFunction: 処理がキャンセルされました");
}
}
}
// UniTask側が用意している待機処理などを使用する場合はCancellationTokenをわたしたら勝手にキャンセルしてくれる
public async UniTask InitA(CancellationToken ct)
{
try
{
Debug.Log("InitA: 開始");
await UniTask.Delay(1000, cancellationToken: ct); // ここでキャンセルされる
Debug.Log("InitA: 終了");
}
catch (OperationCanceledException)
{
Debug.Log("InitA: キャンセルされました");
throw; // 親に例外を伝播
}
catch (Exception ex)
{
Debug.LogError($"InitA: 予期せぬエラー - {ex.Message}");
throw; // 他の例外も再スロー
}
}
// 無限ループ系の非同期処理.
public async UniTask InitB(CancellationToken ct)
{
while (true)
{
// キャンセルされたら例外をスローする
ct.ThrowIfCancellationRequested();
// 何かの処理
Debug.Log("処理中...");
// 次のフレームまで待つ
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
while (!ct.IsCancellationRequested) // キャンセル状態をポーリング
{
// 何かの処理
Debug.Log("処理中...");
// 次のフレームまで待つ
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
}
// 無限ループ系の非同期処理.
public async UniTask InitC(CancellationToken ct)
{
// これでも行ける
while (!ct.IsCancellationRequested) // キャンセル状態をポーリング
{
// 何かの処理
Debug.Log("処理中...");
// 次のフレームまで待つ
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
}
キャンセル状態をポーリングするか、例外で中断するかはケースバイケース
- 無限ループ処理では、ct.ThrowIfCancellationRequested()で中断する方が明示的。
- 短い処理やチェック頻度が少ない場合はct.IsCancellationRequestedでOK。
共通で使用する末端の関数はCancellationTokenを受け取ってcatchした場合はthrowするようにしたほうがいいかも。
public async UniTask SomeFunction(CancellationToken ct)
{
try
{
await testObj1.InitA(ct); // 再スローされた例外をキャッチ
await testObj2.InitB(ct); // この行には到達しない
}
catch (OperationCanceledException)
{
Debug.Log("SomeFunction: 処理がキャンセルされました");
}
}
public async UniTask InitA(CancellationToken ct)
{
try
{
Debug.Log("InitA: 開始");
await UniTask.Delay(1000, cancellationToken: ct); // ここでキャンセルされる
Debug.Log("InitA: 終了");
}
catch (OperationCanceledException)
{
Debug.Log("InitA: キャンセルされました");
throw; // 例外を再スロー
}
}
catchだけしてthrowしないと当然ながら次の処理に行ってしまう。
public async UniTask SomeFunction(CancellationToken ct)
{
try
{
await testObj1.InitA(ct); // キャンセルされても例外がスローされない
await testObj2.InitB(ct); // 次に進む
}
catch (OperationCanceledException)
{
Debug.Log("SomeFunction: 処理がキャンセルされました");
}
}
public async UniTask InitA(CancellationToken ct)
{
try
{
Debug.Log("InitA: 開始");
await UniTask.Delay(1000, cancellationToken: ct); // ここでキャンセルされる
Debug.Log("InitA: 終了");
}
catch (OperationCanceledException)
{
Debug.Log("InitA: キャンセルされました");
// 例外を再スローしない
}
}
DontDestroyやシングルトンなオブジェクトの場合
こちらも他関数と同様に呼び出す関数側からCancellationTokenをもらってキャンセルを行う感じでいいかも
シーンごとにCancellationTokenSourceを持つ場合
// シーン全体を管理するマネージャ
public class SceneManager : MonoBehaviour
{
private CancellationTokenSource cts;
private void OnEnable()
{
cts = new CancellationTokenSource();
}
private void OnDisable()
{
cts.Cancel();
cts.Dispose();
}
public CancellationToken GetSceneCancellationToken()
{
return cts.Token;
}
}
// DontDestroyなオブジェクトの処理
public class GlobalManager : MonoBehaviour
{
public async UniTask RunTask(SceneManager sceneManager)
{
CancellationToken ct = sceneManager.GetSceneCancellationToken();
try
{
await DoSomethingAsync(ct);
}
catch (OperationCanceledException)
{
Debug.Log("処理がキャンセルされました");
}
}
}
DontDestroyOnLoadなオブジェクトやシングルトンなオブジェクトに専用のCancellationTokenSourceを持たせる
public class GlobalManager : MonoBehaviour
{
private CancellationTokenSource cts;
private void OnEnable()
{
cts = new CancellationTokenSource();
}
private void OnDisable()
{
cts.Cancel();
cts.Dispose();
}
public async UniTask RunTask(CancellationToken ct)
{
try
{
// グローバルなタスクを実行
await DoSomethingAsync(ct);
}
catch (OperationCanceledException)
{
Debug.Log("GlobalManager: 処理がキャンセルされました");
throw; // 再スローして他クラスにも伝播
}
}
public CancellationToken GetCancellationToken()
{
return cts.Token;
}
private async UniTask DoSomethingAsync(CancellationToken ct)
{
// タスクの処理
await UniTask.Delay(2000, cancellationToken: ct);
Debug.Log("GlobalManager: 処理完了");
}
}
UniTaskのなかでコルーチン関数を呼ぶ方法
ToUniTaskを使用することでUniTaskの関数内でコルーチン関数を実行できます。
CancellationTokenをToUniTaskに引数として渡すことでキャンセル処理もできます。
Transform character;
private async UniTaskVoid Test()
{
// キャンセル用トークンを取得
var ct = this.GetCancellationTokenOnDestroy();
try
{
// コルーチンをUniTaskでラップして呼び出し
await MoveCharacter(character, new Vector3(5, 0, 0), 2f).ToUniTask(cancellationToken: ct);
// UniTaskの攻撃処理を実行
await PerformAttack(character, ct);
Debug.Log("全ての処理が完了しました!");
}
catch (OperationCanceledException)
{
Debug.Log("処理がキャンセルされました!");
}
}
/// <summary>
/// キャラクターを移動させるコルーチン
/// </summary>
private IEnumerator MoveCharacter(Transform target, Vector3 destination, float duration)
{
Vector3 start = target.position;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
target.position = Vector3.Lerp(start, destination, elapsed / duration);
yield return null; // 次のフレームまで待つ
}
target.position = destination;
}
/// <summary>
/// 攻撃を行う非同期処理 (UniTask)
/// </summary>
private async UniTask PerformAttack(Transform attacker, CancellationToken ct)
{
Debug.Log("攻撃準備中...");
await UniTask.Delay(1000, cancellationToken: ct); // 1秒待機
Debug.Log($"{attacker.name}が攻撃しました!");
// 攻撃のエフェクトやロジックなどをここで記述
}
コルーチンの中でUniTask関数を呼ぶ場合
コルーチンの中でUniTask関数を呼ぶ場合はToCoroutine()を使用することで呼び出すことができます。
public Transform character; // キャラクターのTransform
private void Start()
{
// コルーチン開始
StartCoroutine(CharacterRoutine());
}
/// <summary>
/// キャラクターの行動ルーチン (コルーチン)
/// </summary>
private IEnumerator CharacterRoutine()
{
Debug.Log("キャラクター移動開始");
// 移動処理 (2秒間)
Vector3 destination = new Vector3(5, 0, 0);
yield return StartCoroutine(MoveCharacter(character, destination, 2f));
Debug.Log("移動完了、攻撃準備中...");
// UniTaskをコルーチン内で呼び出し
yield return PerformAttack(character).ToCoroutine();
Debug.Log("キャラクター行動終了");
}
/// <summary>
/// キャラクターを移動させるコルーチン
/// </summary>
private IEnumerator MoveCharacter(Transform target, Vector3 destination, float duration)
{
Vector3 start = target.position;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
target.position = Vector3.Lerp(start, destination, elapsed / duration);
yield return null; // 次のフレームまで待つ
}
target.position = destination;
}
/// <summary>
/// 攻撃を行う非同期処理 (UniTask)
/// </summary>
private async UniTask PerformAttack(Transform attacker)
{
Debug.Log("攻撃開始...");
await UniTask.Delay(1000); // 1秒待機
Debug.Log($"{attacker.name}が攻撃しました!");
// 攻撃エフェクトやロジックをここに記述
}
コメント